Tool Calling

Add API calls, database queries, and custom actions to your AI chat with assistant-ui's tool calling. Real-time visualization, type safety, and streaming.

Tools enable LLMs to take actions and interact with external systems. assistant-ui provides a comprehensive toolkit for creating, managing, and visualizing tool interactions in real-time.

Overview

Tools in assistant-ui are functions that the LLM can call to perform specific tasks. They bridge the gap between the LLM's reasoning capabilities and real-world actions like:

  • Fetching data from APIs
  • Performing calculations
  • Interacting with databases
  • Controlling UI elements
  • Executing workflows

When tools are executed, you can display custom generative UI components that provide rich, interactive visualizations of the tool's execution and results. Learn more in the Generative UI guide.

If you haven't provided a custom UI for a tool, assistant-ui offers a ToolFallback component that you can add to your codebase to render a default UI for tool executions. You can customize this by creating your own Tool UI component for the tool's name.

Tools() API

The Tools() API is the recommended starting point for registering tools in assistant-ui. It provides centralized tool registration that prevents duplicate registrations and works seamlessly with all runtimes. For tools whose availability depends on a specific part of your UI being mounted, see the component-based APIs below; both styles are supported and can be mixed in the same app.

Quick Start

Create a toolkit object containing all your tools, then register it using useAui():

import { useAui, Tools, type Toolkit } from "@assistant-ui/react";
import { z } from "zod";

// Define your toolkit
const myToolkit: Toolkit = {
  getWeather: {
    description: "Get current weather for a location",
    parameters: z.object({
      location: z.string().describe("City name or zip code"),
      unit: z.enum(["celsius", "fahrenheit"]).default("celsius"),
    }),
    execute: async ({ location, unit }) => {
      const weather = await fetchWeatherAPI(location, unit);
      return weather;
    },
    render: ({ args, result }) => {
      if (!result) return <div>Fetching weather for {args.location}...</div>;
      return (
        <div className="weather-card">
          <h3>{args.location}</h3>
          <p>{result.temperature}° {args.unit}</p>
          <p>{result.conditions}</p>
        </div>
      );
    },
  },
  // Add more tools here
};

// Register tools in your runtime provider
function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
  const runtime = useChatRuntime();

  // Register all tools
  const aui = useAui({
    tools: Tools({ toolkit: myToolkit }),
  });

  return (
    <AssistantRuntimeProvider aui={aui} runtime={runtime}>
      {children}
    </AssistantRuntimeProvider>
  );
}

Benefits

  • No Duplicate Registrations: Tools are registered once, preventing the "tool already exists" error
  • Centralized Definition: All your tools in one place, easier to manage and test
  • Type-Safe: Full TypeScript support with proper type inference
  • Flexible: Works with all runtimes (AI SDK, LangGraph, custom, etc.)
  • Composable: Easily split toolkits across files and merge them

Tool Definition

Each tool in the toolkit is a ToolDefinition object with these properties:

type ToolDefinition =
  | {
      // Frontend tool: executes in the browser
      type?: "frontend";
      description?: string;
      parameters: StandardSchemaV1 | JSONSchema7; // e.g. a Zod schema
      execute: (args, context) => Promise<any>;
      toModelOutput?: (opts) => ToolModelContentPart[]; // see "Multi-modal tool results"
      render?: (props) => React.ReactNode;
    }
  | {
      // Human tool: pauses for user input (render is required)
      type: "human";
      description?: string;
      parameters: StandardSchemaV1 | JSONSchema7;
      render: (props) => React.ReactNode;
    }
  | {
      // Backend tool: execution happens server-side (no execute/parameters needed)
      type: "backend";
      render?: (props) => React.ReactNode;
    };

Multi-modal Tool Results

By default, the value returned from execute is sent to the model as a single JSON blob. That is fine for most tools, but it does not work for tools whose useful output is a file or image: a "read PDF" tool, an OCR tool, a chart-rendering tool, etc.

toModelOutput is an optional callback that maps the developer-facing execute result into the multi-modal content the model actually sees. Your render function still receives the rich, typed result; the model receives the projection.

import { tool } from "@assistant-ui/react";
import { convertUint8ArrayToBase64 } from "@ai-sdk/provider-utils";
import { z } from "zod";

