Send & Receive Emails with Vercel AI SDK
Use the AgentMail toolkit directly in the Vercel AI SDK to send and receive emails
Agents built with the Vercel AI SDK get email through the agentmail-toolkit package. This page walks you through an agent that creates its own @agentmail.to inbox, sends an email, and replies to one it receives, first from a script and then from a Next.js route that fires when mail arrives.
0. Get API keys
AGENTMAIL_API_KEYauthenticates the email tools. Generate one from the AgentMail Console.OPENAI_API_KEYauthenticates the model calls behindgenerateText. Generate one from the OpenAI dashboard.
export AGENTMAIL_API_KEY="<API_KEY>"
export OPENAI_API_KEY="<OPENAI_API_KEY>"If you installed AgentMail through the Vercel Marketplace, your project’s environment already carries the AgentMail credentials. Marketplaces and provisioning covers that setup.
1. Install and load the tools
Install the toolkit, the AI SDK, and a model provider. The toolkit builds its tools for AI SDK 6, so pin that major along with @ai-sdk/openai 3, the provider major that matches it:
npm install agentmail-toolkit ai@6 @ai-sdk/openai@3
npm install -D tsx
npm pkg set type=moduletsx runs the TypeScript scripts in the next steps, and "type": "module" lets them use top-level await.
Define the agent in agent.ts. The toolkit’s getTools() returns the email tools as the AI SDK’s own Tool type, keyed by tool name, so the object goes straight into tools::
import { openai } from "@ai-sdk/openai";
import { AgentMailToolkit } from "agentmail-toolkit/ai-sdk";
import { generateText, stepCountIs } from "ai";
export async function runEmailAgent(prompt: string) {
const result = await generateText({
model: openai.chat("gpt-4o"),
system:
"You are an email agent with your own AgentMail inbox. " +
"You can create inboxes, send email, and read and reply to threads.",
prompt,
tools: new AgentMailToolkit().getTools(),
stopWhen: stepCountIs(10),
});
return result.text;
}stopWhen: stepCountIs(10) is what turns one generation into an agent loop. After each tool call the model runs again with the result, up to ten steps, which leaves room for several email operations plus a final answer. The same tools object works in streamText when you want to stream the answer instead.
openai.chat("gpt-4o") picks the Chat Completions API on purpose: through the provider’s default Responses API, the model fills every optional tool field with empty strings, values the listing tools reject. The Troubleshooting accordion at the end of this page shows what that failure looks like.
new AgentMailToolkit() reads AGENTMAIL_API_KEY from the environment. To supply credentials differently, hand it a configured client: new AgentMailToolkit(new AgentMailClient({ apiKey: "..." })).
The hosted MCP server described in MCP and Skills is an alternative to the toolkit if you prefer to connect that way.
2. Test sending emails
First job for the agent: create an inbox and send an email to an account you already read, Gmail, Outlook, whichever you use. Save this next to agent.ts and run it with npx tsx send.ts.
Within seconds, that account receives a message from an @agentmail.to address that AgentMail just created.
import { runEmailAgent } from "./agent";
const output = await runEmailAgent(
"Create an inbox with the display name 'Support agent', then send an email " +
"from it to you@example.com with the subject 'Hello from my agent' and a " +
"one-line introduction. Include the new inbox address in your final answer.",
);
console.log(output);That answer comes from two tool calls, create_inbox and then send_message, followed by a summary:
Done. Your new inbox is faircost773@agentmail.to, and I sent the email to
you@example.com with the subject "Hello from my agent".Note the inbox address in the output. The next step sends mail to it.
3. Test receiving and replying
Now give the agent something to answer. From your own account, email the address from step 2 with a subject line and a question.
Then run the script below to have the agent read and answer it. The reply arrives back in your account, threaded under the message you sent.
import { runEmailAgent } from "./agent";
const output = await runEmailAgent(
"Check inbox <inbox_id>, read its newest thread, and reply to it briefly " +
"in the same thread.",
);
console.log(output);Swap the address from step 2 in for <inbox_id>. Nothing carries over between runs, so the prompt has to name the inbox itself.
To answer, the agent chains list_threads, get_thread, and reply_to_message. Delivery takes about two seconds in each direction. An agent that reports an empty inbox most likely ran before your email arrived, so run it again.
Scope the agent’s tools
getTools() with no arguments returns the full toolkit. Pass an array of names to hand over only what the job calls for:
const tools = new AgentMailToolkit().getTools([
"list_threads",
"get_thread",
"reply_to_message",
]);This scoped set limits the agent to reading and answering mail in inboxes that already exist.
Reply as email arrives
The walkthrough prompts the agent by hand. Deployed on Vercel, your project can react on its own: AgentMail sends a signed POST to a route in your app whenever the inbox receives an email, and the route runs the agent to answer it.
You need a webhook subscription pointing at the route’s public URL, with its signing secret stored as AGENTMAIL_WEBHOOK_SECRET. Webhooks covers creating the subscription, choosing events, and handling retried deliveries. Install the verification library with npm install svix.
import { openai } from "@ai-sdk/openai";
import { AgentMailToolkit } from "agentmail-toolkit/ai-sdk";
import { generateText, stepCountIs } from "ai";
import { Webhook } from "svix";
const verifier = new Webhook(process.env.AGENTMAIL_WEBHOOK_SECRET!);
type ReceivedEvent = {
event_type: string;
message: { inbox_id: string; thread_id: string };
};
export async function POST(request: Request) {
const payload = await request.text();
let event: ReceivedEvent;
try {
event = verifier.verify(payload, {
"svix-id": request.headers.get("svix-id") ?? "",
"svix-timestamp": request.headers.get("svix-timestamp") ?? "",
"svix-signature": request.headers.get("svix-signature") ?? "",
}) as ReceivedEvent;
} catch {
return new Response("invalid signature", { status: 400 });
}
if (event.event_type === "message.received") {
await generateText({
model: openai.chat("gpt-4o"),
system:
"You answer email sent to this inbox. Keep replies short and stay in the same thread.",
prompt:
`Inbox ${event.message.inbox_id} received an email in thread ` +
`${event.message.thread_id}. Read the thread and reply to its latest message.`,
tools: new AgentMailToolkit().getTools(["get_thread", "reply_to_message"]),
stopWhen: stepCountIs(5),
});
}
return new Response(null, { status: 200 });
}The handler reads the raw body before anything else because the signature is computed over the exact bytes. A message.received event names the inbox_id and thread_id that changed, so the prompt hands both to the agent, and the toolkit is scoped to the two tools this job needs: get_thread to read the conversation and reply_to_message to answer it.