Tool Calling and UI Generation with the AI SDK
Generative UI means the model calls tools, and your React tree turns tool results into real components - cards, tables, forms - not only markdown.
Search across all documentation pages
Generative UI means the model calls tools, and your React tree turns tool results into real components - cards, tables, forms - not only markdown.
This page wires a server tool into streamText, streams typed tool parts to useChat, and maps those parts to components.
Define tools with description, inputSchema, and optional execute; stream them through a UI message response; on the client, switch on part.type (tool-${name}) and part.state to render loading, result, and error UI.
tool() (or plain tool objects) and Zod inputSchema.streamText (or an agent) and bound steps with stopWhen.message.parts in the client; map each tool part type to a component.execute and use onToolCall + addToolOutput.sendAutomaticallyWhen when client tool results are complete.ai/tools.ts:
import { tool } from "ai";
import { z } from "zod";
export const displayWeather = tool({
description: "Show the weather for a location in the chat UI",
inputSchema: z.object({
location: z.string().describe("City or place name"),
}),
execute: async ({ location }) => {
// Replace with a real weather API
return { location, weather: "Sunny", temperature: 22 };
},
});
export const tools = { displayWeather };app/api/chat/route.ts:
import {
convertToModelMessages,
isStepCount,
streamText,
type UIMessage,
} from "ai";
import { tools } from "@/ai/tools";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: "openai/gpt-4o-mini", // verify at build
instructions: "Use displayWeather when the user asks about weather.",
messages: await convertToModelMessages(messages),
tools,
stopWhen: isStepCount(5),
});
return result.toUIMessageStreamResponse();
}components/weather.tsx + client render sketch:
"use client";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useState } from "react";
function Weather(props: {
location: string;
weather: string;
temperature: number;
}) {
return (
<div>
<h3>{props.location}</h3>
<p>
{props.weather}, {props.temperature}°C
</p>
</div>
);
}
export default function Page() {
const [input, setInput] = useState("");
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({ api: "/api/chat" }),
});
return (
<div>
{messages.map((m) => (
<div key={m.id}>
{m.parts.map((part, i) => {
if (part.type === "text") return <span key={i}>{part.text}</span>;
if (part.type === "tool-displayWeather") {
if (part.state === "input-available")
return <div key={i}>Loading weather…</div>;
if (part.state === "output-available")
return <Weather key={i} {...part.output} />;
if (part.state === "output-error")
return <div key={i}>Error: {part.errorText}</div>;
}
return null;
})}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
}}
>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</form>
</div>
);
}The model should not invent HTML.
It should call a named tool with structured input; your app owns presentation.
That separation keeps theming, accessibility, and validation under engineering control.
| Pattern | How it works | Use when |
|---|---|---|
Server execute | Route runs the tool; result streams back | Secrets, DB, privileged APIs |
Client onToolCall + addToolOutput | Browser runs the tool; result sent back into chat | Device APIs, pure UI helpers |
| Interactive client tool (no auto execute) | UI shows confirm/input; addToolOutput after user action | Confirmations, form fills |
| Approval-gated server tool | Pause for approve/deny before execute | Writes, spend, irreversible actions |
Treat tool parts as a small state machine:
part.outputAvoid assuming every tool finishes in one frame; multi-step agents interleave text and tools.
Tool selection is only as good as description + schema field .describe() text.
Scope the tool: "weather for a place" beats "general helper."
One tool per UI surface often beats a mega-tool returning arbitrary JSON.
Share Zod schemas or inferred types between tool output and React props.
When part types are generated or narrowed, prefer part.type === 'tool-displayWeather' switches over stringly toolName bags.
After server tools run, stopWhen decides whether the model gets another step to narrate results.
After client tools, configure sendAutomaticallyWhen (for example lastAssistantMessageIsCompleteWithToolCalls) so the user does not click "continue" manually.
Limit which tools exist; do not let the model emit arbitrary component names.
Each tool is an allowlisted UI capability.
content only. Tool UIs never appear if you ignore parts.tool-X must match the tools object key X.onToolCall incorrectly. Some guides warn that improper await patterns can deadlock; follow current AI SDK tool-usage docs.stopWhen. Multi-step tools can loop until platform timeout.execute must stay server-side.dangerouslySetInnerHTML.toUIMessageStreamResponse vs createUIMessageStreamResponse - verify at build for your AI SDK major.| Approach | Pros | Cons |
|---|---|---|
| Generative UI via tools | Typed, allowlisted components | More React wiring |
| Markdown-only chat | Simple | Weak for structured data |
| Server-driven RSC streamables | Tight server components story | Different mental model than useChat |
| Custom WebSocket component protocol | Full control | You own compatibility |
| Client tools only | Great for browser APIs | No secret server actions |
No. The model calls tools; your code maps tool results to components you wrote.
Yes. parts is ordered and can mix text with several tool parts.
No. Some tools are invisible plumbing (search, fetch). Render components when humans benefit from structure.
Infer from the tool's execute return type or define a shared type; cast carefully until your SDK version offers stronger narrowing.
Some providers stream partial tool input. Use input-streaming states for progressive UX when available.
Prefer serializable data in tool results and render components on the client from that data. That keeps the stream protocol stable.
Confirmation tools often use a client tool without execute and addToolOutput. Tool approval (AI SDK 6+) is a first-class pause for server tool execution with approval request/response parts.
Schemas can be Zod or JSON schema depending on version/docs. Zod is the common TypeScript path.
Unit-test pure components with fixture outputs; integration-test the route with mocked model tool calls if your stack supports it.
Yes if the backend emits compatible UI message / data stream events or you map tool events to the AI SDK stream format.
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.
Reviewed by Chris St. John·Last updated Jul 16, 2026