Generative UI (A2UI)
A2UI (“Agent to UI”) is an open, transport-agnostic, JSON-based streaming UI protocol designed for agentic applications.
In standard conversational AI, agents communicate with users strictly through text or Markdown prose. With A2UI, an agent can stream rich, interactive UI surfaces—such as cards, lists, input forms, and buttons—that client applications render incrementally in real time as the model generates them.
How a surface travels
Section titled “How a surface travels”A surface rides on its own data part channel within the Genkit streaming response:
- The server middleware emits Genkit data parts carrying the MIME type
application/a2ui+json. - The part’s
datapayload is an object{"envelopes": [...]}wrapping an array of A2UI envelope messages, such ascreateSurface,updateComponents, andupdateDataModel. - This follows the A2A binding of the A2UI specification, so emitted envelopes are byte-compatible across the JavaScript, Go, and Dart plugins, and can be consumed by standard
@a2ui/*web renderers or Fluttergenui.
Because the wire protocol is completely decoupled from the server language, an agent written in Go, JavaScript/TypeScript, or Dart can stream to a web frontend or Flutter client without compatibility hurdles.
Server: Add the middleware
Section titled “Server: Add the middleware”To give an agent generative UI capabilities, attach the A2UI middleware to your agent or model pipeline. The middleware injects the active catalog’s capabilities into the prompt, intercepts streamed model outputs, extracts a2ui fenced code blocks, validates them against the catalog, and rewrites them into canonical A2UI data parts. Outside these blocks, standard prose passes through untouched.
Install the Go package
Section titled “Install the Go package”Add the A2UI plugin to your Go module:
go get github.com/firebase/genkit/go/plugins/a2ui/expThe package declares itself exp, so these examples import it as a2uix, the same convention as aix and genkitx.
Configure the agent
Section titled “Configure the agent”Attach &a2uix.Surfaces{} with ai.WithUse to an agent’s prompt or a generate call. When called without options, it defaults to the bundled basic catalog.
The basic-middleware/a2ui sample serves an agent configured with the middleware:
package main
import ( "context" "log" "net/http"
"github.com/firebase/genkit/go/ai" aix "github.com/firebase/genkit/go/ai/exp" "github.com/firebase/genkit/go/ai/exp/localstore" "github.com/firebase/genkit/go/genkit" genkitx "github.com/firebase/genkit/go/genkit/exp" a2uix "github.com/firebase/genkit/go/plugins/a2ui/exp" "github.com/firebase/genkit/go/plugins/googlegenai" "github.com/firebase/genkit/go/plugins/middleware")
func main() { ctx := context.Background() g := genkit.Init(ctx, genkit.WithExperimental(), genkit.WithPlugins(&googlegenai.GoogleAI{}), )
type WeatherInput struct { City string `json:"city"` }
type WeatherOutput struct { City string `json:"city"` TempC float64 `json:"tempC"` Condition string `json:"condition"` Humidity int `json:"humidity"` }
getWeather := genkitx.DefineTool(g, "getWeather", "Gets current weather conditions for a city.", func(ctx context.Context, in WeatherInput) (WeatherOutput, error) { return WeatherOutput{ City: in.City, TempC: 22, Condition: "Partly cloudy", Humidity: 55, }, nil }, )
uiAgent := genkitx.DefineAgent(g, "uiAgent", aix.InlinePrompt{ ai.WithModelName("googleai/gemini-flash-latest"), ai.WithSystem("You are a helpful assistant that can render rich UI. Prefer a surface whenever a result is clearer shown than told."), ai.WithTools(getWeather), ai.WithUse(&middleware.Retry{MaxRetries: 5}, &a2uix.Surfaces{}), }, aix.WithSessionStore(localstore.NewInMemorySessionStore[any]()), )
mux := http.NewServeMux() mux.Handle("/api/uiAgent", genkit.Handler(uiAgent)) mux.Handle("/api/uiAgent/getSnapshot", genkit.Handler(uiAgent.GetSnapshotAction())) mux.Handle("/api/uiAgent/abort", genkit.Handler(uiAgent.AbortAction()))
log.Println("Server running on http://localhost:8080") http.ListenAndServe(":8080", mux)}POST /api/uiAgent is the standard endpoint expected by client applications, with getSnapshot and abort at the sub-paths derived by the client. See Serve agents over HTTP for endpoint routing and CORS setup.
You can also attach the middleware to a standalone genkit.Generate call:
resp, err := genkit.Generate(ctx, g, ai.WithModelName("googleai/gemini-flash-latest"), ai.WithSystem("You help users. Render UI when it is clearer than prose."), ai.WithPrompt("Show me the weather in Tokyo."), ai.WithUse(&a2uix.Surfaces{}),)if err != nil { return err}
for _, envelope := range a2uix.EnvelopesFromParts(resp.Message.Content) { log.Println(envelope)}The middleware maintains streaming turn state. When pairing with middleware that re-invokes the model (such as Retry or Fallback), place them outside Surfaces: ai.WithUse(&middleware.Retry{}, &a2uix.Surfaces{}) ensures each retry attempt gets a fresh A2UI turn.
a2uix.EnvelopesFromParts extracts envelopes from any message or chunk content, while a2uix.IsPart reports whether a given part carries A2UI data.
Options
Section titled “Options”Every field of a2uix.Surfaces is per-call configuration:
| Field | Default | Description |
|---|---|---|
Catalog | nil | An inline catalog for code-defined use. Overrides CatalogID when set. Not serialized for JSON-dispatched calls; prefer CatalogID. |
CatalogID | "basic" | The ID of a catalog registered with LoadCatalog, resolved from the registry on each call. |
Instructions | "system" | Where the catalog’s capabilities are injected. Set to "none" to omit prompt injection when managing instructions manually. |
Validate | "warn" | How malformed envelopes are handled: "warn" logs and drops the block; "strict" fails the call; "off" passes everything through. See The trust boundary and security. |
SurfaceID | a fresh UUID | A fixed surface ID to reuse for every surface, for a client that maintains a single live surface. |
Version | "v0.9" | The protocol version stamped on envelopes. Must be one of a2uix.SupportedVersions. |
Register as a plugin
Section titled “Register as a plugin”Passing &a2uix.Surfaces{} directly to ai.WithUse does not require registering a plugin. However, registering &a2uix.A2UI{} makes the middleware discoverable by name in the Developer UI and in .prompt files (use: [a2ui]):
g := genkit.Init(ctx, genkit.WithExperimental(), genkit.WithPlugins(&googlegenai.GoogleAI{}, &a2uix.A2UI{}),)Client: Render surfaces
Section titled “Client: Render surfaces”Because A2UI emits standardized JSON envelopes over HTTP, client-side rendering is completely decoupled from your backend language. A web frontend or Flutter client can interact seamlessly with a backend written in TypeScript, Go, or Dart.
Web clients use @a2ui/web_core and an A2UI renderer. A2UI provides official renderers for Web Components/Lit (@a2ui/lit), React (@a2ui/react), and Angular (@a2ui/angular).
The examples below use the Lit renderer.
1. Install client packages
Section titled “1. Install client packages”Install the client dependencies along with the @genkit-ai/a2ui client helper:
npm install @a2ui/lit @a2ui/web_core @a2ui/markdown-it lit @lit/context @genkit-ai/a2ui genkitpnpm add @a2ui/lit @a2ui/web_core @a2ui/markdown-it lit @lit/context @genkit-ai/a2ui genkityarn add @a2ui/lit @a2ui/web_core @a2ui/markdown-it lit @lit/context @genkit-ai/a2ui genkitbun add @a2ui/lit @a2ui/web_core @a2ui/markdown-it lit @lit/context @genkit-ai/a2ui genkit2. Add client font styles
Section titled “2. Add client font styles”The basic catalog’s Icon component renders icon names as ligatures using the Material Symbols Outlined font. Include the stylesheet in your web app’s HTML <head> so icons render visually:
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0..1,0"/>3. Initialize client styles and markdown rendering
Section titled “3. Initialize client styles and markdown rendering”Initialize @a2ui/web_core styles and provide the Markdown renderer context on the document body so all <a2ui-surface> elements inherit formatting:
import { Context, basicCatalog } from '@a2ui/lit/v0_9';import '@a2ui/lit/v0_9'; // Registers <a2ui-surface> and basic catalog custom elementsimport { renderMarkdown } from '@a2ui/markdown-it';import { MessageProcessor } from '@a2ui/web_core/v0_9';import { injectBasicCatalogStyles } from '@a2ui/web_core/v0_9/basic_catalog';import { ContextProvider } from '@lit/context';
// Inject catalog stylinginjectBasicCatalogStyles();
// Provide the markdown renderer to surface elementsnew ContextProvider(document.body as any, { context: Context.markdown, initialValue: renderMarkdown,});4. Stream and process agent turns
Section titled “4. Stream and process agent turns”Connect to your backend endpoint using remoteAgent() from genkit/beta/client. Iterate over turn.stream, appending prose deltas to your chat view and feeding extracted A2UI envelopes into the MessageProcessor:
import { MessageProcessor } from '@a2ui/web_core/v0_9';import { basicCatalog } from '@a2ui/lit/v0_9';import { remoteAgent } from 'genkit/beta/client';import { a2uiEnvelopesFromParts, actionToMessage, type A2uiClientAction,} from '@genkit-ai/a2ui/client';
const agent = remoteAgent({ url: '/api/uiAgent' });const chat = agent.chat();
// Set up the message processor with the basic catalogconst processor = new MessageProcessor([basicCatalog], (action) => { handleAction(action as unknown as A2uiClientAction);});
// Mount new surfaces when createdprocessor.onSurfaceCreated((surface) => { const container = document.getElementById('chat-log')!; const surfaceEl = document.createElement('a2ui-surface') as any; surfaceEl.surface = surface; container.appendChild(surfaceEl);});
// Stream a user messageasync function sendMessage(text: string) { const turn = chat.sendStream(text);
for await (const chunk of turn.stream) { // 1. Render prose text deltas if (chunk.text) { appendProseText(chunk.text); }
// 2. Extract and process A2UI envelopes from raw data parts const envelopes = a2uiEnvelopesFromParts(chunk.raw.modelChunk?.content); if (envelopes.length > 0) { processor.processMessages(envelopes); } }
await turn.response;}Stream using the lightweight helper
Section titled “Stream using the lightweight helper”If you do not require full session management with remoteAgent, @genkit-ai/a2ui/client also provides the streamA2uiAgent async generator:
import { streamA2uiAgent } from '@genkit-ai/a2ui/client';
for await (const event of streamA2uiAgent({ url: '/api/uiAgent', message: 'What is the weather in Tokyo?',})) { if (event.type === 'text') { appendProseText(event.text); } else if (event.type === 'envelopes') { processor.processMessages(event.envelopes); }}streamA2uiAgent accepts sessionId, headers, and abortSignal in its configuration object.
Flutter applications render A2UI surfaces using genui. Client components use package:genkit/client.dart, package:genkit_a2ui/client.dart, and package:a2ui_core/a2ui_core.dart.
1. Install client packages
Section titled “1. Install client packages”Add the client packages to your Flutter app:
flutter pub add genkit genkit_a2ui genui a2ui_core2. Set up the SurfaceController and remoteAgent
Section titled “2. Set up the SurfaceController and remoteAgent”package:genkit_a2ui/client.dart is browser- and Flutter-safe (no dart:io). Initialize remoteAgent, construct a SurfaceController with the basic catalog, and stream agent turns:
import 'package:a2ui_core/a2ui_core.dart' as core;import 'package:flutter/material.dart';import 'package:genkit/client.dart';import 'package:genkit_a2ui/client.dart';import 'package:genui/genui.dart' hide basicCatalogId, DataPart;
// remoteAgent connects to your backend endpointfinal agent = remoteAgent( url: 'http://localhost:8080/api/uiAgent', getSnapshotUrl: 'http://localhost:8080/api/uiAgent/getSnapshot', abortUrl: 'http://localhost:8080/api/uiAgent/abort',);final chat = agent.chat();
// Re-tag genui's basic catalog with the plugin's advertised basicCatalogIdfinal catalog = BasicCatalogItems.asCatalog().copyWith( catalogId: basicCatalogId,);final surfaceController = SurfaceController(catalogs: [catalog]);3. Stream and process agent turns
Section titled “3. Stream and process agent turns”Iterate over turn.stream, parsing A2UI envelopes from the chunk’s content using a2uiEnvelopesFromParts, and pass each envelope as an A2uiMessage to surfaceController.handleMessage:
final turn = chat.sendStream(text: 'What is the weather in Tokyo?');
await for (final chunk in turn.stream) { // 1. Append prose text if (chunk.text.isNotEmpty) { appendProse(chunk.text); }
// 2. Extract and handle A2UI envelopes for (final envelope in a2uiEnvelopesFromParts(chunk.raw.modelChunk?.content)) { surfaceController.handleMessage(core.A2uiMessage.fromJson(envelope)); }}
await turn.response;4. Mount the Surface widget
Section titled “4. Mount the Surface widget”Listen to surfaceController.surfaceUpdates to detect new surfaces, and render Surface(surfaceContext: surfaceController.contextFor(surfaceId)) in your UI:
surfaceController.surfaceUpdates.listen((update) { if (update is SurfaceAdded) { setState(() { entries.add(update.surfaceId); }); }});
// Inside your build method or ListView:Widget buildSurface(String surfaceId) { return IntrinsicHeight( child: Surface( surfaceContext: surfaceController.contextFor(surfaceId), ), );}Wrap Surface in IntrinsicHeight when placed inside scrollable views such as ListView to provide bounded constraints for components that stretch vertically.
Handle user actions and forms
Section titled “Handle user actions and forms”When users interact with components (such as clicking a Button), the surface triggers an action that is sent back to the agent as the next conversational turn.
Use actionToMessage() to wrap the client action into an AgentInput message and send it as the next conversational turn:
import { actionToMessage, a2uiEnvelopesFromParts, type A2uiClientAction,} from '@genkit-ai/a2ui/client';
async function handleAction(action: A2uiClientAction) { // Send the action payload as the next turn in the conversation const turn = chat.sendStream({ message: actionToMessage(action), });
for await (const chunk of turn.stream) { if (chunk.text) appendProseText(chunk.text); const envelopes = a2uiEnvelopesFromParts(chunk.raw.modelChunk?.content); if (envelopes.length > 0) processor.processMessages(envelopes); }
await turn.response;}actionToMessage puts the action’s name in the user message text so models without custom prompt handling understand the action, and attaches the complete structured action data (including bound form context) as an A2UI data part. The server middleware sanitizes inbound action parts into concise text summaries for the model.
In Flutter, listen to surfaceController.onSubmit. Genui emits a ChatMessage containing a UiInteractionPart, which you decode into an A2uiClientAction and convert with actionToMessage:
import 'dart:convert';import 'package:genkit_a2ui/client.dart';import 'package:genui/genui.dart' hide basicCatalogId, DataPart;
surfaceController.onSubmit.listen((ChatMessage message) { final action = _actionFromSubmit(message); if (action == null || busy) return;
final turn = chat.sendStream(message: actionToMessage(action)); // Stream prose and envelopes as usual...});
A2uiClientAction? _actionFromSubmit(ChatMessage message) { for (final part in message.parts) { final interaction = part.asUiInteractionPart?.interaction; if (interaction == null) continue; final decoded = jsonDecode(interaction); final action = decoded is Map ? decoded['action'] : null; if (action is Map) { final m = action.cast<String, dynamic>(); return A2uiClientAction( name: (m['name'] as String?) ?? 'action', surfaceId: (m['surfaceId'] as String?) ?? '', sourceComponentId: (m['widgetId'] as String?) ?? '', timestamp: DateTime.now().toUtc().toIso8601String(), context: (m['context'] as Map?)?.cast<String, dynamic>() ?? const {}, ); } } return null;}Form inputs and data binding
Section titled “Form inputs and data binding”Input components (TextField, CheckBox, and Slider) do not broadcast values on every keystroke. To capture input upon submission:
- The input component binds its
valueto a data-model path (for example,{ "path": "/email" }). - The submit
Buttonspecifies those same data-model paths in itsaction.event.context.
The instructions injected by the A2UI middleware guide the model to configure these bindings. When the user clicks submit, the client renderer resolves the bound paths from the surface data model and passes the values in action.context.
The basic component catalog
Section titled “The basic component catalog”The built-in basic catalog provides 12 core components across layout, content, and interactive categories:
Layout components
Section titled “Layout components”Row: Lays out child components horizontally.- Props:
children: string[](required IDs),justify?: start|center|end|spaceAround|spaceBetween|spaceEvenly|stretch,align?: start|center|end|stretch.
- Props:
Column: Lays out child components vertically.- Props:
children: string[](required IDs),justify?: start|center|end|spaceBetween|spaceAround|spaceEvenly|stretch,align?: start|center|end|stretch.
- Props:
List: Displays a scrollable or sequential list of items.- Props:
children: string[](required IDs),direction?: vertical|horizontal,listStyle?: ordered|unordered|none.
- Props:
Card: A styled card container with elevation and borders wrapping a single child.- Props:
child: string(required ID of the child component; use aColumnorRowto group multiple elements).
- Props:
Divider: A visual separator line.- Props:
axis?: horizontal|vertical.
- Props:
Content components
Section titled “Content components”Text: Displays plain text or inline Markdown.- Props:
text: string(required),variant?: h1|h2|h3|h4|h5|caption|body.
- Props:
Image: Displays a remote image.- Props:
url: string(required),description?: string,fit?: contain|cover|fill|none|scaleDown,variant?: icon|avatar|smallFeature|mediumFeature|largeFeature|header.
- Props:
Icon: Displays a standard Material symbol ligature.- Props:
name: string(required). Must be one of the supported names, such ascheck,close,refresh,star,info,warning,error,search,home, orfavorite.
- Props:
Interactive components
Section titled “Interactive components”Button: A clickable button that fires an action back to the agent.- Props:
child: string(required child ID, typically aText),variant?: default|primary|borderless,action: { event: { name: string, context?: object } }(required).
- Props:
TextField: A single- or multi-line text input field.- Props:
label: string(required),value?: string or { path } binding,variant?: shortText|longText|number|obscured.
- Props:
CheckBox: A toggleable checkbox.- Props:
label: string(required),value: boolean or { path } binding(required).
- Props:
Slider: A numeric range slider.- Props:
max: number(required),value: number or { path } binding(required),min?: number,step?: number,label?: string.
- Props:
Custom catalogs
Section titled “Custom catalogs”When you want agents to render custom UI widgets or components tailored to your design system, you can register a custom catalog. A catalog defines:
id: A globally unique URI for the catalog (matching the client-side renderer).components: An array of component definitions withname,description, and compactpropsdocumentation.propsis model-facing guidance rather than strict JSON Schema, keeping injected prompt tokens minimal.
Catalog JSON definition
Section titled “Catalog JSON definition”Define your catalog in a JSON file (such as ./catalogs/dashboard.json):
{ "id": "https://example.com/catalogs/dashboard.json", "components": [ { "name": "MetricCard", "description": "Displays a key metric with a title, numeric value, and change indicator.", "props": "title: string (required); value: string|number (required); trend?: up|down|neutral; percentage?: number." }, { "name": "Text", "description": "Displays plain or inline-markdown text.", "props": "text: string (required); variant?: body|caption." } ]}Register the catalog on the server
Section titled “Register the catalog on the server”Load a catalog from a JSON file with LoadCatalogFile, or construct a Catalog struct in memory and register it with LoadCatalog:
myCatalog := &a2uix.Catalog{ ID: "https://example.com/catalogs/dashboard.json", Components: []a2uix.CatalogComponent{ { Name: "MetricCard", Description: "Displays a key metric with a title, numeric value, and trend indicator.", Props: "title: string (required); value: string|number (required); trend?: up|down|neutral.", }, },}if err := a2uix.LoadCatalog(g, myCatalog); err != nil { return err}Or from a file:
catalog, err := a2uix.LoadCatalogFile(g, "./catalogs/dashboard.json")if err != nil { return err}LoadCatalogFile returns the parsed catalog, so catalog.ID is the key to reference it by.
Reference the registered catalog by ID in a2uix.Surfaces:
resp, err := genkit.Generate(ctx, g, ai.WithModelName("googleai/gemini-flash-latest"), ai.WithPrompt("Show dashboard metrics."), ai.WithUse(&a2uix.Surfaces{CatalogID: "https://example.com/catalogs/dashboard.json"}),)Catalogs live in the Genkit registry under a2ui-catalog. The Go plugin keys registrations by the catalog’s own ID, while JavaScript keys by a user-specified lookup key. The underlying wire protocol and catalog JSON schemas are identical.
Register matching widgets on the client
Section titled “Register matching widgets on the client”The client application must register a matching catalog renderer under the exact same catalog ID and support the corresponding component names:
Create a custom component renderer and supply it alongside basicCatalog to the MessageProcessor:
import { MessageProcessor } from '@a2ui/web_core/v0_9';import { basicCatalog } from '@a2ui/lit/v0_9';
const customCatalog = { id: 'https://example.com/catalogs/dashboard.json', components: { // Custom web component renderers mapped to component names MetricCard: metricCardRenderer, },};
const processor = new MessageProcessor([basicCatalog, customCatalog], (action) => { handleAction(action);});In Flutter, implement the component as a genui CatalogItem and add it to the catalog with copyWith:
import 'package:genui/genui.dart' hide basicCatalogId, DataPart;
final metricCardItem = CatalogItem( name: 'MetricCard', // Must match the server component name dataSchema: metricCardSchema, widgetBuilder: (itemContext) { return MetricCardWidget(context: itemContext); },);
final customCatalog = BasicCatalogItems.asCatalog().copyWith( newItems: [metricCardItem], catalogId: 'https://example.com/catalogs/dashboard.json', // Must match server ID);
final surfaceController = SurfaceController(catalogs: [customCatalog]);The trust boundary and security
Section titled “The trust boundary and security”Because generative UI renders model-generated structures in the client DOM or Flutter widget tree, treat every emitted surface as untrusted output:
- Validation checks structure, not values: The
validateoption (strictorwarn) verifies envelope structure and component names against the active catalog. It does not sanitize property values (such asImage.urlor Markdown text withinText). - Sanitize in the client renderer: The client renderer is responsible for sanitizing property values before mounting them into the DOM or widget tree. Markdown parsers must escape raw HTML tags unless intentionally permitted and sanitized.
- Enforce Content Security Policy (CSP): For web applications, configure a strong CSP restricting
img-srcand fetch destinations to trusted domains to prevent remote code execution or data exfiltration. - Protect secrets: Do not place confidential tokens or sensitive IDs in the surface data model, as any bound data may be returned to the server in user action payloads.
Under the hood
Section titled “Under the hood”A2UI operates as a specialized data channel within the Genkit runtime:
- Prompt capability injection: The middleware augments the system prompt with the active catalog’s components and prop descriptions.
- Stream interception: As the model generates text, the middleware intercepts and parses
a2uifenced code blocks. - Envelope translation: Emitted envelopes are validated against the catalog and packaged into Genkit
dataparts with MIME typeapplication/a2ui+json. - Action translation: Inbound user actions sent via
actionToMessage()are converted into concise summaries for the model while preserving full structured payloads in conversation history.
Next steps
Section titled “Next steps”-
Serve agents over HTTP covers endpoint routing and the browser client.
-
Middleware covers composition order and built-in middleware to pair with
Surfaces. -
Run and stream agents covers reading a turn’s chunks, where A2UI data parts arrive alongside text.
-
Explore the
basic-middleware/a2uisample for a complete Go backend. -
A2UI specification provides the full protocol and catalog specification.