Jev’s new System One model dropped and it’s changing GTM agents in a massive way. Jev is an extremely cost effective way to gauge LLM responses, letting us have an intelligent set of eyes sit on top of our existing models monitoring every single generation, and helping us select the best options.
In GTM this can mean leveling up our entire agent stack.
Our email outbounding tool can have Jev sit on top and ensure emails meet both copy guidelines (think subject length, word choice), as well as sales play guidelines i.e. does the generated email speak to the reason we’re reaching out.
There is a massive asymmetry at play here.
Teams using these advanced Jev-based arbiters in their workflows will have dramatically better LLM outputs. Meaning, those teams that adopt this tool in their agents are going to have much more consistent output quality at scale for all their agents be it a lead classifier, an internal knowledge agent, or an outbounding agent.
The only gap here is knowledge. Jev’s low price point combined with open source LLMs can mean ambitious, effective agents without shelling over thousands or tens of thousands of dollars to the top AI labs for state of the art model access.
Putting this into reality, lets be ambitious in today’s post.
We’ll walk through setting up a fully end to end automated agent for sending and replying to a lead list with the goal of booking a meeting.
This’ll be useful for anyone in a GTM related role, whether you’re on a small start up with a tight budget, or a fully VC-backed GTM team.
End to End Meetings Booked Agent
Reaching out to lower tier leads can have questionable ROI, but if we can work them automatically then ROI suddenly doesn’t matter. Any meeting booked from our system is free money.
What we’ll do is use a combination of existing LLMs and Jev to classify leads, generate email outbound, and handle replies to get leads to book a meeting.
The simplest way to do this to start is setting up a new Next.js project. For this build out, we’ll jump straight to code for one big reason -- coding agents. A code-first approach may feel heavy handed, but with Claude Code or similar agents we can use plain English to guide the initial experiment with more flexibility than using a traditional no-code tool.
Leads
We’ve written before on building lead lists with job finders. We’ll use this playbook again for this initiative. Here we’ll do the same thing and use a tool like Sumble or CoreSignal to pull a list of contact emails for companies with job posts connected to our product or service.
Workflows
We’re going to use Vercel Workflows to spin up two core capabilities -- sending an initial email, and responding to replies.
After uploading our list, we’ll use the first workflow to classify, enrich, and send off our initial messages using the Gmail API.
For our second workflow handling replies, we’ll setup a pub/sub on the Gmail API to ping an endpoint on our Next.js app. Our endpoint will find the appropriate lead, and generate a response geared to booking a meeting.
In summary, this is what we’re building towards.