const readPdfTool = tool({
  description: "Fetch a PDF from a URL and return it",
  parameters: z.object({ url: z.string().url() }),
  execute: async ({ url }) => {
    const res = await fetch(url);
    const buf = new Uint8Array(await res.arrayBuffer());
    const base64 = convertUint8ArrayToBase64(buf);
    return { mediaType: "application/pdf", base64, byteLength: buf.byteLength };
  },
  toModelOutput: ({ output }) => [
    { type: "text", text: "PDF contents:" },
    {
      type: "file",
      data: output.base64,
      mediaType: output.mediaType,
    },
  ],
});

ToolModelContentPart is a union of { type: "text"; text } and { type: "file"; data; mediaType; filename? }. Use mediaType (e.g. image/png, application/pdf) to tell the model how to interpret the bytes.

When using the AI SDK runtime, frontend tool results round-trip through the AI SDK chat protocol back to your route handler on the next turn. For toModelOutput to fire on those round-tripped results, your route handler must also pass the tool registry to convertToModelMessages. This is the same pattern AI SDK documents for any multi-modal tool response:

import { frontendTools } from "@assistant-ui/react-ai-sdk";
import { convertToModelMessages, streamText } from "ai";

const aiSDKTools = { ...frontendTools(tools ?? {}) };

const result = streamText({
  model,
  // Pass tools to both calls. convertToModelMessages reads `toModelOutput`
  // from `tools[toolName]` to project prior tool results.
  messages: await convertToModelMessages(messages, { tools: aiSDKTools }),
  tools: aiSDKTools,
});

If you skip the { tools: aiSDKTools } argument, prior tool results will be sent to the model as a plain JSON blob (the AI SDK default), and your toModelOutput will be silently ignored. Tools that do not declare toModelOutput are unaffected either way.

Reserved property name. When toModelOutput is set, the runtime wraps the AI SDK chat output as { __aui_modelContent: ToolModelContentPart[], value: <your result> } before persisting. Do not return objects whose top-level key is literally __aui_modelContent from any tool's execute, or it will be misread as the envelope. The prefix is namespaced for this reason; any other property name is fine.

Read/write compatibility for persisted threads. The __aui_modelContent envelope is recognized by @assistant-ui/react-ai-sdk from this version onward. If you persist UI messages (thread history adapter, cloud, etc.) and read them from multiple environments, upgrade every reader before any writer starts producing toModelOutput. Older readers will treat the entire envelope as the result, which breaks tool render functions for those messages.

Organizing Large Toolkits

For larger applications, split tools across multiple files:

// lib/tools/weather.tsx
export const weatherTools: Toolkit = {
  getWeather: { /* ... */ },
  getWeatherForecast: { /* ... */ },
};

// lib/tools/database.tsx
export const databaseTools: Toolkit = {
  queryData: { /* ... */ },
  insertData: { /* ... */ },
};

// lib/toolkit.tsx
import { weatherTools } from "./tools/weather";
import { databaseTools } from "./tools/database";

export const appToolkit: Toolkit = {
  ...weatherTools,
  ...databaseTools,
};

// App.tsx
import { appToolkit } from "./lib/toolkit";

function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
  const runtime = useChatRuntime();

  const aui = useAui({
    tools: Tools({ toolkit: appToolkit }),
  });

  return (
    <AssistantRuntimeProvider aui={aui} runtime={runtime}>
      {children}
    </AssistantRuntimeProvider>
  );
}

UI-Only Tools

For tools where execution happens elsewhere (e.g., backend MCP tools), omit the execute function:

const uiOnlyToolkit: Toolkit = {
  webSearch: {
    description: "Search the web",
    parameters: z.object({
      query: z.string(),
    }),
    // No execute - handled by backend
    render: ({ args, result }) => {
      return (
        <div>
          <h3>Search: {args.query}</h3>
          {result?.results.map((item) => (
            <div key={item.id}>
              <a href={item.url}>{item.title}</a>
            </div>
          ))}
        </div>
      );
    },
  },
};

Tool Execution Context

Tools receive additional context during execution:

execute: async (args, context) => {
  // context.abortSignal - AbortSignal for cancellation
  // context.toolCallId - Unique identifier for this invocation
  // context.human - Function to request human input

  // Example: Respect cancellation
  const response = await fetch(url, { signal: context.abortSignal });

  // Example: Request user confirmation
  const userResponse = await context.human({
    message: "Are you sure?",
  });
};

Cancellation

context.abortSignal is an AbortSignal that fires when the user stops the run. Pass it to any async I/O so the work stops immediately:

execute: async ({ query }, { abortSignal }) => {
  const res = await fetch(`/api/search?q=${query}`, { signal: abortSignal });
  return res.json();
},

When using LangGraph with unstable_createLangGraphStream, the default onDisconnect value is already "cancel", which tells the LangGraph server to cancel the run on abort:

