---
title: "Fixed Schema A2UI"
description: "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](https://a2ui-composer.ag-ui.com/)) 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 surfaceUpdate + dataModelUpdate + beginRendering
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](https://a2ui-composer.ag-ui.com/) 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 title="apps/agent/src/a2ui_fixed_schema.py"
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 `Flight` TypedDict is essential — LangGraph 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 title="apps/agent/main.py"
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 title="app/api/copilotkit/route.ts"
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>
