Tool Rendering
Render your agent's tool calls with custom UI components.
What is this?#
Tools are how an LLM invokes predefined, typically-deterministic functions. Tool rendering lets you decide how each of those tool calls appears in the chat. Instead of showing raw JSON, you register a React component that draws a branded card for the call (arguments, live status, and the eventual result). This is the Generative UI variant CopilotKit calls tool rendering.
Free course: See this pattern built end-to-end in Build Interactive Agents with Generative UI — a free DeepLearning.AI short course taught by CopilotKit's CEO covering the full Generative UI spectrum (Controlled, Declarative, and Open-Ended).
Free course: See this pattern built end-to-end in Build Interactive Agents with Generative UI — a free DeepLearning.AI short course taught by CopilotKit's CEO covering the full Generative UI spectrum (Controlled, Declarative, and Open-Ended).
When should I use this?#
Render tool calls when you want to:
- Show users exactly what tools the agent is invoking and with what arguments
- Display live progress indicators while a tool executes
- Render rich, polished results once a tool completes
- Give tool-heavy agents a transparent, on-brand chat experience
Default tool rendering (zero-config)#
The simplest entry point: call useDefaultRenderTool() with no arguments.
CopilotKit registers its built-in DefaultToolCallRenderer as the *
wildcard: every tool call renders as a tidy status card (tool name, live
Running → Done pill, collapsible arguments/result) without you writing
any UI.
Without this hook the runtime has no * renderer and tool calls are
invisible; the user only sees the assistant's final text summary.
// Opt in to CopilotKit's built-in default tool-call card. Called with
// no config so the package-provided `DefaultToolCallRenderer` is used
// as the wildcard renderer — this is the "out-of-the-box" UI the cell
// is meant to showcase.
useDefaultRenderTool();Here's what the built-in status card looks like for each tool call:
Custom catch-all#
Once you want on-brand chrome, pass a render function to
useDefaultRenderTool. It's a convenience wrapper around
useRenderTool({ name: "*", ... }): one wildcard renderer handles every
tool call, named or not:
// `useDefaultRenderTool` is a convenience wrapper around
// `useRenderTool({ name: "*", ... })` — a single wildcard renderer
// that handles every tool call not claimed by a named renderer.
useDefaultRenderTool(
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
render: ({ name, parameters, status, result }: any) => (
<CustomCatchallRenderer
name={name}
parameters={parameters}
status={status as CatchallToolStatus}
result={result}
/>
),
},
[],
);Here's the branded catch-all in action, where every tool call gets the same on-brand card:
Per-tool renderers#
The most expressive path is one renderer per tool name. The primary
tool-rendering cell wires two: get_weather draws a branded
WeatherCard, search_flights draws a FlightListCard. Each renderer
receives the tool's parsed arguments, a live status, and (once the agent
returns) the result:
import {
CopilotKitProvider,
CopilotChat,
useRenderTool,
useDefaultRenderTool,
} from "@copilotkit/react-core/v2";
import { z } from "zod";
export default function ToolRendering() {
return (
<CopilotKitProvider runtimeUrl="/api/copilotkit" useSingleEndpoint>
<Demo />
</CopilotKitProvider>
);
}
function Demo() {
useRenderTool({
name: "get_weather",
parameters: z.object({ location: z.string() }),
render: ({ parameters, result, status }) => {
return (
<WeatherCard
loading={status !== "complete"}
parameters={parameters}
result={result}
/>
);
},
});The flight renderer follows the same pattern with a different component and schema:
import { useComponent } from "@copilotkit/react-core/v2";
declare const FlightListCard: React.ComponentType<{
loading: boolean;
origin: string;
destination: string;
flights: unknown[];
}>;
type FlightToolProps = {
status: string;
args?: { origin?: string; destination?: string };
result?: { origin?: string; destination?: string; flights?: unknown[] };
};
export function FlightToolRenderer() {
// Per-tool renderer: search_flights → branded FlightListCard.
useComponent({
name: "search_flights",
render: (props: FlightToolProps) => {
const { status, args, result } = props;
const loading = status !== "complete";
return (
<FlightListCard
loading={loading}
origin={args?.origin ?? result?.origin ?? ""}
destination={args?.destination ?? result?.destination ?? ""}
flights={result?.flights ?? []}
/>
);
},
});The name you pass to useRenderTool must match the tool name the agent
exposes; that's how the runtime routes the call to your component.
Per-tool renderers compose with a catch-all: named renderers claim the
"interesting" tools and a wildcard handles everything else. In the primary
cell, the same CustomCatchallRenderer from above catches get_stock_price
and roll_dice:
useDefaultRenderTool({
render: GenericToolCard,
});The backend tool definition#
The frontend renderer only sees what the agent sends down. Here's the
matching Python definition for get_weather, a standard LangChain tool,
no CopilotKit-specific plumbing required:
import { z } from "zod";
import { toolDefinition } from "@tanstack/ai";
export const weatherTool = toolDefinition({
name: "weather",
description: "Get current weather for a city",
inputSchema: z.object({
city: z.string(),
}),
}).server(async ({ city }) => ({
city,
tempF: 72,
condition: "Partly cloudy",
humidity: 0.45,
}));