| name | a2ui-rich-ui |
| description | Use ONLY when the current turn is being delivered through Gemini Enterprise (a chat surface that natively renders A2UI cards/forms/dashboards). NEVER use when the current turn is happening in a terminal (Code OSS, SSH, or `claude --resume` from a shell prompt) — in a terminal the raw `<a2ui-json>` tags would be shown verbatim and ruin the reply. The same conversation can move between surfaces, so check EVERY turn, not just the first one; even if previous assistant turns contained `<a2ui-json>` blocks, drop them the moment the surface switches to a terminal. Within GE, prefer A2UI when a card, form, list, status board, confirmation, or approval prompt would communicate the answer better than plain markdown; skip it for short conversational replies that don't benefit from structure. |
Speak UI when it helps
You normally answer in markdown. When you have something inherently
structured to show — a summary card, a confirmation, a multi-field form,
a list of items the user might pick from, a side-by-side diff summary,
a progress dashboard — emit an A2UI payload alongside your text.
Gemini Enterprise renders A2UI v0.8 natively as Material-style components.
The wire format is fixed by the official A2UI Python parser
(agent_sdks/python/src/a2ui/parser/parser.py in google/A2UI), which
extracts JSON from <a2ui-json>...</a2ui-json> tags via a regex. The
runtime then packages the extracted JSON into an A2A DataPart with
MIME application/json+a2ui and ships it to the GE renderer.
If you decide to render UI:
- Keep the prose reply short — 1-2 sentences as a header. The UI carries
the detail. Conversational text can appear before, after, or between
<a2ui-json> blocks.
- Wrap the payload in
<a2ui-json>...</a2ui-json> tags (literal XML-style
tags, NOT a markdown code fence). The runtime detects these tags via
the regex <a2ui-json>(.*?)</a2ui-json> and the user never sees them.
- The body inside the tags is a single JSON array of messages
(NOT JSONL, NOT a bare object). Use the v0.8 message shape
(
beginRendering, surfaceUpdate, dataModelUpdate).
- Order rules:
- Put
beginRendering first in the array — this lets the streaming
parser start rendering as soon as the surface is declared.
- Within
surfaceUpdate.components, the root component MUST be the
first element, and every parent MUST appear before its children.
The streaming renderer relies on this top-down order.
- Pick a descriptive
surfaceId (e.g. "build-summary", "pr-review"),
not a generic "main" — surface IDs scope component state and a
collision could overwrite an earlier surface.
- When in doubt, reach for
Card + Column first, then add Text,
Image, Divider, Button, TextField, DateTimeInput, List as
needed.
Detecting the current surface
The runtime forces you to make this call yourself — the same persistent
session can carry over from GE chat to a terminal claude --resume and
back. Look at signals on the current turn before deciding:
You are in Gemini Enterprise when:
- The system prompt explicitly says you are operating inside GE.
- The user message reads like something a non-engineer would type in a
chat ("can you add inventory display to the product page").
- You see structured task IDs / context IDs that the runtime injected.
You are in a terminal when ANY of these is true:
- The user's prompt looks like a CLI command or a short imperative
("run the tests", "fix the lint error in main.py", "ls").
- They reference local files by path or use shell-ish syntax.
- They ask for diffs, command output, or stack traces verbatim.
- The runtime did not inject GE-specific system instructions.
When you detect the terminal surface, do not emit any <a2ui-json>
block this turn, even if the previous assistant turn (sent to GE)
contained one. Reply with plain markdown / code blocks. The same logic
applies in reverse: if the user moves back to GE, you may resume A2UI
replies even though the immediately preceding turns were terminal-style
text.
Other reasons to skip A2UI even in GE
- The reply is conversational ("yes", "got it", a one-line answer).
- You are mid-clarifying-question and the user needs to type a free-form
reply — keep it text so they can answer fast.
- You can't fit the message in the v0.8 catalog without inventing
components.
Output skeleton
Done — see the summary below.
<a2ui-json>
[
{"beginRendering": {"surfaceId": "build-summary", "root": "root"}},
{"surfaceUpdate": {"surfaceId": "build-summary", "components": [
{"id": "root", "component": {"Card": {"child": "col"}}},
{"id": "col", "component": {"Column": {"children": {"explicitList": ["title", "body"]}}}},
{"id": "title", "component": {"Text": {"usageHint": "h2", "text": {"literalString": "Build complete"}}}},
{"id": "body", "component": {"Text": {"text": {"literalString": "3 files changed, 5 tests added, all passing."}}}}
]}}
]
</a2ui-json>
That single block becomes a Material card in the GE chat.
What components exist
The standard v0.8 catalog defines exactly 18 components, listed below.
Do not invent new ones — the GE renderer will surface the placeholder
Unknown element <Name> for anything else (we have hit this in production
with Markdown, which does not exist in v0.8). If you need something not
here, fall back to Text + Column.
The full whitelist (= every component the standard catalog ships with):
Text, Image, Icon, Video, AudioPlayer, Row, Column, List,
Card, Tabs, Divider, Modal, Button, CheckBox, TextField,
DateTimeInput, MultipleChoice, Slider.
Layout
- Row — horizontal box.
children: {explicitList: [...]}, distribution, alignment
- Column — vertical box. Same props as Row.
- List — scrollable. Static
explicitList or dynamic template bound to data
- Card — single rounded container.
child: "id"
- Divider — horizontal rule.
{} is fine. Optional axis: "horizontal"|"vertical".
Display
- Text —
text: {literalString} | {path: "/data/key"}, usageHint: h1|h2|h3|h4|h5|caption|body. Supports simple Markdown inside the literal/path string (bold, italic, lists, inline code) — there is no separate Markdown component in v0.8, just put the markdown text inside Text.
- Image —
url: {literalString | path}, fit: cover|contain|fill, usageHint
- Icon —
name: {literalString} (Material icon name, e.g. "calendarToday", "check")
- Video —
url: {literalString | path}
- AudioPlayer —
url: {literalString | path}
Input
- TextField —
label, text (binding), type: text|number|email|password|multiline
- CheckBox —
label, value (binding to bool)
- DateTimeInput —
label, value (binding), enableDate, enableTime
- Slider —
value, min, max, step
- MultipleChoice —
label, value (binding), options: [...]
- Button —
child: "id-of-Text", primary: true|false, action: {name, context: [{key, value: {path: "..."}}]}
Container
- Modal — overlay container
- Tabs — tabbed container
Actions
Buttons can fire actions. The runtime forwards them as a follow-up user
message, then the router rewrites the message into a structured prompt
before it reaches you (so you see explicit "[A2UI action] The user
clicked X" framing rather than the raw action:X text).
{"id":"submit","component":{"Button":{
"child":"submit-label",
"primary":true,
"action":{
"name":"approve_pr",
"context":[{"key":"prNumber","value":{"path":"/prNumber"}}]
}
}}}
When the user clicks Submit, you receive an [A2UI action] prompt
naming the action and including its context. Carry it out and reply
with a short status line, optionally followed by a new card.
Naming actions
- Use
snake_case verb-first names: approve_pr, cancel,
delete_record, view_details, retry, open_file. The router
validates against ^[a-zA-Z_][\w-]*$.
- Reuse standard names across cards where possible —
cancel and
confirm are the canonical pair for confirmation dialogs.
- Include any data the handler needs in
context via path bindings.
Don't rely on the model-side data store still containing values from
earlier turns.
Destructive actions need a confirmation card
If the action mutates state outside this conversation — deleting data,
sending a message, calling an external API, costing money — surface a
confirmation card first instead of acting on the first click.
The pattern is two cards:
- The card that triggers the action emits a button with the
destructive action name (e.g.
drop_database).
- When you receive that action, reply with a confirmation card that
has two buttons:
cancel (no-op) and a new action name like
confirm_drop_database carrying the same context.
- Only when you receive
confirm_drop_database do you actually do
the work.
examples/confirmation.json shows the second card; the destructive
flow always lives across at least two turns.
Data binding
Every text/image/value field accepts either:
{"literalString": "Hi"} — hard-coded
{"path": "/some/key"} — pulled from the data model
Push data with dataModelUpdate (placed in the same JSON array, after
surfaceUpdate):
{"dataModelUpdate":{"surfaceId":"build-summary","path":"/","contents":[
{"key":"title","valueString":"Build complete"},
{"key":"changes","valueNumber":3}
]}}
This lets you update a value (e.g. progress %) by emitting a follow-up
<a2ui-json> block with just the dataModelUpdate, without re-emitting
the whole tree.
Patterns to copy
The examples/ directory next to this file contains ready-to-tweak
JSON arrays for the most common shapes. Wrap each in <a2ui-json>...</a2ui-json>
when emitting.
examples/build-summary.json — completion card with file count + test count
examples/confirmation.json — destructive-op confirmation with cancel/confirm
examples/clarifying-form.json — multi-field form when you need more info
examples/pr-list.json — list of PRs with action buttons
examples/progress-dashboard.json — multi-step pipeline status
Steal the structure and rewrite the literal strings for your case.
Hard rules
- Wrap the payload in
<a2ui-json>...</a2ui-json> XML-style tags. NOT
a ```a2ui fence — the official parser regex looks for the tags.
- The body is a single JSON array
[...] of messages. NOT JSONL, NOT
a bare object.
- Put
beginRendering first in the array. Within components, the root
component must be first and parents must precede their children.
- All component IDs must be unique within the surface.
- Components reference children by ID, not by nesting. Flat list only.
- Use a descriptive
surfaceId, not "main", to avoid collisions.
- If the runtime is not GE (e.g. terminal), DO NOT emit the tags — they
would be shown verbatim and ruin the reply.
References