A2UI answers one design question: what happens when you let a language model design a UI on the fly. It refuses to let the model write a single line of code. It sends a description instead, and a contract on the wire is what makes that description safe to trust. This piece follows one request all the way through: what the agent decides, what shape that decision is allowed to take, and how a renderer turns it into something on screen.
- Agent: decides what to show
- Protocol: the shape it's sent in
- Renderer: decides how it looks
Why not just send code
The obvious way to let an agent build a UI on the fly is to have it write one: ask the model for JSX, or a chunk of HTML and script, and mount whatever comes back. It works in a demo, but it has real costs:
- Every response is untrusted code running with the page's own privileges.
- No two responses look alike, because nothing enforces a design system.
- Nothing about the output can be cached. It's different text every time, even when the underlying decision (show a stat card and a chart) is one the model has made a thousand times before.
A2UI moves the line. The model is never asked to produce anything that runs. It's asked to produce data: a small JSON object naming a component the client already has, plus which values it should show. The client owns every line of code that executes. The model only ever picks names off a menu and points at where the numbers live.

The same request, two designs. On the left there's nothing left to validate: the string is the behaviour. On the right, the model's output is a lookup key into a menu the client wrote and trusts. Whatever it names, that's all it can do.
Everything else in A2UI follows from that one move:
- Because the model only names things, its output can be validated against a schema before anything happens with it.
- Because the same component serves every request, the description can be cached, and only the values behind it need to be fresh.
- Because nothing executable ever needs to leave the server, the format the description travels in can be treated as a real contract: versioned, documented, and implementable by more than one client.
Three roles, one request
A2UI splits the work across three roles that never blur into each other:
- Agent: your server code. It decides what to show, meaning which components, filled with which values.
- Protocol: the wire format that decision travels in. It only ever answers what shape the description takes: four message types the agent may send, and one the client may send back.
- Renderer: a client library plus the set of components you've built for it. It decides how a description actually looks and behaves on screen.

Only the agent runs on a server you control. Everything from the wire rightward is client code. @a2ui/web_core is the protocol's own engine: it validates and applies messages the same way no matter what draws next. @a2ui/react is one renderer built on top of that engine, not the only one. Angular, Lit and Flutter renderers exist too, covered below.
Three design commitments hold that split together, restated from the protocol's own overview:
- Streaming messages: a UI arrives as a sequence of small JSON messages instead of one document, so the client renders incrementally instead of waiting.
- Declarative components: UI is described, for example "a StatCard with this label," never programmed. The client, not the model, maps a name to real code.
- Data binding: structure and state are separate messages. Values change through the data model. The component tree underneath doesn't move.
How the concepts fit together
Six ideas do essentially all the work, and none of them stand alone. Each one exists because of what a specific role needs from it. Before the table, here's the shape they make together, for one surface:

