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.
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();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(
{
render: ({ name, parameters, status, result }) => (
<CustomCatchallRenderer
name={name}
parameters={parameters}
status={status as CatchallToolStatus}
result={result}
/>
),
},
[],
);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:
// Per-tool renderer #1: get_weather → branded WeatherCard.
useRenderTool(
{
name: "get_weather",
parameters: z.object({
location: z.string(),
}),
render: ({ parameters, result, status }) => {
const loading = status !== "complete";
const parsed = parseJsonResult<WeatherResult>(result);
return (
<WeatherCard
loading={loading}
location={parameters?.location ?? parsed.city ?? ""}
temperature={parsed.temperature}
humidity={parsed.humidity}
windSpeed={parsed.wind_speed}
conditions={parsed.conditions}
/>
);
},
},
[],
); // Per-tool renderer #2: search_flights → branded FlightListCard.
useRenderTool(
{
name: "search_flights",
parameters: z.object({
origin: z.string(),
destination: z.string(),
}),
render: ({ parameters, result, status }) => {
const loading = status !== "complete";
const parsed = parseJsonResult<FlightSearchResult>(result);
return (
<FlightListCard
loading={loading}
origin={parameters?.origin ?? parsed.origin ?? ""}
destination={parameters?.destination ?? parsed.destination ?? ""}
flights={parsed.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:
// Wildcard catch-all for every remaining tool (get_stock_price,
// roll_dice, anything the agent might add later).
useDefaultRenderTool(
{
render: ({ name, parameters, status, result }) => (
<CustomCatchallRenderer
name={name}
parameters={parameters}
status={status as CatchallToolStatus}
result={result}
/>
),
},
[],
);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:
@tool
def get_weather(location: str) -> dict:
"""Get the current weather for a given location.
Useful on its own for weather questions, and a great companion to
`search_flights` - always consider checking the weather at a
destination the user is flying to, and checking flights to any
city whose weather the user has just asked about.
"""
return {
"city": location,
"temperature": 68,
"humidity": 55,
"wind_speed": 10,
"conditions": "Sunny",
}