import { unstable_createLangGraphStream } from "@assistant-ui/react-langgraph";

const stream = unstable_createLangGraphStream({
  client,
  assistantId,
  // onDisconnect defaults to "cancel"; the server cancels the run when the
  // client disconnects or the user stops the message.
});

See the LangGraph quickstart for full setup.

Human-in-the-Loop

Tools can pause execution to request user input or approval:

const confirmationToolkit: Toolkit = {
  sendEmail: {
    description: "Send an email with confirmation",
    parameters: z.object({
      to: z.string(),
      subject: z.string(),
      body: z.string(),
    }),
    execute: async ({ to, subject, body }, { human }) => {
      // Request user confirmation before sending
      const confirmed = await human({
        type: "confirmation",
        action: "send-email",
        details: { to, subject },
      });

      if (!confirmed) {
        return { status: "cancelled" };
      }

      await sendEmail({ to, subject, body });
      return { status: "sent" };
    },
    render: ({ args, result, interrupt, resume }) => {
      // Show confirmation dialog when waiting for user input
      if (interrupt) {
        return (
          <div>
            <h3>Confirm Email</h3>
            <p>Send to: {interrupt.payload.details.to}</p>
            <p>Subject: {interrupt.payload.details.subject}</p>
            <button onClick={() => resume(true)}>Confirm</button>
            <button onClick={() => resume(false)}>Cancel</button>
          </div>
        );
      }

      // Show result
      if (result) {
        return <div>Status: {result.status}</div>;
      }

      return <div>Preparing email...</div>;
    },
  },
};

Streaming Tool Args

While a tool is running, its arguments arrive as partial JSON. Use useToolArgsStatus inside a tool UI render function to react to each top-level field as it streams in. The hook is exported from @assistant-ui/react.

import { useToolArgsStatus } from "@assistant-ui/react";

const SearchToolUI = makeAssistantToolUI<{ query: string; limit: number }, unknown>({
  toolName: "search",
  render: ({ args }) => {
    const { propStatus } = useToolArgsStatus<{ query: string; limit: number }>();

    return (
      <div>
        <span className={propStatus.query === "streaming" ? "animate-pulse" : ""}>
          {args.query ?? "..."}
        </span>
        {propStatus.limit === "complete" && <span> (limit: {args.limit})</span>}
      </div>
    );
  },
});

propStatus maps each top-level key in the args object to "streaming" while it is still being parsed and to "complete" once that field is fully present.

Component-Based APIs

makeAssistantTool, useAssistantTool, and makeAssistantToolUI are component-and-hook-based APIs that coexist with the Tools() toolkit pattern. They are fully supported and the natural fit for the intelligent components pattern, where each part of your UI registers the tools it owns when it is mounted, for example a product-specific tool that should only be exposed while that product's screen is open.

Be careful not to register the same tool from both APIs at once: each API registers under toolName, and duplicate registrations will be rejected.

Tool execution can be registered dynamically (when a component mounts), but tool UI should generally be pre-registered. A render function that is only registered while a specific component is mounted will not render when chat history is replayed or during server-side rendering. Either declare the tool's render in a Tools() toolkit, or mount makeAssistantToolUI near the root of your tree.

Using makeAssistantTool

Register tools with the assistant context. Returns a React component that registers the tool when rendered:

import { makeAssistantTool, tool } from "@assistant-ui/react";
import { z } from "zod";

const weatherTool = tool({
  description: "Get current weather for a location",
  parameters: z.object({
    location: z.string(),
  }),
  execute: async ({ location }) => {
    const weather = await fetchWeatherAPI(location);
    return weather;
  },
});

const WeatherTool = makeAssistantTool({
  ...weatherTool,
  toolName: "getWeather",
});

// Place inside AssistantRuntimeProvider
function App() {
  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <WeatherTool />
      <Thread />
    </AssistantRuntimeProvider>
  );
}

Tradeoff: component-based registration is tied to React lifecycle, so the tool is registered when the component mounts and unregistered when it unmounts. Take care not to remount it accidentally if you also register the same tool elsewhere.

Using the useAssistantTool Hook

Register tools dynamically using React hooks:

import { useAssistantTool } from "@assistant-ui/react";
import { z } from "zod";

function DynamicTools() {
  useAssistantTool({
    toolName: "searchData",
    description: "Search through the data",
    parameters: z.object({
      query: z.string(),
    }),
    execute: async ({ query }) => {
      return await searchDatabase(query);
    },
  });

  return null;
}