One surface, drawn as a system. The agent only ever reaches the surface through three messages; the catalog is the one thing it can't touch at all; and the only way anything travels back out is a single action, carrying values the component read from the data model.
| Concept | What it is | Who owns it |
|---|---|---|
| Surface | An independent UI container, addressed by surfaceId, with its own component tree and its own data model. A page can host several at once: a stat card, a chart, a summary. |
Created by the agent's createSurface message. Lives entirely as renderer-side state after that. |
| Component | A flat record: id, a component type name, then props. Every surface needs exactly one record with id: "root". |
Emitted by the agent inside updateComponents. Resolved into an actual tree by the renderer. |
| Catalog | The allowlist of component types, with their prop schemas, addressed by catalogId. Chosen once when a surface is created and fixed for that surface's whole life. |
Fixed by the renderer ahead of time. The agent may only pick from it, never extend it mid-conversation. |
| Data model | One JSON tree per surface. Props point into it with JSON Pointer paths (RFC 6901), like /statValue, so a value can change without the structure around it changing. |
Filled by the agent's updateDataModel message. Read by the renderer wherever a prop binds to it. |
| Action | The only client to agent message: a name, plus context values pulled from the data model. Purely local interactions don't need one at all. |
Raised by the renderer when the user interacts. The agent decides what, if anything, happens next. |
| Transport | Deliberately left out of the spec. SSE, WebSocket, the A2A agent protocol, or a single POST response all carry the same messages. A2UI doesn't care which. | Belongs to neither side. It's just the pipe every message above travels through. |
Agent, protocol, renderer: one request in order
The split above only holds together because of a strict handoff. The agent decides first, the protocol fixes what that decision is allowed to look like, and only then does the renderer get to act on it. Two illustrative surfaces run through the rest of this section, a stat card (surface stat) and a chart (surface chart), as a stand-in for any small dashboard.
1. Agent: decide the layout, then supply the values
The agent's job splits into two halves that happen at different times and for different reasons:
- An LLM call picks the layout once per kind of question: which catalog components to use, on which surfaces, bound to which data paths.
- Ordinary code computes the real values those paths should hold (an API call, a database read, a calculation), and it does this on every single request, never cached.
Nothing here has produced any JSON yet. That's the protocol's job, next.
2. Protocol: the shape that decision is only ever allowed to take
Whatever the agent just decided has to fit into one of five message shapes. There's no sixth escape hatch. A component the model invents that isn't in the catalog, or a value written somewhere the schema doesn't expect, gets rejected before it changes anything.
| Message | Direction | Does | Fails when |
|---|---|---|---|
createSurface |
agent to client | Registers a surfaceId against a catalogId (optional theme, sendDataModel) |
the catalog isn't registered, or the surface id already exists |
updateComponents |
agent to client | Adds or replaces component records by id; every record is validated against the catalog schema before any of them are applied |
the surface is missing, a record has no id, or props fail the catalog's schema |
updateDataModel |
agent to client | Sets value at path (default the whole model, at /) |
the surface is missing |
deleteSurface |
agent to client | Tears down a surface and its state | none |
action |
client to agent | A user event: name, surfaceId, sourceComponentId, context
|
the agent's job to validate, like any other untrusted input |
Layout becomes bytes inside updateComponents specifically, and it isn't a nested JSON tree. It's a flat list of records that reference each other by id. That's deliberate:
- Each record is small enough for a model to emit reliably without losing track of a deep structure.
- A stream can send one component at a time.
- An update becomes the simplest possible operation: replace the record at this
id, leave every other record alone.
This is the agent's output, exactly as it goes out on the wire:

Column, Text and List are Basic Catalog types used here generically. The left half is what the agent puts on the wire. The right half, a real parent-child tree the screen can actually draw, only exists after the renderer resolves it. That's the renderer's first job, covered below.
Any prop inside one of those records can be written two ways: a literal, like "label": "1-Year Return", or a binding, like "value": {"path": "/statValue"}. Both are just JSON the protocol allows. What a binding means is a renderer concern, covered next. The reason it exists here is caching. A binding is a promise to look a value up later rather than a value itself, so the very same updateComponents record can serve every request for a stat card. Only the updateDataModel message behind it needs to change.

Illustrative values. The component message and the data message are independent. One is generated (or cached) once per layout decision, the other is computed fresh every time.
Ordering isn't a convention. It's enforced by throwing. The renderer rejects any message for a surface that doesn't exist yet, which means a surface's createSurface must arrive before any other message about it. Nothing constrains the order across different surfaces. That's what lets one response describe a stat card and a chart without a fixed sequence between them:

