Ask an agent to help you book a restaurant and watch the conversation degrade into a slot-filling interrogation.
“What day?” “Tomorrow.” “What time?” “7pm.” “How many people?” “Two.” Five turns to do what a date picker and a party-size stepper would do in one tap. Text is a terrible interface for structured decisions, and it gets worse the second you want a map, a form, or a list of choices with images.
The obvious fix — let the LLM write HTML and JavaScript directly — is a security nightmare. You’d be letting a remote, non-deterministic process emit executable code straight into your app. That’s the problem A2UI (Agent-to-User-Interface) is built to solve: agents send a declarative JSON description of UI intent, and the client renders it using its own trusted, pre-approved components. Safe like data. Expressive like code.
Google introduced A2UI in December 2025, co-developed with the Flutter team and the Gemini Enterprise product group, and open-sourced it in January 2026. It’s now at v0.9.1 (stable), with a v1.0 release candidate in the works. I spent time digging into the spec and reference implementation — here’s the architecture, where it’s actually running in production today, and what I think about it.
The core idea in one sentence
An agent never sends markup. It sends a stream of small JSON messages describing components (Card, Button, TextField, ChoicePicker…) and a separate data model those components bind to. The client owns a fixed catalog of components it trusts, maps the abstract description onto its own native widgets, and renders. The same payload can come out as a Lit web component, a React tree, an Angular template, or a Flutter widget — the agent doesn’t know or care which.
Four message types cover the whole protocol:
| Message | Purpose |
|---|---|
createSurface |
Start a new UI surface: {surfaceId, catalogId, theme?} |
updateComponents |
Add or mutate a flat list of components |
updateDataModel |
Write a value at a JSON-Pointer path |
deleteSurface |
Tear down a surface |
Two client→server messages close the loop: action (a user clicked a button, submitted a form) and error (schema validation failed, feed it back to the LLM to self-correct).
Server: createSurface(surfaceId:"main")
Server: updateComponents(components:[root, ...])
Server: updateDataModel(path:"/user", value:"Alice")
User: clicks a button
Client: action(name:"submit", context:{...}) → Server
Server: updateComponents / updateDataModel (dynamic update)
Server: deleteSurface(surfaceId:"main")
Three design decisions that make it actually work
1. The component tree is a flat list, not a tree
Components reference each other by id — a Column just holds the ids of its children. The client keeps a Map<id, Component> and rebuilds the tree at render time. This sounds like a minor implementation detail, but it’s the whole reason streaming works: a parent can reference a child that hasn’t arrived yet, the client renders a placeholder, and fills it in when the child streams in. There’s no diffing protocol — incrementality just emerges from the flat model plus a reactive store.
2. Two-way binding via JSON Pointers, with a deliberate trust boundary
Any bindable prop is either a literal, a {path} (RFC 6901 JSON Pointer into the data model), or a {call} (a named function — never executable code). Typing into a TextField updates the local data model immediately and reactively updates anything else bound to that path — but it does not hit the network. State only reaches the server when the user fires an actual action (a button click), at which point bound paths resolve into that action’s context. That’s a nice piece of design: local reactivity is free, server round-trips are opt-in and explicit.
3. Catalogs are the security and the portability seam
A catalog is a versioned schema of exactly which components and functions an agent is allowed to request — the “basic” catalog ships 18 components and ~25 functions (validation, formatting, openUrl, boolean logic). Production apps typically define their own catalog scoped to their exact design system, which gets baked into the LLM’s prompt. The agent literally cannot ask for a component that isn’t in the catalog. This is the part I find smartest about A2UI: it turns “don’t let the model write code” into a structural guarantee instead of a prompt instruction you’re hoping the model honors.
Topology: where this sits in your stack
A2UI doesn’t care about transport. It’s designed to ride whatever you’re already using:
Agent (Gemini, any LLM + SDK)
│
│ A2UI JSON ⇄ A2A / MCP / WebSocket / REST / SSE
▼
A2UI Renderer (web_core + framework shim)
│
│ renders native widgets
▼
Lit · React · Angular · Flutter
Each message travels as an A2A DataPart tagged application/a2ui+json, and A2UI is advertised as an A2A extension — negotiated via the X-A2A-Extensions header so client and agent agree on a protocol version before the conversation starts. On the client side, a shared framework-agnostic core (web_core) does message processing, the id-keyed component registry, and a schema-driven “generic binder” that auto-synthesizes two-way bindings by inspecting each component’s Zod schema — zero hardcoded per-component wiring. Lit, React, Angular, and Flutter each bolt on a thin renderer over that same core and the same schemas.
What this actually buys you
| Property | Why it matters |
|---|---|
| Security | The agent can never execute code or exfiltrate credentials through a widget — it can only request pre-approved components. API keys stay server-side. |
| Portability | One agent, same payload, renders natively across web, mobile, desktop — no per-framework agent logic. |
| Streaming-first | Flat component list + placeholders means the UI paints progressively as tokens arrive, not after a full response. |
| Structured round-trips | User picks a date from a picker instead of typing “tomorrow” — the agent gets clean JSON instead of parsing free text. |
| Design-system fit | Custom catalogs mean the generated UI looks like your app, not a generic chat bubble. |
Who’s actually running this in production
This part matters more than the spec — protocols are cheap, adoption is the signal. As of mid-2026:
- Gemini Enterprise uses A2UI to let enterprise agents generate dynamic UI on the fly — data entry forms, approval dashboards, and a reference restaurant-finder agent that renders a live, interactive Google Maps widget inline in chat, dropping pins and adjusting coordinates as results stream in. The API key for Maps never touches the LLM — it’s injected server-side.
- Flutter GenUI SDK ships A2UI as its underlying agent↔client protocol — this is the production path for Gemini-powered agents generating native mobile UI in Flutter apps.
- Opal, Google’s internal mini-program platform, was an early production consumer back on A2UI v0.8, before the v0.9 public rework.
- Personal Health Companion, an open-source project by Rebel App Studio and Codemate, replaces static health dashboards with agent-generated interfaces for medical records and wearable data.
- Life Goal Simulator, by Very Good Ventures, uses Gemini to generate interactive Flutter widgets for financial planning — retirement and homebuying scenarios rendered as live, adjustable UI rather than static projections.
- Ecosystem interoperability is further along than I expected: AG2 ships a native
A2UIAgent, Vercel addedjson-renderersupport, Oracle’s Agent Spec is compatible, and CopilotKit (maintainers of the competing-but-cooperating AG-UI protocol) built the A2UI Composer and an A2UI Widget Builder on top of it.
If you want to poke at it yourself before writing any code: the A2UI Composer is a visual editor that generates valid A2UI JSON in the browser, and A2UI Theater is an interactive playground showing the same streamed payload rendering live across Lit, React, and Angular side by side.
What I’d actually build with this
The restaurant-finder demo is the “hello world,” but the pattern generalizes anywhere an agent needs to collect structured input or present something spatial/visual instead of a wall of text:
- Approval workflows and back-office dashboards — an ops agent that renders an actual approve/reject card with the relevant fields, instead of a Slack message you have to parse.
- Insurance, travel, and booking flows — anything that today asks you seven questions in a row becomes one form with pickers, sliders, and validation.
- Device and IoT control surfaces — the same agent renders a thermostat control natively per device family, without maintaining a UI codebase per platform.
- Financial and healthcare planning tools — Life Goal Simulator is already proof this works; any “let me show you three scenarios” use case fits the same shape.
- Embedded support widgets in SaaS products — a support agent that can render your app’s actual components (via a custom catalog) instead of a generic chat bubble that looks bolted on.
- Multi-tenant agent platforms — one agent backend, N different white-labeled catalogs, so the same agent logic renders differently per customer’s design system without forking anything.
What I think
The catalog-as-trust-boundary is the best idea in this spec. It converts “please don’t let the LLM write executable code” from a prompting problem into a structural guarantee — the client validates against a schema it owns, full stop. That’s the right instinct, and it’s consistent with something I keep coming back to on this blog: let the platform (or in this case, the client’s own trusted component library) do the enforcement, not a hope and a system prompt.
The framework-agnostic core is also genuinely well-factored — one shared schema-driven binder, thin renderers on top, no per-framework reimplementation of the binding logic. That’s the kind of architecture I’d want to see more of instead of every framework inventing its own agent-UI story from scratch.
Two honest reservations. First, this is prompt-first, not structured-output-first — the LLM free-generates JSON against a schema described in its prompt, then a validator checks it and bounces errors back for self-correction. That’s more expressive than constrained decoding, but it means occasional retry loops and latency you don’t control. Second, the space is crowded: A2UI, MCP-UI, and CopilotKit’s AG-UI are all solving overlapping versions of “let an agent draw a UI safely,” and the interesting recent development isn’t any one of them winning — it’s that they’ve started building bridges (Vercel’s renderer, CopilotKit’s A2UI tooling) instead of just competing. That’s usually a sign a category is real, not that it’s settled.
If you’re building agentic tools for Gemini Enterprise today, this is production-ready and worth adopting now. If you’re building a standalone agent product and picking a UI protocol cold, I’d prototype against the A2UI Composer this week before committing — the ecosystem is moving fast enough that “which protocol” is still an open question, even if “declarative-JSON-not-executable-code” clearly isn’t.
Go deeper
- A2UI.org — the spec, component catalog reference, and concept docs
- github.com/a2ui-project/a2ui — reference renderers (Lit, React, Angular, Flutter), server SDKs (Python, Kotlin), and the restaurant-finder sample end to end
- Introducing A2UI — Google Developers Blog — the original announcement
- A2UI v0.9: The New Standard for Portable, Framework-Agnostic Generative UI — what shipped in the production-focused v0.9 update
- Guide to Gemini Enterprise and A2UI integration — Google Cloud Blog — the enterprise integration path, step by step
- A2UI Composer — visual JSON editor, no install required
- A2UI Theater — streaming demo across Lit/React/Angular renderers
If you’re experimenting with A2UI or one of the competing agent-UI protocols, I want to hear about it — what worked, what fought you, what you’d change. Find me on X: @mikezupper.
The interface was never the hard part. Knowing what not to let the agent touch was.
