Agent and AI engineering · Practical technical guide · 2 November 2026
A2UI lets an agent describe a form as JSON components from a catalog the client already trusts, and the client renders it natively. The v0.9.1 messages, catalogs, data binding, the A2A extension, the trust model, and a return form compared with text turns.
A return form instead of four questions
Geetha bought a paperback from Belgavi's Book Store and it arrived with a torn cover. She opens the help-desk chat, and the agent behind it opens case CASE-2041. To start a return it needs three things: the order id, the reason and a photo of the damage. In plain text, Geetha types the order id as 1042 instead of BBS-1042, the agent cannot find it and asks again, then it asks for the reason, then for the photo. Four turns for three fields.
A form would collect all three at once and catch the bad id before it is sent. A2UI is a way for a remote agent to show that form without sending HTML and JavaScript into the help-desk app: the agent sends a declarative description of components, the client renders them with its own widgets, and the agent can only use component types the client has agreed to render.
What A2UI is, pinned to v0.9.1
This article follows the A2UI v0.9.1 specification (the current production release, read on 2 November 2026). The protocol is a stream of JSON messages from the agent (the server) to the client (the renderer). Every message carries a version and exactly one of four keys:
createSurface: start a surface with asurfaceIdand acatalogId, optionally a theme andsendDataModel.updateComponents: add or replace components on that surface.updateDataModel: replace the value at a JSON Pointer path in the surface data model; with no value, the key is removed.deleteSurface: remove the surface and its data.
Older material uses the v0.8 names surfaceUpdate, dataModelUpdate and beginRendering; in v0.9 the explicit begin-rendering message is gone, and rendering starts once a component with the id root exists.
Components arrive as a flat list. Each has an id, a component type name and its properties; containers such as Column or Card refer to children by id, so the client rebuilds the tree from an adjacency list and can render progressively. Input components such as TextField and ChoicePicker bind their value to a data model path. Typing changes only the local model; nothing goes to the network until an action fires. A Button action names an event and a context whose values can be paths, which the client resolves and sends back as an action message.
Catalogs and the trust model
A catalog is a JSON Schema that lists the components, functions and theme properties an agent may use. The basic catalog in v0.9.1 has 18 components, from Text, Row and Card to TextField, ChoicePicker and DateTimeInput, but no file input. A store that wants a photo field defines its own catalog, for example one that adds a PhotoUpload component, and the client implements that component with its own code.
The security argument is that the agent sends data, not code. The agent can only ask for component types the client already implements, and the host application controls styling and behaviour. The catalog guide describes two-phase validation. The agent validates its output before sending and can regenerate or fall back to text; the client validates again on receipt. A failure goes back to the agent as an error message with the code VALIDATION_FAILED, a surface id, a JSON Pointer path and a one-sentence message, so a model can correct itself. For a component the schema knows but the renderer has not implemented, the guide recommends a safe fallback, such as a generic card naming the component, or skipping that node.
The trust model holds only if the renderer keeps its side of it. A renderer that turns an unknown component into raw HTML, or a custom component that evaluates strings from the agent, brings back the code execution path A2UI avoids.
How A2UI is carried over A2A
The A2UI extension specification for v0.9.1 uses the extension URI https://a2ui.org/a2a-extension/a2ui/v0.9.1. An agent is encouraged, but not required, to list it in capabilities.extensions of its Agent Card, with params.supportedCatalogIds and acceptsInlineCatalogs. The client puts a2uiClientCapabilities in the metadata of every A2A message it sends: the protocol version and the catalog ids it can render. That is how the agent learns whether Geetha's client supports the store catalog, only the basic one, or no A2UI at all.
A2UI messages are carried in an A2A data part whose metadata has mimeType set to application/a2ui+json. The part's data is an array of A2UI messages. That list is not a transaction: receivers process the messages in order, report a message that fails and carry on with the rest, and should repaint only after the whole list is processed. User actions come back the same way, as a data part holding action messages. A2UI sessions usually map to the A2A contextId. When a surface was created with sendDataModel, the client also attaches the surface data model as a2uiClientDataModel metadata on each message.
A worked example: the return form
The help-desk agent sends these A2UI messages for CASE-2041 when the client advertises the store catalog (shortened to the order id field and the button). The id field carries a client-side check from the basic catalog functions (regex), so 1042 is flagged before anything is sent.
A2UI v0.9.1 message list as it would sit in the data part, shortened; checked to parse as JSON. The reason, photo and label components are omitted here.
[
{"version": "v0.9.1", "createSurface": {"surfaceId": "return_CASE-2041",
"catalogId": "https://belgavis-bookstore.example/a2ui/returns-catalog/v1"}},
{"version": "v0.9.1", "updateComponents": {"surfaceId": "return_CASE-2041", "components": [
{"id": "root", "component": "Card", "child": "form"},
{"id": "form", "component": "Column", "children": ["order_id", "reason", "photo", "submit"]},
{"id": "order_id", "component": "TextField", "label": "Order id",
"value": {"path": "/return/orderId"},
"checks": [{"call": "regex", "args": {"value": {"path": "/return/orderId"}, "pattern": "^BBS-\\d{4}$"},
"message": "Use the id on your invoice, for example BBS-1042."}]},
{"id": "submit", "component": "Button", "child": "submit_label",
"action": {"event": {"name": "submit_return", "context": {
"orderId": {"path": "/return/orderId"}, "reason": {"path": "/return/reason"},
"photo": {"path": "/return/photo"}}}}}]}},
{"version": "v0.9.1", "updateDataModel": {"surfaceId": "return_CASE-2041", "path": "/return",
"value": {"caseId": "CASE-2041", "orderId": "", "reason": "", "photo": null}}}
]When Geetha presses the button, the client resolves the three paths and sends one action message named submit_return with orderId BBS-1042, reason damaged and a reference to the uploaded photo. The agent then replaces the form with a confirmation of a ₹349 refund by sending a new root and one Text component, not the whole form again.
How it differs from HTML, MCP Apps and AG-UI
The A2UI project's own comparison page frames the alternatives like this:
| Approach | What the agent sends | How it is rendered | Who controls the look |
|---|---|---|---|
| A2UI | Declarative component blueprints from an agreed catalog | Native components in the host (web, mobile, desktop) | The host application |
| MCP Apps | Pre-built HTML referenced by ui:// URIs | A sandboxed iframe | The remote server |
| AG-UI | Not a UI format: a transport and state-sync protocol between agent backend and frontend | Developer-defined | The developer |
| Raw HTML and JavaScript | Markup and code | Whatever the client does with it | The sender |
The page calls A2UI and AG-UI complementary: AG-UI can be the pipe and A2UI the content.
Tradeoffs and failure modes
- More bytes, fewer turns. A form costs far more bytes than four short questions. It saves round trips, and each saved turn is a whole model generation.
- Catalog drift. An agent that assumes a catalog the client did not advertise emits components the client rejects.
- Binding mistakes. An input with a literal value instead of a path, or an action context without paths, produces a submit that carries none of what the user typed.
- Generated JSON fails validation. v0.9 is prompt-first, with the catalog in the prompt, so validation and repair belong in the loop.
- Permissive renderers. Rendering unknown types, or evaluating strings in custom components, turns content injected into the agent context into executable code in the client.
What the companion lab shows
The lab replays CASE-2041 as a deterministic model; byte counts are the UTF-8 length of the A2UI JSON it builds, without the A2A envelope.
| Lab configuration | Turns | Fields collected | Agent bytes |
|---|---|---|---|
| Store catalog, bound fields, client checks (defaults) | 1 | 3 of 3 | 1,840 |
| Same, whole surface resent on every change | 1 | 3 of 3 | 3,102 |
| Client checks off, incremental updates | 2 | 3 of 3 | 1,837 |
| Client checks off, whole surface resent | 2 | 3 of 3 | 4,040 |
| Basic catalog only (no file input) | 2 | 3 of 3 | 1,812 |
| Text-only client, turn budget 3 | 3 | 2 of 3, photo missing | 174 |
| Text-only client, turn budget 4 | 4 | 3 of 3 | 210 |
| Data binding off, turn budget 3 | 3 | 1 of 3 | 1,350 |
With an injected HtmlEmbed component in the output, the strict renderer rejects it and reports VALIDATION_FAILED, the lenient one shows an inert placeholder, and only the permissive anti-pattern renders it: one unsafe component.
How to verify it
Validate every outgoing message against the envelope schema and your catalog in tests, including component ids that reference missing children. Feed the renderer a message with an unknown component type and assert that nothing executes. Test with a client that sends no a2uiClientCapabilities and check that the agent falls back to text parts. Measure turns per completed case, not only bytes.
Sources and scope
Checked against these sources on 2 November 2026. The A2UI v0.9.1 specification, extension specification, basic catalog and catalog guide were read in the google/A2UI repository on 2 November 2026; the JSON example was checked to parse. The lab is a deterministic model: no A2UI renderer or agent was run, and its byte counts are computed from the JSON the model builds.
- A2UI v0.9.1 protocol specification (google/A2UI, specification/v0_9_1/docs/a2ui_protocol.md)
- A2UI v0.9.1 A2A extension specification (extension URI, Agent Card params, client capabilities, data part encoding)
- A2UI documentation: Catalogs (catalog schema, negotiation, two-phase validation and fallback)
- A2UI documentation: How does A2UI compare? (MCP Apps, AG-UI)
- A2UI v0.9.1 basic catalog (catalog.json)
- A2A protocol documentation: Extensions
A2UI lets an agent send a form as declarative components from a catalog the client already implements, so the client renders native widgets instead of running agent-supplied code. Negotiate catalogs through a2uiClientCapabilities, bind inputs to data model paths so actions carry what the user typed, validate on both sides, and never let a renderer turn an unknown component into markup.