Source: https://cli.nylas.com/guides/vercel-ai-sdk-email-tools

# Email Tools for the Vercel AI SDK

The Vercel AI SDK gives a model typed tools through its tool() helper and a Zod schema, then runs the tool calls for you inside generateText. To add email, you don't need a provider SDK — a tool whose execute() shells out to the Nylas CLI returns JSON the model can read and reaches six providers from one connection. Here's how to define the tools and wire them into a tool-calling loop in TypeScript.

Written by [Pouya Sanooei](https://cli.nylas.com/authors/pouya-sanooei) Software Engineer

Updated June 8, 2026

> **TL;DR:** Define a Vercel AI SDK `tool()` with a Zod input schema whose `execute()` runs the Nylas CLI through Node's `child_process` and returns the JSON. Pass it to `generateText` and the model calls it when it needs the inbox. One tool reaches six providers with no provider SDK. Make sends a separate, guarded tool — or a draft step — so the model can't mail unattended.

Command references used in this guide: [`nylas email list`](https://cli.nylas.com/docs/commands/email-list), [`nylas email search`](https://cli.nylas.com/docs/commands/email-search), and [`nylas email drafts create`](https://cli.nylas.com/docs/commands/email-drafts-create).

## How do you give the Vercel AI SDK email tools?

You give the AI SDK email by defining a `tool()` whose `execute` function shells out to the Nylas CLI. The SDK handles the model's tool-call protocol; your job is the implementation. Inside `execute`, you run `nylas email list --json` with Node's `child_process`, parse stdout, and return it — the SDK passes the result back to the model automatically.

A [Zod](https://zod.dev/) schema on the tool's `parameters` types the inputs, so the model must supply a valid `limit` or `query` before the tool runs. The CLI must be installed on the host and authenticated once with `nylas auth login`. The [AI SDK tools documentation](https://sdk.vercel.ai/docs/foundations/tools) covers the `tool()` contract.

## How do you define the email tools?

Define one tool per capability with a tight Zod schema. The reader tool takes a `limit` and runs `nylas email list --json`; a search tool takes a `query`. Promisify `execFile` so `execute` can `await` the CLI, and return the parsed JSON so the model receives structured data rather than a string blob.

```typescript
import { tool } from "ai";
import { z } from "zod";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const run = promisify(execFile);

export const readInbox = tool({
  description: "List recent emails as JSON.",
  parameters: z.object({ limit: z.number().min(1).max(50).default(10) }),
  execute: async ({ limit }) => {
    const { stdout } = await run("nylas", ["email", "list", "--json", "--limit", String(limit)]);
    return JSON.parse(stdout);
  },
});

export const searchInbox = tool({
  description: "Search the mailbox server-side and return matching messages.",
  parameters: z.object({ query: z.string() }),
  execute: async ({ query }) => {
    const { stdout } = await run("nylas", ["email", "search", query, "--json", "--limit", "20"]);
    return JSON.parse(stdout);
  },
});
```

## How do you use the tools in a tool-calling loop?

Pass the tools to [`generateText`](https://sdk.vercel.ai/docs/ai-sdk-core/generating-text) and set `maxSteps` so the model can call a tool, read the result, and respond in one round. The SDK runs each requested tool, feeds the JSON back, and continues until the model produces a final answer. The example asks the model to triage the inbox, which it does by calling `readInbox` and reasoning over the result.

```typescript
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";

const { text } = await generateText({
  model: openai("gpt-4o"),
  tools: { readInbox, searchInbox },
  maxSteps: 3,
  prompt: "Summarize my 15 most recent emails and flag the three that need a reply today. "
        + "Do not send anything.",
});
console.log(text);
```

## Why use the CLI instead of a provider SDK?

The CLI gives one tool across six providers. A provider SDK ties the tool to a single backend and makes you handle that provider's OAuth and response shapes; a second provider means a second integration. The CLI's connected grant determines the backend, so the same `readInbox` tool works against Gmail today and Outlook tomorrow with no code change. Output is uniform JSON regardless of provider.

It also keeps the agent server-only. Because the tool runs `nylas` through `child_process`, it belongs in a server action or route handler, never the browser — which is where a tool touching a real mailbox should live anyway. Make sure the CLI binary is available in your deployment's runtime, not just local dev.

## What guardrails should the agent have?

Keep sending separate and guarded. Rather than a send tool the model can fire, expose a draft tool that runs `nylas email drafts create` and returns a draft ID for a person to review. A misread message then produces a draft, not a sent email. If you do expose sending, gate it behind an explicit user confirmation in your UI, not the model's judgment.

Email bodies are untrusted input: a message can contain text aimed at the model, so never let the agent act on content as if it were an instruction. Validate tool inputs with the Zod schema, log each call, and verify before any send. See [build a human-in-the-loop email agent](https://cli.nylas.com/guides/build-human-in-loop-email-agent) and [email prompt injection defense](https://cli.nylas.com/guides/email-prompt-injection-defense) for the patterns.

## Next steps

- [Build a CrewAI email agent](https://cli.nylas.com/guides/crewai-email-agent) — the Python equivalent of this pattern
- [Send email from Node.js](https://cli.nylas.com/guides/send-email-nodejs) — the CLI from Node without an agent
- [Build a human-in-the-loop email agent](https://cli.nylas.com/guides/build-human-in-loop-email-agent) — review and approval flows
- [Email prompt injection defense](https://cli.nylas.com/guides/email-prompt-injection-defense) — untrusted message bodies
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
