Guide

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 Software Engineer

VerifiedCLI 3.1.16 · Gmail · last tested June 8, 2026

Command references used in this guide: nylas email list, nylas email search, and nylas 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 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 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.

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 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.

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 and email prompt injection defense for the patterns.

Next steps