n4nAI

Define tools with Zod schemas in the Vercel AI SDK

Learn how to define tools with Zod schemas in the Vercel AI SDK for type-safe function calling. Step-by-step guide with runnable TypeScript code.

n4n Team3 min read659 words

Audio narration

Coming soon — every post will get a voice note here.

The cleanest way to build type-safe function calling in TypeScript is through a vercel ai sdk zod schema tool definition. By declaring your tool’s input shape with Zod, you get runtime validation, inferred types, and a JSON Schema that the model provider can consume without extra boilerplate.

Step 1: Install the required packages

You need the AI SDK core, a model provider package, and Zod. The examples below use the OpenAI provider adapter, but the pattern is identical for other adapters.

npm install ai @ai-sdk/openai zod
# or pnpm/yarn

Pin zod to v3.x. The AI SDK’s JSON Schema converter expects standard Zod primitives; Zod v4 may work but is not yet fully aligned with the bundled converter. Use Node 18+ or Edge runtime.

Step 2: Define your Zod schema for tool parameters

A tool is only as safe as its input contract. Write a z.object that describes exactly what the model must pass. Use .describe() on fields—the AI SDK forwards these as JSON Schema description keys, which improves model accuracy.

// tools/weather.ts
import { z } from 'zod';

export const weatherParams = z.object({
  location: z
    .string()
    .describe('City and country, e.g. "Berlin, Germany"'),
  unit: z
    .enum(['celsius', 'fahrenheit'])
    .default('celsius')
    .describe('Temperature unit'),
});

This weatherParams schema is your single source of truth. The inferred type z.infer<typeof weatherParams> is what your execute function will receive.

Step 3: Create the tool with the tool helper

The tool function from ai wraps the schema and an async execute method. The SDK validates incoming arguments against the schema before calling execute; if validation fails, the model gets a structured error instead of crashing your runtime.

// tools/weather.ts
import { tool } from 'ai';
import { z } from 'zod';

export const weatherParams = z.object({
  location: z.string().describe('City and country'),
  unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
});

export const weatherTool = tool({
  parameters: weatherParams,
  execute: async ({ location, unit }) => {
    // Replace with a real fetch to a weather API
    const fakeTemp = 22;
    return {
      location,
      unit,
      temperature: fakeTemp,
      condition: 'sunny',
    };
  },
});

The execute return value must be serializable. The SDK sends it back to the model as the tool result, so keep it concise.

Step 4: Wire the tool into a model call

Instantiate a model and pass your tools to generateText (or streamText). Set maxSteps to allow the model to call the tool and then continue generating based on the result.

// main.ts
import { generateText } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
import { weatherTool } from './tools/weather';

const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });

const { text, toolCalls, toolResults } = await generateText({
  model: openai('gpt-4o'),
  tools: { weather: weatherTool },
  maxSteps: 3,
  prompt: 'What is the weather in Berlin in celsius?',
});

console.log(text);
console.log('tool calls:', toolCalls);

If you point the SDK at a gateway such as n4n.ai, which exposes a single OpenAI-compatible endpoint for 240+ models with automatic fallback, your vercel ai sdk zod schema tool definition works unchanged—just set baseURL and apiKey on createOpenAI and pick any supported model id.

Step 5: Handle multi-step execution and errors

With maxSteps > 1, the SDK loops: model emits a tool call → SDK validates and executes → result is fed back → model generates final answer. You do not need to manually invoke execute. If you need manual control (e.g., human-in-the-loop), omit execute from the tool and inspect toolCalls from the response, then call your function yourself.

// manual pattern (no execute in tool)
const result = await generateText({
  model: openai('gpt-4o'),
  tools: { weather: tool({ description: '...', parameters: weatherParams }) },
  prompt: 'Weather in Berlin?',
});

for (const call of result.toolCalls ?? []) {
  if (call.toolName === 'weather') {
    const args = weatherParams.parse(call.args); // explicit validation
    const data = await fetchWeather(args);
    // send data back in a follow-up generateText call
  }
}

Always wrap external calls in try/catch inside execute. The SDK will serialize the caught error message and return it to the model, letting it recover or explain the failure.

Step 6: Verify success

Create a script and run it with tsx or ts-node:

npx tsx main.ts

Success criteria:

  1. The process exits 0.
  2. toolCalls contains one entry with toolName: 'weather' and parsed args match { location: 'Berlin, Germany', unit: 'celsius' }.
  3. text includes a natural language answer referencing the mocked temperature (22°).
  4. If you change the schema (e.g., make location a z.number()), the run fails validation before execute runs—proving the guard works.

Add a unit test with vitest to lock this behavior:

import { expect, test } from 'vitest';
import { weatherParams } from './tools/weather';

test('rejects missing location', () => {
  const r = weatherParams.safeParse({ unit: 'celsius' });
  expect(r.success).toBe(false);
});

Schema constraints you must respect

The AI SDK converts Zod to JSON Schema via zod-to-json-schema. Not every Zod feature survives that translation:

  • Avoid z.transform(), z.preprocess(), or .refine() with side effects—they are ignored for the model-facing schema.
  • Use z.enum() over z.union() of strings when possible; some providers mishandle complex unions.
  • z.record() and z.array() are fine, but deeply nested maps can blow up token counts in the system prompt.
  • Default values work, but the model may still explicitly pass them. Your execute should not assume omission.

A vercel ai sdk zod schema tool definition is the boundary between unstructured model output and your typed codebase. Treat the schema as a strict interface, and the rest of your pipeline stays safe.

Streaming with tools

For chat UIs, use streamText the same way. Tool calls appear in the streamed toolCalls part; the SDK still executes them when execute is provided and maxSteps allows. You can surface partial tool status to the client by reading stream.toolCalls.

import { streamText } from 'ai';

const stream = streamText({
  model: openai('gpt-4o'),
  tools: { weather: weatherTool },
  maxSteps: 3,
  prompt: 'Weather in Paris?',
});

for await (const chunk of stream.textStream) {
  process.stdout.write(chunk);
}

That is the full loop: declare schema, wrap in tool, attach to a model call, let the SDK validate and execute, then verify against the criteria above.

Tagsvercel-ai-sdkzodtool-callingschema

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All vercel ai sdk tool & function calling posts →