CopilotKit

Tool Rendering

Render your agent's tool calls with custom UI components.


This example demonstrates the implementation section applied in the

CopilotKit feature viewer

.

What is this?#

Tools are a way for the LLM to call predefined, typically, deterministic functions. CopilotKit allows you to render these tools in the UI as a custom component, which we call Generative UI.

When should I use this?#

Rendering tools in the UI is useful when you want to provide the user with feedback about what your agent is doing, specifically when your agent is calling tools. CopilotKit allows you to fully customize how these tools are rendered in the chat.

Implementation#

Run and connect your agent#

You'll need to run your agent and connect it to CopilotKit before proceeding. If you haven't done so already, you can follow the instructions in the Getting Started guide.

If you don't already have an agent, you can use the coagent starter as a starting point as this guide uses it as a starting point.

Give your agent a tool to call#

The agent in the live demo above exposes a mock get_weather tool. This is the definition straight from the Python backend:

tool_rendering_agent.py
from random import choice, randintfrom langchain.agents import create_agentfrom langchain.tools import toolfrom langchain_openai import ChatOpenAIfrom copilotkit import CopilotKitMiddleware# Multi-tool chaining prompt.## The goal of this demo is to surface MULTIPLE tool-call cards per turn so# the rendering patterns (per-tool + catch-all) get exercised visibly. The# prompt nudges the model toward an explore-then-enrich pattern (e.g.# `get_weather("Tokyo")` -> `search_flights(..., "Tokyo")`) without forcing# a rigid recipe: we describe the *habit*, not a chain.SYSTEM_PROMPT = (    "You are a helpful travel & lifestyle concierge. You have mock tools "    "for weather, flights, stock prices, and dice rolls - they all return "    "fake data, so call them liberally.\n\n"    "Your habit is to CHAIN tools when one answer naturally invites another. "    "For a single user question, call at least TWO tools in succession when "    "the topic allows before composing your final reply. Examples of "    "helpful chains you should default to:\n"    "  - 'What's the weather in Tokyo?' -> call get_weather('Tokyo'), then "    "call search_flights(origin='SFO', destination='Tokyo') so the user "    "also sees how to get there.\n"    "  - 'How is AAPL doing?' -> call get_stock_price('AAPL'), then call "    "get_stock_price on a related ticker (e.g. 'MSFT' or 'GOOGL') for "    "comparison.\n"    "  - 'Roll a d20' -> call roll_dice(20), then call roll_dice again with "    "a different number of sides so the user sees a contrast.\n"    "  - 'Find flights from SFO to JFK' -> call search_flights, then call "    "get_weather on the destination city.\n\n"    "Only skip chaining when the user has clearly asked for a single, "    "atomic answer and more tool calls would feel intrusive. Never "    "fabricate data that a tool could provide.")@tooldef 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",    }

Render the tool call in your frontend#

At this point, your agent will be able to call the get_weather tool. Now we just need to add a useRenderTool hook to render the tool call in the UI.

Important

In order to render a tool call in the UI, the name must match the name of the tool.

The example below uses a branded <WeatherCard /> component to render the call — same pattern as the minimal example above, just wired to a richer UI:

page.tsx
import React from "react";import { CopilotKit } from "@copilotkit/react-core";import {  CopilotChat,  useRenderTool,  useConfigureSuggestions,} from "@copilotkit/react-core/v2";import { z } from "zod";function parseJsonResult<T>(result: unknown): T {  if (!result) return {} as T;  try {    return (typeof result === "string" ? JSON.parse(result) : result) as T;  } catch {    return {} as T;  }}export default function ToolRenderingDemo() {  return (    <CopilotKit runtimeUrl="/api/copilotkit" agent="tool-rendering">      <Chat />    </CopilotKit>  );}function Chat() {  useRenderTool({    name: "get_weather",    parameters: z.object({      location: z.string(),    }),    render: ({ args, result, status }: any) => {      if (status !== "complete") {        return (          <div className="bg-[#667eea] text-white p-4 rounded-lg max-w-md">            <span className="animate-spin">Retrieving weather...</span>          </div>        );      }      const parsed = parseJsonResult<any>(result);      const weatherResult: WeatherToolResult = {        temperature: parsed?.temperature || 0,        conditions: parsed?.conditions || "clear",        humidity: parsed?.humidity || 0,        windSpeed: parsed?.wind_speed || 0,        feelsLike: parsed?.feels_like || parsed?.temperature || 0,      };      const themeColor = getThemeColor(weatherResult.conditions);      return (        <WeatherCard          location={args.location}          themeColor={themeColor}          result={weatherResult}        />      );    },  });

Add a catch-all renderer (optional)#

For every other tool the agent calls, a single useDefaultRenderTool hook can provide a uniform fallback UI — this is exactly how the demo above handles get_stock_price, roll_dice, and any tool the agent might add later without a dedicated renderer:

render-flight-tool.snippet.tsx
  // Wildcard catch-all for every remaining tool — anything the agent might  // call that doesn't have a dedicated useRenderTool registration.  useDefaultRenderTool(    {      render: ({ name, parameters, status, result }) => (        <CustomCatchallRenderer          name={name}          parameters={parameters}          status={status as CatchallToolStatus}          result={result}        />      ),    },    [],  );

Give it a try!#

Try asking the agent to get the weather for a location. You should see the custom UI component that we added render the tool call and display the arguments that were passed to the tool.

Default Tool Rendering#

useDefaultRenderTool provides a catch-all renderer for any tool that doesn't have a specific useRenderToolCall defined. This is useful for:

  • Displaying all tool calls during development
  • Rendering MCP (Model Context Protocol) tools
  • Providing a generic fallback UI for unexpected tools
app/page.tsx
import { useDefaultRenderTool } from "@copilotkit/react-core/v2"; 
// ...

const YourMainContent = () => {
  // ...
  useDefaultRenderTool({
    render: ({ name, args, status, result }) => {
      return (
        <div style={{ color: "black" }}>
          <span>
            {status === "complete" ? "✓" : "⏳"}
            {name}
          </span>
          {status === "complete" && result && (
            <pre>{JSON.stringify(result, null, 2)}</pre>
          )}
        </div>
      );
    },
  });
  // ...
};

Unlike useRenderToolCall, which targets a specific tool by name, useDefaultRenderTool catches all tools that don't have a dedicated renderer.

In v2, use useDefaultRenderTool for wildcard fallback rendering, and useRenderTool for named or wildcard renderer registration.