Tradeoff: like makeAssistantTool, the registration follows the component lifecycle. Useful for dynamic tools that depend on component state or props.

Using makeAssistantToolUI

Create UI-only components for tools defined elsewhere:

import { makeAssistantToolUI } from "@assistant-ui/react";

const SearchResultsUI = makeAssistantToolUI<
  { query: string },
  { results: Array<any> }
>({
  toolName: "webSearch",
  render: ({ args, result }) => {
    return (
      <div>
        <h3>Search: {args.query}</h3>
        {result.results.map((item) => (
          <div key={item.id}>{item.title}</div>
        ))}
      </div>
    );
  },
});

function App() {
  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <SearchResultsUI />
      <Thread />
    </AssistantRuntimeProvider>
  );
}

Tradeoff: like the other component-based APIs, the UI is registered while the component is mounted. Useful when the tool UI needs access to surrounding component state or context.

Tool Paradigms

Frontend Tools

Tools that execute in the browser:

const frontendToolkit: Toolkit = {
  screenshot: {
    description: "Capture a screenshot of the current page",
    parameters: z.object({
      selector: z.string().optional(),
    }),
    execute: async ({ selector }) => {
      const element = selector ? document.querySelector(selector) : document.body;
      const screenshot = await captureElement(element);
      return { dataUrl: screenshot };
    },
  },
};

Backend Tools

Tools executed server-side live in your API route. A minimal example with the AI SDK:

@/app/api/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { streamText, convertToModelMessages, tool, zodSchema } from "ai";
import { z } from "zod";

export async function POST(req: Request) {
  const { messages } = await req.json();
  const result = streamText({
    model: openai("gpt-5.4-nano"),
    messages: await convertToModelMessages(messages),
    tools: {
      queryDatabase: tool({
        description: "Query the application database",
        inputSchema: zodSchema(z.object({ query: z.string(), table: z.string() })),
        execute: async ({ query, table }) => db.query(query, { table }),
      }),
    },
  });
  return result.toUIMessageStreamResponse();
}

For the full AI SDK v6 backend setup including multi-step tool calls, frontend tools, history persistence with withFormat, and more, see the AI SDK v6 guide.

Client-Defined Tools with frontendTools

The Vercel AI SDK adapter implements automatic serialization of client-defined tools. Tools registered via the Tools() API are automatically included in API requests:

// Frontend: Define tools with Tools() API
const clientToolkit: Toolkit = {
  calculate: {
    description: "Perform calculations",
    parameters: z.object({
      expression: z.string(),
    }),
    execute: async ({ expression }) => {
      return eval(expression); // Use proper parser in production
    },
  },
};

function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
  const runtime = useChatRuntime();

  const aui = useAui({
    tools: Tools({ toolkit: clientToolkit }),
  });

  return (
    <AssistantRuntimeProvider aui={aui} runtime={runtime}>
      {children}
    </AssistantRuntimeProvider>
  );
}

// Backend: Use frontendTools to receive client tools
import { frontendTools } from "@assistant-ui/react-ai-sdk";

export async function POST(req: Request) {
  const { messages, tools } = await req.json();

  const result = streamText({
    model: openai("gpt-5.4-nano"),
    messages: await convertToModelMessages(messages),
    tools: {
      ...frontendTools(tools), // Client-defined tools
      // Additional server-side tools
      queryDatabase: {
        description: "Query the database",
        inputSchema: zodSchema(z.object({ query: z.string() })),
        execute: async ({ query }) => {
          return await db.query(query);
        },
      },
    },
  });

  return result.toUIMessageStreamResponse();
}

Per-tool Provider Options

