Skip to main content
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.

Tool basics

Tools are defined using the tool function from the Vercel AI SDK and use Zod schemas for input validation.

Anatomy of a tool

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}&current=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.

Creating a simple tool

1

Create the tool file

Create a new file in lib/ai/tools/ for your tool:
touch lib/ai/tools/get-stock-price.ts
2

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.",
      };
    }
  },
});
3

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
  },
});
4

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.

Advanced tool patterns

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",
      };
    },
  });

Tools with geocoding

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
  },
});

Tool best practices

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 and tools

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.

Example: Database query tool

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.

Build docs developers (and LLMs) love