Defining AI Tools in the AI SDK
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 toolinputSchema: Zod schema for type-safe input validationexecute: 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>;
Related Skills
- using-ai-sdk-streams: How to use tools with streamText
- persisting-chat-sessions: Storing tool results
- building-ai-agent: Agent-specific tool patterns