Tools allow the AI to perform actions beyond text generation, such as fetching data from APIs, creating documents, or interacting with external services. The Vercel AI Chatbot includes several built-in tools that you can use as examples for creating your own.
Tools are defined using the tool function from the Vercel AI SDK and use Zod schemas for input validation.
Here’s the structure of the weather tool:
lib/ai/tools/get-weather.ts
import { tool } from "ai" ;
import { z } from "zod" ;
export const getWeather = tool ({
description: "Get the current weather at a location. You can provide either coordinates or a city name." ,
inputSchema: z . object ({
latitude: z . number (). optional (),
longitude: z . number (). optional (),
city: z . string ()
. describe ( "City name (e.g., 'San Francisco', 'New York', 'London')" )
. optional (),
}),
needsApproval: true ,
execute : async ( input ) => {
// Tool implementation
const response = await fetch (
`https://api.open-meteo.com/v1/forecast?latitude= ${ latitude } &longitude= ${ longitude } ¤t=temperature_2m`
);
const weatherData = await response . json ();
return weatherData ;
},
});
Key properties
description A clear description that helps the AI understand when to use this tool. Be specific about the tool’s purpose and parameters.
inputSchema A Zod schema defining the tool’s parameters. Use .describe() to provide context for each parameter.
needsApproval When true, the user must approve the tool call before execution. Recommended for tools that make external requests or modify data.
execute An async function that performs the tool’s action and returns a result. This can be any serializable data.
Create the tool file
Create a new file in lib/ai/tools/ for your tool: touch lib/ai/tools/get-stock-price.ts
Define the tool
Implement your tool with proper input validation: lib/ai/tools/get-stock-price.ts
import { tool } from "ai" ;
import { z } from "zod" ;
export const getStockPrice = tool ({
description: "Get the current stock price for a given ticker symbol. Use this when users ask about stock prices or market data." ,
inputSchema: z . object ({
symbol: z . string ()
. describe ( "Stock ticker symbol (e.g., 'AAPL', 'GOOGL', 'MSFT')" ),
}),
needsApproval: true ,
execute : async ({ symbol }) => {
try {
const response = await fetch (
`https://api.example.com/quote/ ${ symbol . toUpperCase () } `
);
if ( ! response . ok ) {
return {
error: `Could not find stock data for symbol " ${ symbol } ".` ,
};
}
const data = await response . json ();
return {
symbol: data . symbol ,
price: data . price ,
change: data . change ,
changePercent: data . changePercent ,
timestamp: new Date (). toISOString (),
};
} catch ( error ) {
return {
error: "Failed to fetch stock data. Please try again." ,
};
}
},
});
Register the tool
Add your tool to the chat API route: app/(chat)/api/chat/route.ts
import { getStockPrice } from "@/lib/ai/tools/get-stock-price" ;
const result = streamText ({
model: getLanguageModel ( selectedChatModel ),
system: systemPrompt ({ selectedChatModel , requestHints }),
messages: modelMessages ,
experimental_activeTools: [
"getWeather" ,
"createDocument" ,
"updateDocument" ,
"requestSuggestions" ,
"getStockPrice" , // Add your tool
],
tools: {
getWeather ,
createDocument: createDocument ({ session , dataStream }),
updateDocument: updateDocument ({ session , dataStream }),
requestSuggestions: requestSuggestions ({ session , dataStream }),
getStockPrice , // Register your tool
},
});
Test the tool
Ask the chatbot a question that requires your tool: “What’s the current price of Apple stock?” The AI will automatically call your getStockPrice tool with the appropriate parameters.
Tools with session context
Some tools need access to the user session for authentication or personalization:
lib/ai/tools/create-document.ts
import { tool , type UIMessageStreamWriter } from "ai" ;
import type { Session } from "next-auth" ;
import { z } from "zod" ;
type CreateDocumentProps = {
session : Session ;
dataStream : UIMessageStreamWriter < ChatMessage >;
};
export const createDocument = ({ session , dataStream } : CreateDocumentProps ) =>
tool ({
description: "Create a document for writing or content creation activities." ,
inputSchema: z . object ({
title: z . string (),
kind: z . enum ([ "text" , "code" , "spreadsheet" ]),
}),
execute : async ({ title , kind }) => {
const id = generateUUID ();
// Write metadata to data stream
dataStream . write ({
type: "data-kind" ,
data: kind ,
transient: true ,
});
// Create document in database
await createDocumentInDb ({
id ,
title ,
kind ,
userId: session . user . id ,
});
return {
id ,
title ,
kind ,
content: "A document was created and is now visible to the user." ,
};
},
});
Use the factory function pattern (returning a tool from a function) when your tool needs runtime dependencies like session data or data streams.
Streaming data to the UI
Tools can stream data to the UI using the dataStream parameter:
lib/ai/tools/request-suggestions.ts
export const requestSuggestions = ({ session , dataStream }) =>
tool ({
description: "Request writing suggestions for an existing document." ,
inputSchema: z . object ({
documentId: z . string (). describe ( "The UUID of an existing document" ),
}),
execute : async ({ documentId }) => {
const document = await getDocumentById ({ id: documentId });
const { partialOutputStream } = streamText ({
model: getArtifactModel (),
system: "You are a writing assistant..." ,
prompt: document . content ,
output: Output . array ({
element: z . object ({
originalSentence: z . string (),
suggestedSentence: z . string (),
description: z . string (),
}),
}),
});
// Stream suggestions to UI as they're generated
for await ( const partialOutput of partialOutputStream ) {
for ( const suggestion of partialOutput ) {
dataStream . write ({
type: "data-suggestion" ,
data: suggestion ,
transient: true ,
});
}
}
return {
message: "Suggestions have been added to the document" ,
};
},
});
The weather tool demonstrates geocoding before making an API request:
lib/ai/tools/get-weather.ts
async function geocodeCity (
city : string
) : Promise <{ latitude : number ; longitude : number } | null > {
try {
const response = await fetch (
`https://geocoding-api.open-meteo.com/v1/search?name= ${ encodeURIComponent ( city ) } &count=1`
);
if ( ! response . ok ) return null ;
const data = await response . json ();
if ( ! data . results || data . results . length === 0 ) return null ;
return {
latitude: data . results [ 0 ]. latitude ,
longitude: data . results [ 0 ]. longitude ,
};
} catch {
return null ;
}
}
export const getWeather = tool ({
execute : async ( input ) => {
let latitude : number ;
let longitude : number ;
if ( input . city ) {
const coords = await geocodeCity ( input . city );
if ( ! coords ) {
return {
error: `Could not find coordinates for " ${ input . city } ".` ,
};
}
latitude = coords . latitude ;
longitude = coords . longitude ;
}
// Proceed with weather API call
},
});
Clear descriptions : Write descriptions that clearly explain when the AI should use the tool. Include examples of valid inputs.
Input validation : Use Zod’s .describe() method on each schema field to provide context about expected values.
Error handling : Always handle errors gracefully and return meaningful error messages that the AI can communicate to the user.
User approval : Set needsApproval: true for tools that:
Make external API requests
Modify or delete data
Access sensitive information
Perform costly operations
Reasoning models (like Claude with extended thinking) have tools disabled by default:
app/(chat)/api/chat/route.ts
const isReasoningModel =
selectedChatModel . endsWith ( "-thinking" ) ||
( selectedChatModel . includes ( "reasoning" ) &&
! selectedChatModel . includes ( "non-reasoning" ));
const result = streamText ({
experimental_activeTools: isReasoningModel
? [] // No tools for reasoning models
: [ "getWeather" , "createDocument" , ... ],
});
This prevents the model from getting distracted during deep reasoning tasks.
Here’s a complete example of a tool that queries a database:
lib/ai/tools/search-documents.ts
import { tool } from "ai" ;
import { z } from "zod" ;
import { searchUserDocuments } from "@/lib/db/queries" ;
type SearchDocumentsProps = {
session : Session ;
};
export const searchDocuments = ({ session } : SearchDocumentsProps ) =>
tool ({
description: "Search through the user's documents by title or content. Use this when users want to find or reference their previous documents." ,
inputSchema: z . object ({
query: z . string (). describe ( "Search query to find documents" ),
limit: z . number ()
. optional ()
. describe ( "Maximum number of results to return (default: 5)" ),
}),
execute : async ({ query , limit = 5 }) => {
if ( ! session . user ?. id ) {
return { error: "User not authenticated" };
}
const results = await searchUserDocuments ({
userId: session . user . id ,
query ,
limit ,
});
if ( results . length === 0 ) {
return {
message: `No documents found matching " ${ query } ".` ,
};
}
return {
results: results . map ( doc => ({
id: doc . id ,
title: doc . title ,
kind: doc . kind ,
preview: doc . content . substring ( 0 , 150 ) + "..." ,
createdAt: doc . createdAt ,
})),
};
},
});
When returning multiple results, provide structured data that the AI can easily parse and present to the user in a readable format.