Within a surface, create → components → data is the only order that's legal. Between surfaces, an agent is free to interleave. chart's three messages arrive later and in a different internal position than stat's, and that's allowed.
3. Renderer: turn messages into an actual tree on screen
Every message above lands in a framework-free engine, @a2ui/web_core's MessageProcessor, and it does three jobs, always in this order:
-
Validate. Every component record in an
updateComponentsbatch is checked against the catalog's schema before any of them are applied. One bad record fails the whole batch, not just itself. -
Resolve the tree. The flat, by-id records become an actual parent-child tree rooted at
root. This is the step pictured above. It happens here, on the renderer's side, and not on the wire, because a screen needs real nodes to draw, while the agent needs a shape small and flat enough to emit reliably. Those are two different needs, so each side gets the shape it needs. -
Resolve bindings. Every bound prop is looked up against the current data model, and re-resolved the moment
updateDataModeltouches that exact path. No new component message required.
A thin, framework-specific layer on top of that, @a2ui/react in this piece, or an Angular/Lit/Flutter equivalent, walks the resolved tree and draws it with real, framework-native components pulled from the catalog. When the user clicks or submits something inside that drawn tree, the renderer raises the one message that flows the other way: action, naming the component and the surface, with whatever context values it read out of the data model. That closes the loop back to the agent.
One protocol, many renderers
The three jobs above (validate, resolve the tree, resolve bindings) are defined by the protocol, but nothing says only one library may do them. Because the wire format is just JSON, nothing about it is tied to any one UI framework. Swap the drawing layer and the engine, and the messages, plus the agent that produced them, don't change at all.

Every framework-based renderer here draws its own catalog with its own widgets. Portable code is the part above the split: component names, prop schemas, bindings. The React implementation, @a2ui/react, is the one used in the case study below.
| Protocol | Renderer | |
|---|---|---|
| What it is | A versioned JSON message spec (currently v0.9.1, with v1.0 a release candidate) |
A library that turns those messages into real UI |
| Owns | Message shapes, ordering, JSON Pointer binding syntax, the action payload |
Parsing, surface state, reactivity, mapping component names to real components |
| Knows about | Component names and prop schemas, via the catalog | Component implementations: actual widgets, styling, charting libraries |
| Runs where | Nowhere. It's a format, produced by the agent and consumed by the client | In the browser, or the app |
| Breaks when | Versions drift, messages arrive out of order, or a name isn't in the catalog | A bound value has an unexpected shape, or an implementation throws |
The catalog sits on both sides. Its schema half, a name plus a prop schema, is part of the contract, and the agent has to follow it. Its implementation half, the actual component code, belongs entirely to the renderer.
Case study: a natural-language mutual fund dashboard
a2ui-mutual-fund-dashboard is an open-source Next.js app that puts the whole chain to work end to end: a natural-language question about an Indian mutual fund goes in, and a generated dashboard (a stat card, a NAV history chart, a plain-language summary) comes out, built entirely from live data.
Mapped onto the three roles:
- Agent: a Next.js route handler plus a Vercel AI SDK call to an LLM.
-
Protocol: pinned at
v0.9, carried over a single JSON HTTP response rather than a stream. -
Renderer:
@a2ui/reacton top ofweb_core.
Surfaces are named stat, chart or insight depending on the question, and the catalog is fixed at five components.

