Defining AI Tools in the AI SDK

development

Defines AI tools for Vercel AI SDK using tool() helper and Zod schemas. Use when adding new tools to chat, building agentic capabilities, or implementing function calling. Covers inputSchema, execute, and tool registration patterns.

Defining AI Tools

Patterns for creating tools that AI models can invoke via Vercel AI SDK.

Quick Start

Define a tool with tool() helper and Zod schema:

import { tool } from 'ai';
import { z } from 'zod';

export const weatherTool = tool({
  description: 'Get the weather in a location',
  inputSchema: z.object({
    location: z.string().describe('City name'),
    unit: z.enum(['C', 'F']).optional().default('F'),
  }),
  execute: async ({ location, unit }) => ({
    location,
    temperature: 72,
    unit,
    conditions: 'sunny',
  }),
});

Key points:

  • description: Helps model understand when to use the tool
  • inputSchema: Zod schema for type-safe input validation
  • execute: Async function that runs when tool is called
  • TypeScript infers execute parameter types from inputSchema

Tool Definition Structure

Required Properties

tool({
  description: string,      // What the tool does (model sees this)
  inputSchema: ZodSchema,   // Zod schema for parameters
  execute: async (args) => result,  // Implementation
})

Input Schema Patterns

Use .describe() on fields to help the model:

inputSchema: z.object({
  query: z.string().describe('Search query to find documents'),
  limit: z.number().int().min(1).max(100).default(10).describe('Max results'),
  filters: z.object({
    startDate: z.string().optional().describe('ISO date for range start'),
    endDate: z.string().optional().describe('ISO date for range end'),
  }).optional(),
})

Optional Parameters with Defaults

inputSchema: z
  .object({
    query: z.string().optional().default(''),
    topK: z.number().int().min(3).max(30).optional().default(15),
  })
  .default({})  // Entire schema optional

Factory Pattern for Context

Wrap tools in factory functions when they need session/context:

import { tool } from 'ai';
import { z } from 'zod';
import type { AppSession } from '@/lib/auth/session';

export function docSearchTool({
  session,
  matterId,
}: {
  session: AppSession;
  matterId?: string | null;
}) {
  return tool({
    description: 'Search user documents for relevant passages',
    inputSchema: z.object({
      query: z.string().describe('Search query'),
      topK: z.number().int().min(3).max(30).optional().default(15),
    }),
    execute: async ({ query, topK }) => {
      // Use session.user.id and matterId in implementation
      const results = await search({ query, userId: session.user.id, matterId });
      return { query, results };
    },
  });
}

Registering Tools with streamText

Pass tools object to streamText:

import { streamText, stepCountIs } from 'ai';

const result = streamText({
  model: gateway.languageModel(modelId),
  messages: await convertToModelMessages(messages),
  tools: {
    doc_search: docSearchTool({ session, matterId }),
    web_search: webSearchTool(),
    create_document: createDocumentTool({ session, dataStream }),
  },
  stopWhen: stepCountIs(50),
});

Tool Bundles

Group related tools into bundles:

export function filesystemTools({ session, matterId }: ToolContext) {
  return {
    tools: {
      list_files: listFilesTool({ session, matterId }),
      read_file: readFileTool({ session, matterId }),
      write_file: writeFileTool({ session, matterId }),
    },
    activeToolNames: ['list_files', 'read_file', 'write_file'],
  };
}

// In route:
const fsBundle = filesystemTools({ session, matterId });
const allTools = { ...baseTools, ...fsBundle.tools };

Execute Function Patterns

Returning Structured Data

execute: async ({ documentId }) => {
  const doc = await getDocument(documentId);
  return {
    id: doc.id,
    title: doc.title,
    content: doc.content.substring(0, 1000), // Limit for context
    metadata: { pages: doc.pageCount, type: doc.mimeType },
  };
}

Error Handling

Return errors as data, not exceptions (model can retry):

execute: async ({ query }) => {
  try {
    const results = await search(query);
    return { success: true, results };
  } catch (err) {
    return {
      success: false,
      error: err instanceof Error ? err.message : 'Search failed',
    };
  }
}

Streaming Side Effects via dataStream

For tools that update UI state:

export function createDocumentTool({
  session,
  dataStream,
}: {
  session: AppSession;
  dataStream: UIMessageWriter;
}) {
  return tool({
    description: 'Create a new document',
    inputSchema: z.object({
      title: z.string(),
      content: z.string(),
    }),
    execute: async ({ title, content }) => {
      const doc = await saveDocument({ title, content, userId: session.user.id });

      // Notify client of document creation
      dataStream.write({
        type: 'data-document-created',
        id: `doc-${doc.id}`,
        data: { id: doc.id, title },
      });

      return { success: true, documentId: doc.id };
    },
  });
}

Common Pitfalls

❌ Tools Without Execute

Problem: Defining tools without execute function.

Why it's wrong: Without execute, tool calls are sent to client for handling. This breaks multi-step execution and requires client-side logic.

Fix: Always define execute for server-side tools. The SDK runs them automatically.

❌ Invalid Schema Types

Problem: Using non-object schemas as inputSchema.

Why it's wrong: AI SDK requires inputSchema.type === 'object'. Arrays, primitives, or unions at root level fail.

Fix: Always wrap in z.object({}), even for single parameters:

// ❌ Wrong
inputSchema: z.string()

// ✅ Correct
inputSchema: z.object({ value: z.string() })

❌ Throwing Exceptions in Execute

Problem: Throwing errors from execute function.

Why it's wrong: Exceptions break the tool chain and may cause unclear errors.

Fix: Return error objects; let the model decide how to proceed:

execute: async (args) => {
  try {
    return await doThing(args);
  } catch (err) {
    return { error: err.message, suggestion: 'Try a different approach' };
  }
}

❌ Overly Long Descriptions

Problem: Multi-paragraph tool descriptions.

Why it's wrong: Wastes context tokens; model needs concise guidance.

Fix: One sentence describing what + when:

// ❌ Wrong
description: `This tool searches through all of the user's uploaded 
documents to find relevant passages. It uses semantic search with 
embeddings and returns the most relevant chunks. Use this when...`

// ✅ Correct  
description: 'Search user documents for relevant passages. Prefer over web_search for document-specific facts.'

Type Inference

TypeScript infers execute parameter types from inputSchema:

const myTool = tool({
  inputSchema: z.object({
    name: z.string(),
    count: z.number().optional(),
  }),
  execute: async ({ name, count }) => {
    // name: string, count: number | undefined (inferred)
  },
});

For exported tools, use InferUITools:

import type { InferUITools, ToolSet } from 'ai';

const tools = {
  weather: weatherTool,
  search: searchTool,
} satisfies ToolSet;

export type ChatTools = InferUITools<typeof tools>;
  • using-ai-sdk-streams: How to use tools with streamText
  • persisting-chat-sessions: Storing tool results
  • building-ai-agent: Agent-specific tool patterns

Danger Zone