A2UI Protocol Implementation Guide
This skill provides comprehensive knowledge for building AI agents that generate rich, adaptive user interfaces using the A2UI Protocol v0.8.
Reference: https://a2ui.org/specification/v0.8-a2ui/
Protocol Overview
A2UI is a JSONL-based streaming UI protocol enabling AI agents to generate declarative, interactive interfaces that render natively across platforms.
Key Design Principles
Principle Description
Security Declarative JSON, not executable code. Agents use pre-approved component catalogs.
LLM Compatibility Flat streaming JSON structure for incremental generation.
Framework Agnostic Same response renders across React, Flutter, Angular, native mobile.
Progressive Rendering UI streams incrementally for real-time updates.
Architecture Flow
User Input → Agent → A2UI Messages (JSONL Stream) → Client Renderer → Native UI ↓ User Interaction ↓ userAction → Agent
Core Concepts
- Surface
A Surface is a distinct, controllable region of the client's UI. Each surface has:
-
Unique surfaceId
-
Component hierarchy (adjacency list)
-
Data model (state container)
-
Root component reference
{ "surfaceId": "main", "components": { ... }, "dataModel": { ... }, "rootId": "root-component" }
- Adjacency List Model
Components are stored as a flat map where parent-child relationships use ID references, not nesting:
{ "components": [ { "id": "card-1", "component": { "Card": { "children": { "explicitList": ["card-1-content"] } } } }, { "id": "card-1-content", "component": { "Column": { "children": { "explicitList": ["card-1-title", "card-1-body"] } } } }, { "id": "card-1-title", "component": { "Text": { "text": {"literalString": "Hello World"}, "usageHint": "h2" } } } ] }
- BoundValue System
Properties that can be data-bound accept a BoundValue object:
Property Type Description
literalString
string Static string value
literalNumber
number Static number value
literalBoolean
boolean Static boolean value
path
string JSON Pointer to data model (e.g., /user/name )
Examples:
// Literal value only {"literalString": "Hello"}
// Data binding only {"path": "/user/name"}
// Combined (initialize and bind) { "literalString": "Default Name", "path": "/user/name" }
- Data Model
The data model stores dynamic state accessible via JSON Pointer paths:
{ "dataModel": { "contents": [ {"key": "user", "valueMap": [ {"key": "name", "valueString": "John"}, {"key": "age", "valueNumber": 30} ]}, {"key": "items", "valueList": [ {"valueString": "Item 1"}, {"valueString": "Item 2"} ]} ] } }
Value Types:
-
valueString : String values
-
valueNumber : Numeric values
-
valueBoolean : Boolean values
-
valueMap : Nested objects (array of key-value pairs)
-
valueList : Arrays
Message Types (Server → Client)
- surfaceUpdate
Delivers component definitions to a surface:
{ "surfaceUpdate": { "surfaceId": "main", "components": [ { "id": "title", "component": { "Text": { "text": {"literalString": "Welcome"}, "usageHint": "h1" } } } ] } }
- dataModelUpdate
Updates the surface's data model:
{ "dataModelUpdate": { "surfaceId": "main", "contents": [ {"key": "username", "valueString": "Alice"}, {"key": "count", "valueNumber": 42} ] } }
- beginRendering
Signals the client to render the surface:
{ "beginRendering": { "surfaceId": "main", "root": "root-component-id", "catalogId": "https://a2ui.org/specification/v0_8/standard_catalog_definition.json" } }
- deleteSurface
Removes a surface and its contents:
{ "deleteSurface": { "surfaceId": "secondary-panel" } }
Message Ordering
Required sequence:
-
surfaceUpdate (components)
-
dataModelUpdate (data)
-
beginRendering (trigger render)
Standard Component Catalog (v0.8)
Catalog ID: https://a2ui.org/specification/v0_8/standard_catalog_definition.json
Text
Displays text content with semantic hints:
{ "Text": { "text": {"literalString": "Hello World"}, "usageHint": "h1" } }
usageHint Values: h1 , h2 , h3 , body , caption
Image
Renders images from URLs:
{ "Image": { "url": {"literalString": "https://example.com/image.png"}, "alt": {"literalString": "Description"} } }
Button
Interactive button with action handling:
{ "Button": { "label": {"literalString": "Submit"}, "action": { "name": "submit_form", "context": [ {"key": "formId", "value": {"literalString": "contact-form"}} ] } } }
Action Properties:
-
name : Action identifier sent to server
-
context : Key-value pairs resolved against data model
Card
Container with single child:
{ "Card": { "title": {"literalString": "Card Title"}, "children": { "explicitList": ["card-content-id"] } } }
Row
Horizontal layout container:
{ "Row": { "children": { "explicitList": ["child-1", "child-2", "child-3"] }, "alignment": "center" } }
Alignment: start , center , end , spaceBetween , spaceAround
Column
Vertical layout container:
{ "Column": { "children": { "explicitList": ["child-1", "child-2"] }, "alignment": "start" } }
List
Dynamic list with template rendering:
{ "List": { "children": { "template": { "dataPath": "/items", "componentId": "item-template" } } } }
ListItem
Template for list items:
{ "ListItem": { "title": {"path": "/title"}, "subtitle": {"path": "/subtitle"}, "trailing": {"path": "/price"} } }
User Actions (Client → Server)
When users interact with components having action definitions:
{ "userAction": { "name": "submit_form", "surfaceId": "main", "sourceComponentId": "submit-button", "timestamp": "2024-01-01T12:00:00Z", "context": { "formId": "contact-form", "userName": "Alice" } } }
Fields:
-
name : Action identifier from component's action.name
-
surfaceId : Originating surface
-
sourceComponentId : Component that triggered the action
-
timestamp : ISO 8601 timestamp
-
context : Resolved action.context with all BoundValues evaluated
A2A Integration
A2UI integrates with A2A protocol via the extension URI: https://a2ui.org/a2a-extension/a2ui/v0.8
Agent Card Declaration
{ "capabilities": { "extensions": ["https://a2ui.org/a2a-extension/a2ui/v0.8"] }, "a2uiParams": { "supportedCatalogIds": [ "https://a2ui.org/specification/v0_8/standard_catalog_definition.json" ], "acceptsInlineCatalogs": false } }
A2A DataPart Format
A2UI messages travel as A2A DataPart objects:
{ "type": "data", "mimeType": "application/json+a2ui", "data": { "surfaceUpdate": { ... } } }
Implementation Guide
Python A2UI Builder
from typing import Any, Optional from dataclasses import dataclass, field import json
@dataclass class BoundValue: literal_string: Optional[str] = None literal_number: Optional[float] = None literal_boolean: Optional[bool] = None path: Optional[str] = None
def to_dict(self) -> dict:
result = {}
if self.literal_string is not None:
result["literalString"] = self.literal_string
if self.literal_number is not None:
result["literalNumber"] = self.literal_number
if self.literal_boolean is not None:
result["literalBoolean"] = self.literal_boolean
if self.path is not None:
result["path"] = self.path
return result
class A2UIBuilder: def init(self, surface_id: str = "main"): self.surface_id = surface_id self.components: list[dict] = [] self.data_model: list[dict] = []
# --- Component Methods ---
def text(
self,
id: str,
text: str | BoundValue,
usage_hint: str = "body"
) -> "A2UIBuilder":
text_val = (
text.to_dict() if isinstance(text, BoundValue)
else {"literalString": text}
)
self.components.append({
"id": id,
"component": {
"Text": {
"text": text_val,
"usageHint": usage_hint
}
}
})
return self
def image(
self,
id: str,
url: str | BoundValue,
alt: Optional[str] = None
) -> "A2UIBuilder":
url_val = (
url.to_dict() if isinstance(url, BoundValue)
else {"literalString": url}
)
img = {"url": url_val}
if alt:
img["alt"] = {"literalString": alt}
self.components.append({
"id": id,
"component": {"Image": img}
})
return self
def button(
self,
id: str,
label: str,
action_name: str,
context: Optional[dict] = None
) -> "A2UIBuilder":
action = {"name": action_name}
if context:
action["context"] = [
{"key": k, "value": {"literalString": str(v)}}
for k, v in context.items()
]
self.components.append({
"id": id,
"component": {
"Button": {
"label": {"literalString": label},
"action": action
}
}
})
return self
def card(
self,
id: str,
title: Optional[str] = None,
children: Optional[list[str]] = None
) -> "A2UIBuilder":
card = {}
if title:
card["title"] = {"literalString": title}
if children:
card["children"] = {"explicitList": children}
self.components.append({
"id": id,
"component": {"Card": card}
})
return self
def row(
self,
id: str,
children: list[str],
alignment: str = "start"
) -> "A2UIBuilder":
self.components.append({
"id": id,
"component": {
"Row": {
"children": {"explicitList": children},
"alignment": alignment
}
}
})
return self
def column(
self,
id: str,
children: list[str],
alignment: str = "start"
) -> "A2UIBuilder":
self.components.append({
"id": id,
"component": {
"Column": {
"children": {"explicitList": children},
"alignment": alignment
}
}
})
return self
def list_item(
self,
id: str,
title: str | BoundValue,
subtitle: Optional[str | BoundValue] = None,
trailing: Optional[str | BoundValue] = None
) -> "A2UIBuilder":
def to_bound(val):
if isinstance(val, BoundValue):
return val.to_dict()
return {"literalString": val}
item = {"title": to_bound(title)}
if subtitle:
item["subtitle"] = to_bound(subtitle)
if trailing:
item["trailing"] = to_bound(trailing)
self.components.append({
"id": id,
"component": {"ListItem": item}
})
return self
# --- Data Model Methods ---
def set_data(self, key: str, value: Any) -> "A2UIBuilder":
if isinstance(value, str):
self.data_model.append({"key": key, "valueString": value})
elif isinstance(value, bool):
self.data_model.append({"key": key, "valueBoolean": value})
elif isinstance(value, (int, float)):
self.data_model.append({"key": key, "valueNumber": value})
elif isinstance(value, list):
self.data_model.append({
"key": key,
"valueList": [self._convert_value(v) for v in value]
})
elif isinstance(value, dict):
self.data_model.append({
"key": key,
"valueMap": [
{"key": k, **self._convert_value(v)}
for k, v in value.items()
]
})
return self
def _convert_value(self, value: Any) -> dict:
if isinstance(value, str):
return {"valueString": value}
elif isinstance(value, bool):
return {"valueBoolean": value}
elif isinstance(value, (int, float)):
return {"valueNumber": value}
return {"valueString": str(value)}
# --- Build Methods ---
def build_surface_update(self) -> dict:
return {
"surfaceUpdate": {
"surfaceId": self.surface_id,
"components": self.components
}
}
def build_data_model_update(self) -> dict:
return {
"dataModelUpdate": {
"surfaceId": self.surface_id,
"contents": self.data_model
}
}
def build_begin_rendering(self, root_id: str) -> dict:
return {
"beginRendering": {
"surfaceId": self.surface_id,
"root": root_id,
"catalogId": "https://a2ui.org/specification/v0_8/standard_catalog_definition.json"
}
}
def build_all(self, root_id: str) -> list[dict]:
"""Returns all messages in correct order."""
messages = [self.build_surface_update()]
if self.data_model:
messages.append(self.build_data_model_update())
messages.append(self.build_begin_rendering(root_id))
return messages
Example: Travel Card Generator
def generate_travel_card(destination: dict) -> list[dict]: builder = A2UIBuilder(surface_id="main")
# Build component hierarchy
builder.card("card-root", title=destination["name"], children=["card-content"])
builder.column("card-content", children=["card-image", "card-info", "card-actions"])
builder.image("card-image", url=destination["image_url"], alt=destination["name"])
builder.column("card-info", children=["card-desc", "card-price"])
builder.text("card-desc", text=destination["description"], usage_hint="body")
builder.text("card-price", text=f"${destination['price']}", usage_hint="h3")
builder.row("card-actions", children=["btn-details", "btn-book"])
builder.button(
"btn-details",
label="View Details",
action_name="view_details",
context={"destinationId": destination["id"]}
)
builder.button(
"btn-book",
label="Book Now",
action_name="book_destination",
context={"destinationId": destination["id"]}
)
# Set data model
builder.set_data("destination", destination)
return builder.build_all(root_id="card-root")
SSE Streaming Example (FastAPI)
from fastapi import FastAPI from fastapi.responses import StreamingResponse from sse_starlette.sse import EventSourceResponse import json
app = FastAPI()
async def generate_a2ui_stream(user_message: str): builder = A2UIBuilder()
# Stream surfaceUpdate
builder.text("loading", "Processing your request...", "body")
yield {
"event": "message",
"data": json.dumps(builder.build_surface_update())
}
# Simulate processing...
result = await process_request(user_message)
# Stream updated components
builder = A2UIBuilder()
builder.text("result-title", "Results", "h2")
builder.text("result-content", result, "body")
yield {
"event": "message",
"data": json.dumps(builder.build_surface_update())
}
# Stream beginRendering
yield {
"event": "message",
"data": json.dumps(builder.build_begin_rendering("result-title"))
}
yield {"event": "done", "data": json.dumps({"status": "complete"})}
@app.post("/chat/stream") async def chat_stream(request: ChatRequest): return EventSourceResponse(generate_a2ui_stream(request.message))
TypeScript Renderer Types
// BoundValue resolution interface BoundValue { literalString?: string; literalNumber?: number; literalBoolean?: boolean; path?: string; }
// Component wrapper interface A2UIComponent { id: string; component: { Text?: TextComponent; Image?: ImageComponent; Button?: ButtonComponent; Card?: CardComponent; Row?: RowComponent; Column?: ColumnComponent; List?: ListComponent; ListItem?: ListItemComponent; }; }
// Surface state interface Surface { surfaceId: string; components: Map<string, A2UIComponent>; dataModel: Map<string, any>; rootId: string | null; isReady: boolean; }
// Message types interface SurfaceUpdate { surfaceUpdate: { surfaceId: string; components: A2UIComponent[]; }; }
interface DataModelUpdate { dataModelUpdate: { surfaceId: string; contents: DataModelContent[]; }; }
interface BeginRendering { beginRendering: { surfaceId: string; root: string; catalogId?: string; }; }
// User action interface UserAction { userAction: { name: string; surfaceId: string; sourceComponentId: string; timestamp: string; context: Record<string, any>; }; }
// Utility: Resolve BoundValue function resolveBoundValue( boundValue: BoundValue, dataModel: Map<string, any> ): any { if (boundValue.path) { return getValueAtPath(dataModel, boundValue.path); } return boundValue.literalString ?? boundValue.literalNumber ?? boundValue.literalBoolean; }
Best Practices
- Component ID Strategy
Use consistent, meaningful IDs with prefixes:
card-{entityId} card-{entityId}-title card-{entityId}-content btn-{action}-{entityId}
- Data Separation
Keep large data in the data model, reference via paths:
// Good: Data in model, bound in component {"text": {"path": "/article/content"}}
// Avoid: Large data inline {"text": {"literalString": "Very long text..."}}
- Streaming Order
Always send messages in order:
-
surfaceUpdate (structure)
-
dataModelUpdate (state)
-
beginRendering (render trigger)
- Progressive Updates
For long operations, send intermediate updates:
Initial loading state
yield surface_update(loading_component) yield begin_rendering("loading")
Final result
yield surface_update(result_components) yield data_model_update(result_data) yield begin_rendering("result-root")
- Action Context
Include all necessary data in action context:
{ "action": { "name": "add_to_cart", "context": [ {"key": "productId", "value": {"path": "/product/id"}}, {"key": "quantity", "value": {"literalNumber": 1}} ] } }
References
-
Official Site: https://a2ui.org/
-
v0.8 Specification: https://a2ui.org/specification/v0.8-a2ui/
-
A2A Extension: https://a2ui.org/specification/v0.8-a2a-extension/
-
GitHub Repository: https://github.com/google/A2UI
-
Standard Catalog: https://a2ui.org/specification/v0_8/standard_catalog_definition.json