Skip to content

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.

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 data payload is an object {"envelopes": [...]} wrapping an array of A2UI envelope messages, such as createSurface, updateComponents, and updateDataModel.
  • 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 Flutter genui.

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.

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.

Add genkit_a2ui alongside your core Genkit packages:

Terminal window
dart pub add genkit genkit_a2ui genkit_google_genai genkit_shelf shelf_router

In Genkit Dart, middleware is resolved from the registry, so you must register A2uiPlugin() in Genkit(plugins: [...]) before referencing a2ui(). Add a2ui() to the agent’s use list to enable generative UI with the default basic catalog:

import 'package:genkit/genkit.dart';
import 'package:genkit/experimental.dart';
import 'package:genkit_a2ui/a2ui.dart';
import 'package:genkit_google_genai/genkit_google_genai.dart';
import 'package:genkit_shelf/genkit_shelf.dart';
import 'package:shelf_router/shelf_router.dart';
// Register A2uiPlugin so `use: [a2ui()]` resolves from the registry
final ai = Genkit(plugins: [googleAI(), A2uiPlugin()]);
final uiAgent = ai.defineAgent(
name: 'uiAgent',
model: googleAI.gemini('gemini-flash-latest'),
system:
'You help users. Render an A2UI surface whenever a result is clearer '
'shown than told. Keep prose brief; put the primary substance in the UI.',
use: [a2ui()], // defaults to the bundled 'basic' catalog
store: InMemorySessionStore(),
);
// Serve the agent over HTTP using shelf
final app = Router();
app.post('/api/uiAgent', shelfHandler(uiAgent.action));
app.post(
'/api/uiAgent/getSnapshot',
shelfHandler(uiAgent.getSnapshotDataAction),
);
app.post('/api/uiAgent/abort', shelfHandler(uiAgent.abortAgentAction));

The middleware also works with one-shot ai.generate() calls:

final res = await ai.generate(
model: googleAI.gemini('gemini-flash-latest'),
prompt: 'Show me the weather in Tokyo',
use: [a2ui()],
);

Pass configuration options to a2ui(...):

OptionDefaultDescription
catalog'basic'Catalog ID resolved from the Genkit registry.
instructions'system'Where to inject catalog capabilities. Set to 'system' to append to the system prompt, or 'none' to omit.
validate'warn'Envelope validation strategy. 'warn' logs invalid envelopes and drops them; 'strict' throws errors on validation failure; 'off' passes everything through. See The trust boundary and security.
surfaceIda fresh UUIDSurface ID assignment policy. Defaults to a new UUID per surface; pass a fixed string to reuse a single surface.
version'v0.9'The A2UI protocol version stamped on emitted envelopes.

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.

Install the client dependencies along with the @genkit-ai/a2ui client helper:

Terminal window
npm install @a2ui/lit @a2ui/web_core @a2ui/markdown-it lit @lit/context @genkit-ai/a2ui genkit

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 elements
import { 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 styling
injectBasicCatalogStyles();
// Provide the markdown renderer to surface elements
new ContextProvider(document.body as any, {
context: Context.markdown,
initialValue: renderMarkdown,
});

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 catalog
const processor = new MessageProcessor([basicCatalog], (action) => {
handleAction(action as unknown as A2uiClientAction);
});
// Mount new surfaces when created
processor.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 message
async 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;
}

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.

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.

Input components (TextField, CheckBox, and Slider) do not broadcast values on every keystroke. To capture input upon submission:

  1. The input component binds its value to a data-model path (for example, { "path": "/email" }).
  2. The submit Button specifies those same data-model paths in its action.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 built-in basic catalog provides 12 core components across layout, content, and interactive categories:

  • Row: Lays out child components horizontally.
    • Props: children: string[] (required IDs), justify?: start|center|end|spaceAround|spaceBetween|spaceEvenly|stretch, align?: start|center|end|stretch.
  • Column: Lays out child components vertically.
    • Props: children: string[] (required IDs), justify?: start|center|end|spaceBetween|spaceAround|spaceEvenly|stretch, align?: start|center|end|stretch.
  • List: Displays a scrollable or sequential list of items.
    • Props: children: string[] (required IDs), direction?: vertical|horizontal, listStyle?: ordered|unordered|none.
  • Card: A styled card container with elevation and borders wrapping a single child.
    • Props: child: string (required ID of the child component; use a Column or Row to group multiple elements).
  • Divider: A visual separator line.
    • Props: axis?: horizontal|vertical.
  • Text: Displays plain text or inline Markdown.
    • Props: text: string (required), variant?: h1|h2|h3|h4|h5|caption|body.
  • Image: Displays a remote image.
    • Props: url: string (required), description?: string, fit?: contain|cover|fill|none|scaleDown, variant?: icon|avatar|smallFeature|mediumFeature|largeFeature|header.
  • Icon: Displays a standard Material symbol ligature.
    • Props: name: string (required). Must be one of the supported names, such as check, close, refresh, star, info, warning, error, search, home, or favorite.
  • Button: A clickable button that fires an action back to the agent.
    • Props: child: string (required child ID, typically a Text), variant?: default|primary|borderless, action: { event: { name: string, context?: object } } (required).
  • TextField: A single- or multi-line text input field.
    • Props: label: string (required), value?: string or { path } binding, variant?: shortText|longText|number|obscured.
  • CheckBox: A toggleable checkbox.
    • Props: label: string (required), value: boolean or { path } binding (required).
  • Slider: A numeric range slider.
    • Props: max: number (required), value: number or { path } binding (required), min?: number, step?: number, label?: string.

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 with name, description, and compact props documentation. props is model-facing guidance rather than strict JSON Schema, keeping injected prompt tokens minimal.

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."
}
]
}

Load a catalog from a JSON file with loadCatalog(ai, id: ..., file: ...) or construct an A2uiCatalog in memory:

import 'package:genkit_a2ui/a2ui.dart';
const dashboardCatalogId = 'https://example.com/catalogs/dashboard.json';
final dashboardCatalog = A2uiCatalog(
id: dashboardCatalogId,
components: [
...basicCatalog.components,
const A2uiCatalogComponent(
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.',
),
],
);
// Register at startup before handling turns
await loadCatalog(ai, id: dashboardCatalogId, catalog: dashboardCatalog);

Then configure your agent with catalog: dashboardCatalogId:

final dashboardAgent = ai.defineAgent(
name: 'dashboardAgent',
model: googleAI.gemini('gemini-flash-latest'),
use: [a2ui(catalog: dashboardCatalogId, validate: 'strict')],
store: InMemorySessionStore(),
);

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);
});

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 validate option (strict or warn) verifies envelope structure and component names against the active catalog. It does not sanitize property values (such as Image.url or Markdown text within Text).
  • 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-src and 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.

A2UI operates as a specialized data channel within the Genkit runtime:

  1. Prompt capability injection: The middleware augments the system prompt with the active catalog’s components and prop descriptions.
  2. Stream interception: As the model generates text, the middleware intercepts and parses a2ui fenced code blocks.
  3. Envelope translation: Emitted envelopes are validated against the catalog and packaged into Genkit data parts with MIME type application/a2ui+json.
  4. Action translation: Inbound user actions sent via actionToMessage() are converted into concise summaries for the model while preserving full structured payloads in conversation history.