Code
Let’s get tangible and work through some code snippets to build up this agent.
Lead Object and Storage
We’ll setup our leads object to fit this type, ensuring we have some decent enough baseline to generate our future copy with:
// lib/types.ts
export type Lead = {
id: string;
email: string;
firstName: string;
role: string;
company: string;
assumedFit: "high" | "medium" | "low";
companySummary: string;
jobSignal: {
title: string;
description: string;
url: string;
};
status: "queued" | "sent" | "manual_review" | "stopped";
gmailThreadId?: string;
// Treat “four back and forths” as at most four automated replies.
agentReplyCount: number;
};To keep things light from an infrastructure perspective, we’ll choose to store these lead objects in JSON, and upload the file to Vercel Blob. For this we’ll just need a Vercel Blob read write token, and some path we’d like to store this file under:
// lib/lead-store.ts
import { put } from "@vercel/blob";
import type { Lead } from "./types";
const pathname = "gtm/leads.json";
const blobUrl = process.env.LEADS_BLOB_URL!;
export async function loadLeads(): Promise<Lead[]> {
const response = await fetch(blobUrl, {
cache: "no-store",
headers: {
Authorization: `Bearer ${process.env.BLOB_READ_WRITE_TOKEN}`,
},
});
if (!response.ok) throw new Error(`Lead read failed: ${response.status}`);
return response.json();
}
export async function saveLeads(leads: Lead[]) {
await put(pathname, JSON.stringify(leads, null, 2), {
access: "private",
allowOverwrite: true,
contentType: "application/json",
});
}The Jev Arbiter
Jev’s new System One model will evaluate all email copy generated by our LLM model of choice, and pick the best option.
This step is the alpha.
We’re no longer throwing a prompt across the fence and hoping the LLM returns something that’s usable, and won’t cause us blowback down the road.
Every single email generation gets a structured review we can take action. In this case, we’ll generate 3 emails and let Jev decide which the best one. Another approach is to regenerate email copy one at a time until Jev is happy with the output.
In this case, because we’re using a lower cost LLM we can generate multiple attempts in one go.
Our evaluation code with Jev:
// lib/evaluate-outbound.ts
import { experimental_evaluate as evaluate } from "ai";
import type { Lead } from "./types";
export async function evaluateOutbound(lead: Lead, message: string) {
const result = await evaluate({
model: "typesafe-ai/jev",
state: { prospect: lead, outboundMessage: message },
questions: {
concise: {
type: "boolean",
instructions:
"Is the message short, specific, easy to scan, and free of filler?",
},
notTooSalesy: {
type: "boolean",
instructions:
"Does it avoid hype, pressure, fake familiarity, and unsupported claims?",
},
companyAligned: {
type: "boolean",
instructions:
"Does the message connect the supplied job signal to a plausible company need?",
},
roleAligned: {
type: "boolean",
instructions:
"Is the message relevant to this prospect’s role rather than merely personalized by name?",
},
},
providerOptions: {
gateway: { zeroDataRetention: true },
},
});
const answers = result.answers;
const score = Object.values(answers).filter(Boolean).length;
return {
answers,
score,
approved: score === Object.keys(answers).length,
};
}And the final email copy generation flow:
// lib/create-outbound.ts
import { generateText } from "ai";
import type { Lead } from "./types";
import { evaluateOutbound } from "./evaluate-outbound";
async function draftMessage(lead: Lead) {
const { text } = await generateText({
model: "deepseek/deepseek-v4.1-flash",
system: `You write plain-text cold emails sent by an obvious AI sales assistant.
Keep the email under 80 words. Use no invented facts, hype, or fake familiarity.
Connect the job posting to one plausible job to be done. End with one low-pressure question.
Sign as “ShipGTM AI Assistant” from sales@company.com.`,
prompt: JSON.stringify(lead),
});
return text.trim();
}
export async function createApprovedOutbound(lead: Lead) {
const drafts = await Promise.all([
draftMessage(lead),
draftMessage(lead),
draftMessage(lead),
]);
const judged = await Promise.all(
drafts.map(async (message) => ({
message,
evaluation: await evaluateOutbound(lead, message),
})),
);
return (
judged
.filter((candidate) => candidate.evaluation.approved)
.sort((a, b) => b.evaluation.score - a.evaluation.score)[0] ?? null
);
}Gmail API
With the core capability in place, lets move on now to the Gmail API for sending and receiving email.
Our general preference here is default to appearing as a bot, and not try to impersonate a human. Our logic here is that from a brand reputation perspective, it’s best to lead with honesty and transparency especially as outbound in it of itself is a low-trust communication channel.
We’ll start with an email alias like sales@company.com as an authorized Gmail sender or alias. The address and signature make the automation obvious instead of attempting to appear as a person.
// lib/gmail.ts
import { google } from "googleapis";
const oauth = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
);
oauth.setCredentials({ refresh_token: process.env.GOOGLE_REFRESH_TOKEN });
const gmail = google.gmail({ version: "v1", auth: oauth });
function encodeMessage(lines: string[]) {
return Buffer.from(lines.join("\r\n")).toString("base64url");
}
export async function sendInitialEmail(to: string, body: string) {
const raw = encodeMessage([
`From: ShipGTM AI Assistant <sales@company.com>`,
`To: ${to}`,
`Subject: Quick question`,
`Content-Type: text/plain; charset="UTF-8"`,
"",
body,
]);
return gmail.users.messages.send({
userId: "me",
requestBody: { raw },
});
}
export async function sendThreadReply(input: {
to: string;
subject: string;
body: string;
threadId: string;
messageId: string;
}) {
const raw = encodeMessage([
`From: ShipGTM AI Assistant <sales@company.com>`,
`To: ${input.to}`,
`Subject: Re: ${input.subject.replace(/^Re:\s*/i, "")}`,
`In-Reply-To: ${input.messageId}`,
`References: ${input.messageId}`,
`Content-Type: text/plain; charset="UTF-8"`,
"",
input.body,
]);
return gmail.users.messages.send({
userId: "me",
requestBody: { raw, threadId: input.threadId },
});
}
Workflow -- Initial Outbound Send

