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.

Live Demo: LangGraph (Python)tool-renderingOpen full demo →

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.

frontend/src/app/page.tsx — useDefaultRenderTool()
L43–47
  // 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();
Live Demo: LangGraph (Python)tool-rendering-default-catchallOpen full demo →

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:

frontend/src/app/page.tsx — custom wildcard renderer
L39–54
  // `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}
        />
      ),
    },
    [],
  );
Live Demo: LangGraph (Python)tool-rendering-custom-catchallOpen full demo →

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:

frontend/src/app/page.tsx — weather renderer
L67–90
  // 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}
          />
        );
      },
    },
    [],
  );
frontend/src/app/page.tsx — flight renderer
L92–114
  // 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 ?? []}
          />
        );
      },
    },
    [],
  );
Info

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:

frontend/src/app/page.tsx — per-tool + catch-all
L116–130
  // 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:

backend/agent.py — weather tool
L53–68
@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",
    }

Choose your AI backend

See Integrations for all available frameworks (generative-ui/tool-rendering).