docs/content/docs/integrations/langgraph/generative-ui/a2ui/fixed-schema.mdx
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.
a2ui.render() with createSurface + updateComponents + updateDataModelDesign 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
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:
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>
from src.a2ui_fixed_schema import search_flights
agent = create_agent(
tools=[search_flights, ...],
...
)
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: {},
});
The action_handlers in the example above work together with buttons defined in the A2UI schema. Here's how the schema side looks:
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.