Components as Tools
Let your agent render rich React components directly in the chat by calling them as tools.
"use client";import React from "react";import { CopilotChat, CopilotKit, useComponent,} from "@copilotkit/react-core/v2";import { BarChart, barChartPropsSchema } from "./bar-chart";import { PieChart, pieChartPropsSchema } from "./pie-chart";import { useSuggestions } from "./suggestions";function Chat() { useComponent({ name: "render_bar_chart", description: "Display a bar chart with labeled numeric values.", parameters: barChartPropsSchema, render: BarChart, }); useComponent({ name: "render_pie_chart", description: "Display a pie chart with labeled numeric values.", parameters: pieChartPropsSchema, render: PieChart, }); useSuggestions(); return ( <div className="flex justify-center items-center h-screen w-full"> <div className="h-full w-full max-w-4xl"> <CopilotChat agentId="gen-ui-tool-based" className="h-full rounded-2xl" /> </div> </div> );}export default function ControlledGenUiDemo() { return ( <CopilotKit runtimeUrl="/api/copilotkit" agent="gen-ui-tool-based"> <Chat /> </CopilotKit> );}What is this?#
Tool-based Generative UI is the simplest form of Generative UI: you register
a React component with useComponent, and CopilotKit exposes it to the
agent as a tool. When the agent calls the tool, CopilotKit renders your
component inline in the chat, passing the tool's arguments straight through
as typed props.
Unlike tool rendering, which wraps a real backend tool in a custom UI, tool-based GenUI is the component. There is no handler, no user interaction, no server-side execution. The agent decides when to show it, populates the data, and CopilotKit paints it.
When should I use this?#
Use useComponent when you want to:
- Display rich UI (cards, charts, tables, dashboards) inline in the chat
- Show structured data the agent has derived from its reasoning
- Render previews, status indicators, or visual summaries
- Let the agent present information beyond plain text
For components that need user interaction, see Human-in-the-loop. For operational transparency around a real backend tool, see Tool rendering.
How it works in code#
Declare the component as an external tool
Agno's AG-UI interface does not forward the request's tool definitions to
the model. The model only sees tools declared on the Agent, so a
component registered with useComponent needs a matching declaration
whose body stays empty. Mark it external_execution=True: Agno pauses the
run, the browser renders the component, and the run resumes with the
result.
The name and the arguments have to match the useComponent registration
exactly. The docstring is what the model reads, so describe when to call
it there.
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools import tool
@tool(external_execution=True)
def render_bar_chart(title: str, data: list[dict]):
"""
Render a bar chart in the chat.
Call this whenever the user asks for a chart or a comparison.
Args:
title (str): A concise chart title.
data (list[dict]): Items shaped `{label, value}`.
"""
agent = Agent(
model=OpenAIChat(id="gpt-4o"),
db=db,
tools=[render_bar_chart],
instructions=SYSTEM_PROMPT,
)Give the agent somewhere to store the paused run
Agno stores the paused run before the browser can return its result, so
the Agent that owns the external tool needs a database. An agent with an
external tool and no db cannot resume after the browser answers.
from agno.db.sqlite import SqliteDb
db = SqliteDb(db_file="tmp/agno.db")SqliteDb needs sqlalchemy installed. For production use durable shared
storage such as PgDb, because an ephemeral container file cannot resume a
run on another instance.
Import the React hook and Zod in the component that registers the tool. This also applies to the built-in agent, which needs no backend tool-registration step.
import { useComponent } from "@copilotkit/react-core/v2";
import { z } from "zod";useComponent takes a name, a Zod schema for its props, and the component
to render. The runtime registers it as a frontend tool so the agent can
discover it, and the schema becomes that tool's parameter definition — it is
what tells the model which arguments to send.
parameters is optional, but leaving it out advertises the tool with an
empty parameter schema ({ "type": "object", "properties": {} }). The model
then has nothing to fill in, so it calls the tool with no arguments and your
component renders with no props. Pass a schema for any component that needs
data.
useComponent({ name: "render_bar_chart", description: "Display a bar chart with labeled numeric values.", parameters: barChartPropsSchema, render: BarChart, });The component itself is ordinary React: it reads only its props and can stream in as the agent fills the payload. The example above uses Recharts for the bar chart; it doesn't know anything about CopilotKit.
The name you pass to useComponent is what the agent sees as the tool
name. Make it a verb like render_bar_chart or show_weather so the LLM
reliably picks it when the user asks for that visualization.
Rendering in a headless chat#
CopilotKit's built-in chat components paint registered components for you. A headless or custom chat renders the message list itself, so nothing paints a tool call unless you render it — the component is registered and the agent calls it, but the chat stays empty.
Render the tool calls on each assistant message with
CopilotChatToolCallsView:
import { CopilotChatToolCallsView } from "@copilotkit/react-core/v2";
<CopilotChatToolCallsView message={assistantMessage} messages={allMessages} />;It looks up the sibling tool-role message for each tool call and hands both
to the registered renderer. For finer placement, call useRenderToolCall() and
paint each tool call yourself — see
Headless UI.