Skip to main content
AI tools allow your chatbot to interact with external APIs, manipulate data, and create rich interactive experiences. This guide covers advanced tool building patterns from the chatbot’s implementation.

Tool fundamentals

Tools in the AI SDK are defined using the tool() function with a schema, description, and execute function.
import { tool } from "ai";
import { z } from "zod";

export const myTool = tool({
  description: "A clear description of what this tool does",
  inputSchema: z.object({
    param: z.string().describe("Description for the AI"),
  }),
  execute: async ({ param }) => {
    // Tool implementation
    return { result: "success" };
  },
});

Simple tool: Weather API

Let’s examine the weather tool, which demonstrates API integration and user approval:
lib/ai/tools/get-weather.ts
import { tool } from "ai";
import { z } from "zod";

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&language=en&format=json`
    );

    if (!response.ok) return null;
    const data = await response.json();

    if (!data.results || data.results.length === 0) return null;

    const result = data.results[0];
    return {
      latitude: result.latitude,
      longitude: result.longitude,
    };
  } catch {
    return null;
  }
}

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) => {
    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}". Please check the city name.`,
        };
      }
      latitude = coords.latitude;
      longitude = coords.longitude;
    } else if (input.latitude !== undefined && input.longitude !== undefined) {
      latitude = input.latitude;
      longitude = input.longitude;
    } else {
      return {
        error: "Please provide either a city name or both latitude and longitude coordinates.",
      };
    }

    const response = await fetch(
      `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m&hourly=temperature_2m&daily=sunrise,sunset&timezone=auto`
    );

    const weatherData = await response.json();

    if ("city" in input) {
      weatherData.cityName = input.city;
    }

    return weatherData;
  },
});
The needsApproval: true flag requires user confirmation before executing the tool. This is useful for tools that make external API calls or perform sensitive operations.

Advanced tool: Document creation

The document creation tool demonstrates streaming, session handling, and complex state management:
lib/ai/tools/create-document.ts
import { tool, type UIMessageStreamWriter } from "ai";
import type { Session } from "next-auth";
import { z } from "zod";
import {
  artifactKinds,
  documentHandlersByArtifactKind,
} from "@/lib/artifacts/server";
import type { ChatMessage } from "@/lib/types";
import { generateUUID } from "@/lib/utils";

type CreateDocumentProps = {
  session: Session;
  dataStream: UIMessageStreamWriter<ChatMessage>;
};

export const createDocument = ({ session, dataStream }: CreateDocumentProps) =>
  tool({
    description:
      "Create a document for a writing or content creation activities. This tool will call other functions that will generate the contents of the document based on the title and kind.",
    inputSchema: z.object({
      title: z.string(),
      kind: z.enum(artifactKinds),
    }),
    execute: async ({ title, kind }) => {
      const id = generateUUID();

      dataStream.write({
        type: "data-kind",
        data: kind,
        transient: true,
      });

      dataStream.write({
        type: "data-id",
        data: id,
        transient: true,
      });

      dataStream.write({
        type: "data-title",
        data: title,
        transient: true,
      });

      dataStream.write({
        type: "data-clear",
        data: null,
        transient: true,
      });

      const documentHandler = documentHandlersByArtifactKind.find(
        (documentHandlerByArtifactKind) =>
          documentHandlerByArtifactKind.kind === kind
      );

      if (!documentHandler) {
        throw new Error(`No document handler found for kind: ${kind}`);
      }

      await documentHandler.onCreateDocument({
        id,
        title,
        dataStream,
        session,
      });

      dataStream.write({ type: "data-finish", data: null, transient: true });

      return {
        id,
        title,
        kind,
        content: "A document was created and is now visible to the user.",
      };
    },
  });

Key patterns in document creation

1

Dependency injection

The tool accepts session and dataStream as parameters, allowing it to access user context and send real-time updates:
export const createDocument = ({ session, dataStream }: CreateDocumentProps) =>
  tool({
    // ...
  });
2

Streaming updates

The tool sends transient data updates to the UI as it progresses:
dataStream.write({
  type: "data-kind",
  data: kind,
  transient: true,
});
Transient data is sent to the client but not persisted in the database.
3

Handler delegation

The tool delegates to specialized handlers based on document type:
const documentHandler = documentHandlersByArtifactKind.find(
  (handler) => handler.kind === kind
);

await documentHandler.onCreateDocument({
  id,
  title,
  dataStream,
  session,
});
4

User feedback

The return value provides feedback to the AI about what happened:
return {
  id,
  title,
  kind,
  content: "A document was created and is now visible to the user.",
};

Complex tool: Request suggestions

The suggestions tool demonstrates streaming structured output and nested AI calls:
lib/ai/tools/request-suggestions.ts
import { Output, streamText, tool, type UIMessageStreamWriter } from "ai";
import type { Session } from "next-auth";
import { z } from "zod";
import { getDocumentById, saveSuggestions } from "@/lib/db/queries";
import type { Suggestion } from "@/lib/db/schema";
import type { ChatMessage } from "@/lib/types";
import { generateUUID } from "@/lib/utils";
import { getArtifactModel } from "../providers";

type RequestSuggestionsProps = {
  session: Session;
  dataStream: UIMessageStreamWriter<ChatMessage>;
};

