Skip to main content

What is Static AG-UI?

Static AG-UI is a generative UI approach where you hand-craft React components and register them with CopilotKit. Your agent then decides when to invoke these components and what data to pass to them. This approach guarantees:
  • Visual polish and consistency - You control every pixel
  • Type safety - Full TypeScript support with Zod schemas
  • Predictability - Components behave exactly as designed
  • Brand alignment - Components match your design system

Two Approaches: Display vs Tool Rendering

CopilotKit provides two hooks for static generative UI, each serving a different purpose:

useComponent: Display Components

Use useComponent when you want the agent to directly render a visual component as a tool. The component appears in the chat without requiring backend tool execution.
import { useComponent } from "@copilotkit/react-core/v2";
import { z } from "zod";

useComponent({
  name: "showChart",
  description: "Display a chart with data visualization",
  parameters: z.object({
    title: z.string(),
    data: z.array(z.object({ 
      label: z.string(), 
      value: z.number() 
    })),
  }),
  render: ({ title, data }) => (
    <div className="chart-container">
      <h3>{title}</h3>
      <BarChart data={data} />
    </div>
  ),
});
When to use:
  • You want the agent to display rich UI (cards, charts, tables) directly
  • No backend processing is needed
  • The component is purely presentational

useRenderTool: Tool Call Rendering

Use useRenderTool when you want to customize how backend tool calls appear in the chat. This shows users what tools your agent is executing and their progress.
import { useRenderTool } from "@copilotkit/react-core/v2";
import { z } from "zod";

useRenderTool({
  name: "searchDocs",
  parameters: z.object({ 
    query: z.string() 
  }),
  render: ({ status, parameters, result }) => {
    if (status === "inProgress") {
      return <div>Preparing search...</div>;
    }
    if (status === "executing") {
      return <div>Searching for "{parameters.query}"...</div>;
    }
    return <div>Found results: {result}</div>;
  },
});
When to use:
  • Your agent calls backend tools (database queries, API calls, computations)
  • You want to show progress indicators or tool arguments
  • You want to render custom results when tools complete

Complete Example: Weather Display

Let’s build a complete example showing both approaches:

Frontend: Register Components

app/page.tsx
"use client";

import { CopilotKit } from "@copilotkit/react-core/v2";
import { CopilotSidebar } from "@copilotkit/react-ui/v2";
import { useComponent, useRenderTool } from "@copilotkit/react-core/v2";
import { z } from "zod";
import { WeatherCard } from "@/components/WeatherCard";

export default function Home() {
  // Display component: Shows weather data
  useComponent({
    name: "showWeather",
    description: "Display weather information in a card format",
    parameters: z.object({
      location: z.string().describe("City name"),
      temperature: z.number().describe("Temperature in Celsius"),
      condition: z.string().describe("Weather condition"),
      humidity: z.number().describe("Humidity percentage"),
    }),
    render: WeatherCard,
  });

  // Tool renderer: Shows API call progress
  useRenderTool({
    name: "getWeather",
    parameters: z.object({
      location: z.string(),
    }),
    render: ({ status, parameters, result }) => (
      <div className="tool-call-indicator">
        {status === "executing" && (
          <>
            <Spinner />
            <span>Fetching weather for {parameters.location}...</span>
          </>
        )}
        {status === "complete" && (
          <span>Weather data retrieved ✓</span>
        )}
      </div>
    ),
  });

  return (
    <CopilotKit runtimeUrl="/api/copilotkit">
      <div className="app-container">
        <h1>Weather Assistant</h1>
        <CopilotSidebar />
      </div>
    </CopilotKit>
  );
}

Component: Weather Card

components/WeatherCard.tsx
import { z } from "zod";

export const WeatherCardProps = z.object({
  location: z.string(),
  temperature: z.number(),
  condition: z.string(),
  humidity: z.number(),
});

export function WeatherCard({ 
  location, 
  temperature, 
  condition, 
  humidity 
}: z.infer<typeof WeatherCardProps>) {
  return (
    <div className="weather-card">
      <h3>{location}</h3>
      <div className="temperature">{temperature}°C</div>
      <div className="condition">{condition}</div>
      <div className="details">
        <span>Humidity: {humidity}%</span>
      </div>
    </div>
  );
}

Agent: Python Backend (AG2 Example)

agent.py
from typing import Annotated
from autogen import ConversableAgent, LLMConfig
from autogen.ag_ui import AGUIStream