Every tool definition accepts an optional providerOptions field. assistant-ui does not interpret the value; it serializes it under the tool entry verbatim, the AI SDK route handler forwards it via frontendTools(...), and the provider SDK reads the keys it cares about. This is how you opt into provider-specific tool behaviors (such as Anthropic's defer_loading for progressive tool disclosure on large catalogs) without adding any provider-aware code to assistant-ui itself.

// Frontend: declare provider-specific metadata next to the tool.
useAssistantTool({
  toolName: "searchDocs",
  description: "Search the documentation index.",
  parameters: z.object({ query: z.string() }),
  providerOptions: {
    anthropic: { deferLoading: true },
  },
  execute: async ({ query }) => searchIndex(query),
});

The same field is available on toolkit entries used with Tools({ toolkit }) and on makeAssistantTool. The outer key is the provider name (anthropic, openai, ...); the inner object is whatever the provider's AI SDK package expects under tool.providerOptions[<provider>]. Refer to the provider's package docs for the exact shape and for any companion entries you may need in your route handler (for example, Anthropic exposes Tool Search Tool factories on the anthropic.tools namespace; see the @ai-sdk/anthropic documentation for the current API).

toToolsJSONSchema emits tool entries in alphabetical order by name. Two renders that register the same tool set in different orders produce byte-identical request bodies, which keeps provider prompt caches stable across renders.

MCP (Model Context Protocol) Tools

Integration with MCP servers using AI SDK's experimental MCP support:

import { experimental_createMCPClient, streamText } from "ai";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

export async function POST(req: Request) {
  const client = await experimental_createMCPClient({
    transport: new StdioClientTransport({
      command: "npx",
      args: ["@modelcontextprotocol/server-github"],
    }),
  });

  try {
    const tools = await client.tools();

    const result = streamText({
      model: openai("gpt-5.4-nano"),
      tools,
      messages: await convertToModelMessages(messages),
    });

    return result.toUIMessageStreamResponse();
  } finally {
    await client.close();
  }
}

LangGraph subgraph events

When a LangGraph graph contains sub-agents (nested subgraphs), events from those subgraphs arrive with a metadata.namespace field identifying the originating subgraph. Pass event handlers to useLangGraphRuntime (or useLangGraphMessages) to react to them:

const runtime = useLangGraphRuntime({
  stream,
  eventHandlers: {
    onSubgraphValues: (namespace, values) => {
      console.log("subgraph", namespace, "state:", values);
    },
    onSubgraphUpdates: (namespace, updates) => {
      console.log("subgraph", namespace, "updates:", updates);
    },
    onSubgraphError: (namespace, error) => {
      console.error("subgraph", namespace, "error:", error);
    },
  },
});

namespace is a pipe-separated string like "parent|child_agent". Messages emitted by a subgraph include metadata.namespace so you can attribute tool results to the correct sub-agent.

useLangChainState

When using @assistant-ui/react-langchain (useStreamRuntime), the useLangChainState hook lets you read any key from the current LangChain/LangGraph state on the client without a separate API call:

import { useLangChainState } from "@assistant-ui/react-langchain";

function TodoSidebar() {
  const todos = useLangChainState<string[]>("todos", []);
  return <ul>{todos.map((t) => <li key={t}>{t}</li>)}</ul>;
}

The second argument is an optional default value. The hook re-renders whenever the state key changes during a stream.

Best Practices

  1. Pick one registration style per tool: avoid registering the same tool through both the Tools() toolkit and a component-based API; both routes will register, and duplicates are rejected
  2. Centralize Definitions: Keep all tools in a toolkit file for easy management
  3. Clear Descriptions: Write descriptive tool descriptions that help the LLM understand when to use each tool
  4. Parameter Validation: Use Zod schemas to ensure type safety
  5. Error Handling: Handle errors gracefully with user-friendly messages
  6. Loading States: Provide visual feedback during tool execution
  7. Security: Validate permissions and sanitize inputs
  8. Performance: Use abort signals for cancellable operations
  9. Testing: Test tools in isolation and with the full assistant flow

Switching from Component-Based to Toolkit

If you prefer the toolkit shape, switching is mechanical:

  1. Create a toolkit object with all your tools
  2. Move tool definitions from makeAssistantTool/useAssistantTool calls into the toolkit
  3. Register once using useAui({ tools: Tools({ toolkit }) }) in your runtime provider
  4. Remove component registrations (<WeatherTool />, etc.)
  5. Test to ensure all tools work as expected

Example migration:

// Component-based API
const WeatherTool = makeAssistantTool({
  toolName: "getWeather",
  description: "Get weather",
  parameters: z.object({ location: z.string() }),
  execute: async ({ location }) => { /* ... */ },
});

function App() {
  return (
    <AssistantRuntimeProvider runtime={runtime}>
      <WeatherTool />
      <Thread />
    </AssistantRuntimeProvider>
  );
}

// Toolkit API
const toolkit: Toolkit = {
  getWeather: {
    description: "Get weather",
    parameters: z.object({ location: z.string() }),
    execute: async ({ location }) => { /* ... */ },
  },
};

function MyRuntimeProvider({ children }: { children: React.ReactNode }) {
  const runtime = useChatRuntime();

  const aui = useAui({
    tools: Tools({ toolkit }),
  });

  return (
    <AssistantRuntimeProvider aui={aui} runtime={runtime}>
      {children}
    </AssistantRuntimeProvider>
  );
}