export const requestSuggestions = ({
  session,
  dataStream,
}: RequestSuggestionsProps) =>
  tool({
    description:
      "Request writing suggestions for an existing document artifact. Only use this when the user explicitly asks to improve or get suggestions for a document they have already created. Never use for general questions.",
    inputSchema: z.object({
      documentId: z
        .string()
        .describe(
          "The UUID of an existing document artifact that was previously created with createDocument"
        ),
    }),
    execute: async ({ documentId }) => {
      const document = await getDocumentById({ id: documentId });

      if (!document || !document.content) {
        return {
          error: "Document not found",
        };
      }

      const suggestions: Omit<
        Suggestion,
        "userId" | "createdAt" | "documentCreatedAt"
      >[] = [];

      const { partialOutputStream } = streamText({
        model: getArtifactModel(),
        system:
          "You are a help writing assistant. Given a piece of writing, please offer suggestions to improve the piece of writing and describe the change. It is very important for the edits to contain full sentences instead of just words. Max 5 suggestions.",
        prompt: document.content,
        output: Output.array({
          element: z.object({
            originalSentence: z.string().describe("The original sentence"),
            suggestedSentence: z.string().describe("The suggested sentence"),
            description: z
              .string()
              .describe("The description of the suggestion"),
          }),
        }),
      });

      let processedCount = 0;
      for await (const partialOutput of partialOutputStream) {
        if (!partialOutput) continue;

        for (let i = processedCount; i < partialOutput.length; i++) {
          const element = partialOutput[i];
          if (
            !element?.originalSentence ||
            !element?.suggestedSentence ||
            !element?.description
          ) {
            continue;
          }

          const suggestion = {
            originalText: element.originalSentence,
            suggestedText: element.suggestedSentence,
            description: element.description,
            id: generateUUID(),
            documentId,
            isResolved: false,
          };

          dataStream.write({
            type: "data-suggestion",
            data: suggestion as Suggestion,
            transient: true,
          });

          suggestions.push(suggestion);
          processedCount++;
        }
      }

      if (session.user?.id) {
        await saveSuggestions({
          suggestions: suggestions.map((suggestion) => ({
            ...suggestion,
            userId: session.user.id,
            createdAt: new Date(),
            documentCreatedAt: document.createdAt,
          })),
        });
      }

      return {
        id: documentId,
        title: document.title,
        kind: document.kind,
        message: "Suggestions have been added to the document",
      };
    },
  });

Advanced patterns

The tool uses streamText within the execute function to generate structured suggestions:
const { partialOutputStream } = streamText({
  model: getArtifactModel(),
  system: "You are a help writing assistant...",
  prompt: document.content,
  output: Output.array({
    element: z.object({
      originalSentence: z.string(),
      suggestedSentence: z.string(),
      description: z.string(),
    }),
  }),
});
The tool processes suggestions as they’re generated and streams them to the UI:
let processedCount = 0;
for await (const partialOutput of partialOutputStream) {
  if (!partialOutput) continue;

  for (let i = processedCount; i < partialOutput.length; i++) {
    const element = partialOutput[i];
    // Process and stream each suggestion
    dataStream.write({
      type: "data-suggestion",
      data: suggestion as Suggestion,
      transient: true,
    });
    processedCount++;
  }
}
After streaming all suggestions, they’re saved to the database:
await saveSuggestions({
  suggestions: suggestions.map((suggestion) => ({
    ...suggestion,
    userId: session.user.id,
    createdAt: new Date(),
    documentCreatedAt: document.createdAt,
  })),
});

Registering tools

Tools are registered in the chat API route:
app/(chat)/api/chat/route.ts
const result = streamText({
  model: getLanguageModel(selectedChatModel),
  system: systemPrompt({ selectedChatModel, requestHints }),
  messages: modelMessages,
  experimental_activeTools: isReasoningModel
    ? []
    : [
        "getWeather",
        "createDocument",
        "updateDocument",
        "requestSuggestions",
      ],
  tools: {
    getWeather,
    createDocument: createDocument({ session, dataStream }),
    updateDocument: updateDocument({ session, dataStream }),
    requestSuggestions: requestSuggestions({ session, dataStream }),
  },
});
Reasoning models (like those with “-thinking” suffix) don’t support tools. Use experimental_activeTools: [] to disable tools for these models.

Tool best practices

1

Clear descriptions

Write detailed descriptions that help the AI understand when and how to use the tool:
description:
  "Request writing suggestions for an existing document artifact. Only use this when the user explicitly asks to improve or get suggestions for a document they have already created. Never use for general questions.",
2

Detailed parameter descriptions

Use Zod’s .describe() method to provide context for each parameter:
documentId: z
  .string()
  .describe(
    "The UUID of an existing document artifact that was previously created with createDocument"
  ),
3

Error handling

Return structured error objects instead of throwing:
if (!document) {
  return {
    error: "Document not found",
  };
}
4

Stream progress updates

Keep users informed with transient data streams:
dataStream.write({
  type: "data-clear",
  data: null,
  transient: true,
});
5

Return informative results

Provide context about what happened for the AI to communicate to the user:
return {
  id,
  title,
  kind,
  content: "A document was created and is now visible to the user.",
};

Custom data types

Define custom data types for streaming tool updates to the UI:
lib/types.ts
export type CustomUIDataTypes = {
  textDelta: string;
  imageDelta: string;
  sheetDelta: string;
  codeDelta: string;
  suggestion: Suggestion;
  appendMessage: string;
  id: string;
  title: string;
  kind: ArtifactKind;
  clear: null;
  finish: null;
  "chat-title": string;
};
These types define what data can be streamed from tools to the UI using dataStream.write().

Next steps

Build docs developers (and LLMs) love