agent = ConversableAgent(
    name="weather_assistant",
    system_message="You are a weather assistant. Use tools to fetch weather data.",
    llm_config=LLMConfig({"model": "gpt-4"}),
)

@agent.register_for_llm(
    description="Get weather data for a location"
)
def get_weather(
    location: Annotated[str, "City name"]
) -> dict:
    # Simulate API call
    return {
        "location": location,
        "temperature": 22,
        "condition": "Sunny",
        "humidity": 65
    }

# Register for execution
agent.register_for_execution(name="get_weather")(get_weather)

Agent vs Tool Rendering: When to Use Which

ScenarioUseWhy
Display data visualizationuseComponentNo backend processing needed
Show a chart or carduseComponentPurely presentational
Render preview or summaryuseComponentDirect display of information
Show tool execution progressuseRenderToolUser needs transparency
Display API call statususeRenderToolBackend processing happening
Render database query resultsuseRenderToolTool returning data

Advanced: Tool Rendering States

useRenderTool provides three states to handle different execution phases:
useRenderTool({
  name: "analyzeData",
  parameters: z.object({
    dataset: z.string(),
    analysisType: z.enum(["summary", "detailed"]),
  }),
  render: ({ status, parameters, result }) => {
    // State 1: inProgress (partial parameters streaming in)
    if (status === "inProgress") {
      return (
        <div className="analyzing">
          <Spinner size="small" />
          <span>Starting analysis...</span>
        </div>
      );
    }

    // State 2: executing (all parameters received, tool running)
    if (status === "executing") {
      return (
        <div className="analyzing">
          <ProgressBar />
          <span>
            Running {parameters.analysisType} analysis on {parameters.dataset}
          </span>
        </div>
      );
    }

    // State 3: complete (tool finished, result available)
    return (
      <div className="analysis-result">
        <h4>Analysis Complete</h4>
        <pre>{result}</pre>
      </div>
    );
  },
});

Agent Scoping

Both hooks support scoping to specific agents:
useComponent({
  name: "showMetrics",
  agentId: "analytics-agent",  // Only this agent can use it
  parameters: MetricsSchema,
  render: MetricsCard,
});

useRenderTool({
  name: "runQuery",
  agentId: "database-agent",  // Scoped to database agent
  parameters: QuerySchema,
  render: QueryProgress,
});

Comparison with Other Approaches

FeatureStatic AG-UIDeclarative A2UIOpen-ended MCP
Component controlDeveloperSpec-definedAgent/MCP Server
Type safetyFull (Zod + TS)Schema-levelNone
Visual consistencyGuaranteedGoodVariable
Setup complexityMediumMediumLow
Cross-platformReact-basedMulti-rendererWeb-only
MaintainabilityHighMediumLow

Best Practices

1. Keep Components Simple and Focused

// Good: Single-purpose component
useComponent({
  name: "showUserProfile",
  parameters: UserSchema,
  render: UserProfile,
});

// Avoid: Mega-component doing too much
useComponent({
  name: "showEverything",
  parameters: EverythingSchema,
  render: MegaComponent, // Hard to maintain
});

2. Use Descriptive Names and Descriptions

useComponent({
  name: "showWeatherForecast",  // Clear, specific name
  description: "Display a 5-day weather forecast with temperatures and conditions",
  parameters: ForecastSchema,
  render: ForecastCard,
});

3. Leverage Zod Descriptions for Better Agent Understanding

const SearchParams = z.object({
  query: z.string().describe("The search query, e.g. 'React hooks tutorial'"),
  maxResults: z.number()
    .min(1)
    .max(20)
    .describe("Maximum number of results to return (1-20)"),
  category: z.enum(["all", "docs", "examples"])
    .describe("Filter results by category"),
});

4. Handle All Tool States Gracefully

useRenderTool({
  name: "processData",
  parameters: ParamsSchema,
  render: ({ status, parameters, result }) => {
    switch (status) {
      case "inProgress":
        return <Skeleton />;
      case "executing":
        return <ProgressIndicator params={parameters} />;
      case "complete":
        return <ResultDisplay result={result} />;
      default:
        return null;
    }
  },
});

Next Steps

useComponent API

Full API reference for useComponent hook

useRenderTool API

Full API reference for useRenderTool hook

Declarative A2UI

Learn about JSON-based UI specifications

MCP Apps

Explore open-ended HTML/JavaScript rendering