Back to Copilotkit

Fixed Schema A2UI

docs/content/docs/integrations/langgraph/generative-ui/a2ui/fixed-schema.mdx

1.58.04.7 KB
Original Source

import { Callout } from "fumadocs-ui/components/callout" import { Steps, Step } from "fumadocs-ui/components/steps"

In the fixed-schema approach, you design the UI schema once (in a JSON file or using the A2UI Composer) and your agent tool only provides the data. The surface appears instantly when the tool returns.

How it works

  1. Schema is loaded from a JSON file at startup
  2. Agent tool receives data from the LLM (e.g., flight search results)
  3. Tool returns a2ui.render() with createSurface + updateComponents + updateDataModel
  4. The A2UI middleware intercepts the tool result and renders the surface

Implementation

<Steps> <Step> ### Create the A2UI schema

Design your schema using the A2UI Composer or write it by hand. Save it as a JSON file:

apps/agent/src/a2ui/schemas/flight_schema.json
</Step> <Step> ### Define the agent tool (Python)
python
from copilotkit import a2ui
from langchain.tools import tool
from pathlib import Path
from typing import TypedDict

class Flight(TypedDict):
    id: str
    airline: str
    airlineLogo: str
    flightNumber: str
    origin: str
    destination: str
    date: str
    departureTime: str
    arrivalTime: str
    duration: str
    status: str
    statusIcon: str
    price: str

SURFACE_ID = "flight-search-results"
FLIGHT_SCHEMA = a2ui.load_schema(
    Path(__file__).parent / "a2ui" / "schemas" / "flight_schema.json"
)
BOOKED_SCHEMA = a2ui.load_schema(
    Path(__file__).parent / "a2ui" / "schemas" / "booked_schema.json"
)

@tool
def search_flights(flights: list[Flight]) -> str:
    """Search for flights and display results as rich cards."""
    return a2ui.render(
        operations=[
            a2ui.create_surface(SURFACE_ID),
            a2ui.update_components(SURFACE_ID, FLIGHT_SCHEMA),
            a2ui.update_data_model(SURFACE_ID, {"flights": flights}),
        ],
        action_handlers={
            # Exact match: fires when a button with action.name="book_flight" is clicked
            "book_flight": [
                a2ui.update_components(SURFACE_ID, BOOKED_SCHEMA),
                a2ui.update_data_model(SURFACE_ID, {
                    "title": "Booking Confirmed",
                    "detail": "Your flight has been booked.",
                }),
            ],
            # Catch-all: fires for any button action without a specific match
            "*": [
                a2ui.update_data_model(SURFACE_ID, {
                    "status": "Action received",
                }),
            ],
        },
    )

Key points:

  • The Flight TypedDict is essential — LangChain serializes it into the tool's JSON schema, which is what the LLM sees when deciding what data to generate.
  • action_handlers declares optimistic UI responses. When a user clicks a button, the matching handler replaces the surface instantly — no round-trip to the server.
  • "book_flight" matches the action.name from the schema button. "*" is a catch-all for any unmatched action. </Step>
<Step> ### Register the tool
python
from src.a2ui_fixed_schema import search_flights

agent = create_agent(
    tools=[search_flights, ...],
    ...
)
</Step> <Step> ### Configure the runtime (TypeScript)

Enable A2UI in your CopilotRuntime. The middleware auto-detects A2UI operations in any tool result, so no tool injection is needed here — the agent's search_flights tool returns them directly.

typescript
const runtime = new CopilotRuntime({
  agents: { default: myAgent },
  a2ui: {},
});
</Step> </Steps>

Action handler details

The action_handlers in the example above work together with buttons defined in the A2UI schema. Here's how the schema side looks:

Button with action context

In your flight_schema.json, buttons declare an action with data-bound context fields. When clicked, the values are resolved from that specific card's data:

json
{
  "Button": {
    "label": "Book",
    "action": {
      "name": "book_flight",
      "context": [
        { "key": "flightNumber", "value": { "path": "/flightNumber" } },
        { "key": "price", "value": { "path": "/price" } }
      ]
    }
  }
}

When this button is clicked on a card showing flight AA100 at $350, the "book_flight" handler fires with context: { flightNumber: "AA100", price: "$350" }, and the surface instantly replaces with the booking confirmation.

<Callout type="info"> For custom frontend handling with `useA2UIActionHandler`, custom orchestrators, and the full resolution chain, see the [Advanced — Action Handlers](./advanced#action-handlers) guide. </Callout>