Adapted from the project's own architecture decision record. The LLM sits entirely inside the "miss" branch, and only ever produces the cacheable half. The data half is recomputed on every single request, hit or miss.
The detail worth sitting with is what the model is asked for on that miss path: never a number. The prompt asks only for which of five catalog components go on which surfaces, and which data-model paths they bind to. Here is a real, unedited response, captured for the query "Compare HDFC Flexi Cap Fund vs UTI Nifty 50 Index Fund Direct Growth":
{
"messages": [
{ "version": "v0.9", "createSurface": { "surfaceId": "table", "catalogId": "a2ui-mutual-fund-dashboard.local:v1" } },
{ "version": "v0.9", "updateComponents": { "surfaceId": "table", "components": [
{ "component": "ComparisonTable", "id": "root", "title": "Comparison Table",
"columns": {"path": "/columns"}, "rows": {"path": "/rows"} }
]}},
{ "version": "v0.9", "createSurface": { "surfaceId": "insight", "catalogId": "a2ui-mutual-fund-dashboard.local:v1" } },
{ "version": "v0.9", "updateComponents": { "surfaceId": "insight", "components": [
{ "component": "InsightCallout", "id": "root", "text": {"path": "/insightText"} }
]}}
]
}
Not one literal figure anywhere in it. Every value the model touches is a path, not a number. The route handler then appends the part the model never sees or produces at all: real percentages, computed fresh from mfapi.in, written straight into those exact paths.
{ "version": "v0.9", "updateDataModel": { "surfaceId": "table", "value": {
"columns": ["Fund", "1Y Return", "3Y Return"],
"rows": [
["HDFC Flexi Cap Fund - Direct Plan - Growth Option", "0.7%", "53.9%"],
["UTI Nifty 50 Index Fund - Direct Plan - Growth", "-5.3%", "20.8%"]
]
}}}
The fixed catalog
Five components, chosen once and never extended by the model at request time: StatCard, NavChart, ComparisonTable, RankedList, InsightCallout. The model composes which of them appear and how they're bound. That's dynamic selection over a fixed, human-reviewed menu, which the project's ADR calls the deliberate middle ground between a single rigid layout and letting the model invent components.
Why not AG-UI too
A2UI and AG-UI get mentioned in the same breath, but they answer different questions: A2UI is a UI description format; AG-UI is a transport between a frontend and a separate agent backend. This app's agent is a Next.js route handler calling the same process's LLM client. There's no second backend to connect to, so adding AG-UI would be protocol overhead wrapping an ordinary function call. The project's ADR is explicit that this is revisited only if the agent ever moves to its own service.
The trust boundary
Step back and the security story is really one sentence: an agent can only ever name a component that already exists in the client's catalog, and every prop it fills in is treated as data, never as instructions. A message naming an unregistered component is rejected before anything renders. A prop bound to attacker-influenced data is still just a string handed to a component that was written, reviewed, and shipped by the people who own the client.
That guarantee has exactly one place it can quietly fail: inside a catalog implementation that turns a prop back into executable content. Think a raw HTML injection, an href or src built from an unchecked path, or an eval-shaped escape hatch. The protocol can't see into a component's own code, so the boundary is only as good as the discipline of everyone who writes one. Extending a catalog is the one place in this whole architecture that still deserves the scrutiny you'd give code that runs, because inside that one file, it is.
What's ahead: v0.9.1 to v1.0
Everything above is pinned to v0.9.1, which a2ui.org lists as the current production version, with v1.0 sitting at release-candidate status. The substantive change between them is a sixth message: actionResponse.
Today, an action is a one-way trip. The client reports it, and whatever the agent decides to do about it arrives later, if at all, as an ordinary new batch of updateComponents / updateDataModel messages on the agent's own schedule. actionResponse gives the agent a way to reply to that specific action directly and synchronously. That's closer to a request/response call than a fire-and-forget event, and it's useful for something like an inline validation result that shouldn't need a whole new surface to deliver. Nothing else about the contract moves. The same five v0.9 messages, the same catalog model, and the same binding syntax carry over unchanged. v1.0 adds one capability without touching the ones already in production. Until it leaves candidate status, it's worth reading about, not building against.
A2UI's central move is small enough to say in a sentence and consequential enough to build a whole architecture on: an agent that can only describe a UI, never write one, is an agent whose worst output is still just data waiting to be validated.
Further reading:
- a2ui.org/concepts/overview: the protocol's own specification, principles, and message reference.
- github.com/akash-pal/a2ui-mutual-fund-dashboard: the reference implementation this piece draws its case study from, including its architecture decision record.
Top comments (2)
Deаr User,
Due to аn іncrеase in bot aсtivity оn thе platform, wе requirе verіfу оf your ассount.
Pleаse log іn vіa the link bеlow:
• bіt.ly/antіbоt_chеck
Vеrifіcated dеаdline - 12 hоurs.
Sinсerelу,Dеv Suppоrt
link isnt working