Fixed Schema A2UI
Pre-defined A2UI schema with dynamic data. The fastest approach — no LLM schema generation needed.
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#
- Schema is loaded from a JSON file at startup
- Agent tool receives data from the LLM (e.g., flight search results)
- Tool returns
a2ui.render()with surfaceUpdate + dataModelUpdate + beginRendering - The A2UI middleware intercepts the tool result and renders the surface
Implementation#
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
Define the agent tool (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.surface_update(SURFACE_ID, FLIGHT_SCHEMA),
a2ui.data_model_update(SURFACE_ID, {"flights": flights}),
a2ui.begin_rendering(SURFACE_ID, "root"),
],
action_handlers={
# Exact match: fires when a button with action.name="book_flight" is clicked
"book_flight": [
a2ui.surface_update(SURFACE_ID, BOOKED_SCHEMA),
a2ui.data_model_update(SURFACE_ID, {
"title": "Booking Confirmed",
"detail": "Your flight has been booked.",
}),
a2ui.begin_rendering(SURFACE_ID, "root"),
],
# Catch-all: fires for any button action without a specific match
"*": [
a2ui.data_model_update(SURFACE_ID, {
"status": "Action received",
}),
],
},
)
Key points:
- The
FlightTypedDict is essential — LangGraph serializes it into the tool's JSON schema, which is what the LLM sees when deciding what data to generate. action_handlersdeclares 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 theaction.namefrom the schema button."*"is a catch-all for any unmatched action.
Register the tool#
from src.a2ui_fixed_schema import search_flights
agent = create_agent(
tools=[search_flights, ...],
...
)
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.
const runtime = new CopilotRuntime({
agents: { default: myAgent },
a2ui: {},
});
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:
{
"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.
For custom frontend handling with useA2UIActionHandler, custom orchestrators, and the full resolution chain, see the Advanced — Action Handlers guide.