We move on to our first core workflow to send off our initial outbound messaging.
This workflow reads one lead, creates and judges multiple drafts, and only sends an approved message.
// workflows/outbound-lead.ts
import { createApprovedOutbound } from "@/lib/create-outbound";
import { sendInitialEmail } from "@/lib/gmail";
import { loadLeads, saveLeads } from "@/lib/lead-store";
async function prepareLead(leadId: string) {
"use step";
const leads = await loadLeads();
const lead = leads.find((item) => item.id === leadId);
if (!lead || lead.status !== "queued") return null;
const candidate = await createApprovedOutbound(lead);
if (!candidate) {
lead.status = "manual_review";
await saveLeads(leads);
return null;
}
return { lead, message: candidate.message };
}
async function deliver(leadId: string, email: string, message: string) {
"use step";
const sent = await sendInitialEmail(email, message);
const leads = await loadLeads();
const lead = leads.find((item) => item.id === leadId)!;
lead.status = "sent";
lead.gmailThreadId = sent.data.threadId ?? undefined;
await saveLeads(leads);
}
export async function outboundLead(leadId: string) {
"use workflow";
const prepared = await prepareLead(leadId);
if (!prepared) return { sent: false };
await deliver(prepared.lead.id, prepared.lead.email, prepared.message);
return { sent: true };
}Workflow -- Handling Replies
Here is our reply workflow. We’ll flesh out the helper functions below, but at a high level we are interpreting the reply, generating a response, then terminating the conversation after 4 follow ups in case the user is responding in bad faith.
Anything unsafe or unapproved is stopped or routed for review.
// workflows/handle-reply.ts
import { evaluateInbound } from "@/lib/evaluate-inbound";
import { createApprovedReply } from "@/lib/create-reply";
import { sendThreadReply } from "@/lib/gmail";
import { loadLeads, saveLeads } from "@/lib/lead-store";
export type InboundEmail = {
from: string;
subject: string;
text: string;
threadId: string;
messageId: string;
};
async function processInbound(email: InboundEmail) {
"use step";
const leads = await loadLeads();
const lead = leads.find(
(item) => item.email.toLowerCase() === email.from.toLowerCase(),
);
if (!lead) return { action: "ignore" as const };
if (lead.agentReplyCount >= 4) {
lead.status = "stopped";
await saveLeads(leads);
return { action: "limit_reached" as const };
}
const gate = await evaluateInbound(lead, email.text);
if (gate.answers.explicitOptOut) {
lead.status = "stopped";
await saveLeads(leads);
return { action: "opt_out" as const };
}
if (!gate.shouldReply) {
lead.status = "manual_review";
await saveLeads(leads);
return { action: "manual_review" as const };
}
const reply = await createApprovedReply(lead, email.text);
if (!reply) {
lead.status = "manual_review";
await saveLeads(leads);
return { action: "manual_review" as const };
}
await sendThreadReply({
to: lead.email,
subject: email.subject,
body: reply,
threadId: email.threadId,
messageId: email.messageId,
});
lead.agentReplyCount += 1;
await saveLeads(leads);
return { action: "replied" as const };
}
export async function handleReply(email: InboundEmail) {
"use workflow";
return processInbound(email);
}Jev Arbiter to Gate Inbound Requests
Again we reach for Jev to monitor incoming emails, and take the appropriate action. Opt-outs stop immediately. Complex, sensitive, unrelated, or underspecified requests go to a person rather than encouraging the model to improvise.
// lib/evaluate-inbound.ts
import { experimental_evaluate as evaluate } from "ai";
import type { Lead } from "./types";
export async function evaluateInbound(
lead: Lead,
inboundMessage: string,
) {
const result = await evaluate({
model: "typesafe-ai/jev",
state: { prospect: lead, inboundMessage },
questions: {
explicitOptOut: {
type: "boolean",
instructions:
"Does the sender ask to unsubscribe, stop, be removed, or otherwise end contact?",
},
safeToAutomate: {
type: "boolean",
instructions:
"Can this be handled using only the supplied company context, basic product facts, or a meeting link—without pricing negotiation, legal/security claims, or invented details?",
},
canAdvanceMeeting: {
type: "boolean",
instructions:
"Does the sender show interest, ask a surface-level question, or indicate scheduling intent that can reasonably be guided toward a meeting?",
},
needsHuman: {
type: "boolean",
instructions:
"Would a good response require judgment, negotiation, sensitive information, or knowledge absent from the supplied state?",
},
},
providerOptions: {
gateway: { zeroDataRetention: true },
},
});
const a = result.answers;
return {
answers: a,
shouldReply:
!a.explicitOptOut &&
a.safeToAutomate &&
a.canAdvanceMeeting &&
!a.needsHuman,
};
}Jev Arbiter to Maintain Reply Copy Quality
We again reach for Jev now to monitor our agent-generated replies. Our agent should answer basic questions, but every approved reply must stay grounded and gently move toward a booked meeting.
// lib/create-reply.ts
import { experimental_evaluate as evaluate, generateText } from "ai";
import type { Lead } from "./types";
export async function createApprovedReply(lead: Lead, inbound: string) {
const { text } = await generateText({
model: "deepseek/deepseek-v4.1-flash",
system: `You are the obvious AI assistant behind sales@company.com.
Answer only surface-level questions supported by the provided product context.
Never invent pricing, legal, security, integration, or implementation details.
Keep the reply under 100 words and guide interested prospects to this booking link:
${process.env.BOOKING_URL}
Sign as “ShipGTM AI Assistant.”`,
prompt: JSON.stringify({ lead, inbound }),
});
const judged = await evaluate({
model: "typesafe-ai/jev",
state: { prospect: lead, inboundMessage: inbound, proposedReply: text },
questions: {
answersQuestion: {
type: "boolean",
instructions:
"If a surface-level question was asked, does the reply answer it directly?",
},
grounded: {
type: "boolean",
instructions:
"Is every factual claim supported by the supplied state, with no invented details?",
},
guidesToMeeting: {
type: "boolean",
instructions:
"Does the reply give an interested prospect a clear, low-pressure next step to book a meeting?",
},
restrained: {
type: "boolean",
instructions:
"Is the reply concise, non-pushy, and limited to what an automated assistant should handle?",
},
},
providerOptions: {
gateway: { zeroDataRetention: true },
},
});
const approved = Object.values(judged.answers).every(Boolean);
return approved ? text.trim() : null;
}Kicking Off Workflows
Initial Email Outbound
To kick off this workflow, as well as to visualize the state of our leads, we can have our coding agent draft a simple UI for this. Our preference is a simple leads table, as well as some buttons to kick off the relevant workflows. Vercel will automatically handling provisioning, and provide status indicators for running workflows.
Handling Replies
This workflow should be kicked off automatically via the Gmail API’s pub/sub notification system.
A Gmail push notification contains a mailbox address and `historyId`, not the full email. The endpoint acknowledges quickly; production code uses that history ID to fetch new messages, parses the relevant inbound email, and starts one reply workflow per message.
// app/api/gmail/push/route.ts
import { start } from "workflow/api";
import { handleReply } from "@/workflows/handle-reply";
import { getNewInboundMessages } from "@/lib/gmail-history";
export async function POST(request: Request) {
const envelope = await request.json();
const notification = JSON.parse(
Buffer.from(envelope.message.data, "base64").toString("utf8"),
) as { emailAddress: string; historyId: string };
// This helper calls users.history.list from the last persisted historyId,
// then users.messages.get(format: "full") for each newly added message.
const messages = await getNewInboundMessages(notification.historyId);
await Promise.all(
messages.map((message) => start(handleReply, [message])),
);
return new Response(null, { status: 204 });
}Where to Start
Generation makes an agent possible. Judgment makes it impressive.
We’ve worked through a lot of code that should give any coding agent enough context to kick off an agent like this for your team.
To simplify it even further, start with scoring your existing LLM model outputs using Jev via in the AI gateway to gauge how well your existing prompts are working.
Later, get more ambitious and start actioning on those scores -- terminating LLM responses that don’t meet requirements, taking more human-like actions like updating the CRM or sending outbound.
Then, work to start building out more ambitious projects like full end to end agents like the one we’ve outlined in this post.




