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.

Install @genkit-ai/a2ui along with your core Genkit packages:

Terminal window
npm install @genkit-ai/a2ui genkit @genkit-ai/google-genai @genkit-ai/express

Pass a2ui() in the agent’s use array. When configured without options, the agent defaults to the bundled basic catalog, exposing 12 core layout, content, and interactive components.

import { genkit, z, InMemorySessionStore } from 'genkit/beta';
import { googleAI } from '@genkit-ai/google-genai';
import { a2ui } from '@genkit-ai/a2ui';
import { expressHandler } from '@genkit-ai/express';
import express from 'express';
const ai = genkit({
plugins: [googleAI()],
});
// A sample tool the model can call to fetch data before generating UI
const getWeather = ai.defineTool(
{
name: 'getWeather',
description: 'Gets current weather conditions for a city.',
inputSchema: z.object({ city: z.string() }),
outputSchema: z.object({
city: z.string(),
tempC: z.number(),
condition: z.string(),
humidity: z.number(),
}),
},
async ({ city }) => {
return {
city,
tempC: 22,
condition: 'Partly cloudy',
humidity: 55,
};
},
);
export const uiAgent = ai.defineAgent({
name: 'uiAgent',
model: googleAI.model('gemini-flash-latest'),
system: `You are an interactive assistant that can render rich UI surfaces.
Prefer rendering an A2UI surface whenever a visual display is clearer than plain prose,
such as weather forecasts, comparisons, lists, forms, or interactive cards. Keep prose
brief and place the primary information in the UI components.`,
tools: [getWeather],
use: [a2ui()],
store: new InMemorySessionStore(),
});
// Serve the agent over HTTP
const app = express();
app.use(express.json());
app.post('/api/uiAgent', expressHandler(uiAgent));
app.listen(8080, () => {
console.log('Server running on http://localhost:8080');
});

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

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

The a2ui() middleware accepts the following options:

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' disables envelope checking.
surfaceIdundefinedSurface ID assignment policy. Defaults to generating a fresh UUID per surface. Provide a fixed string to reuse a single surface.
version'v0.9'The A2UI protocol version stamped on emitted envelopes.

Add the A2UI plugin to your Go module:

Terminal window
go get github.com/firebase/genkit/go/plugins/a2ui

Attach &a2ui.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"
"github.com/firebase/genkit/go/plugins/a2ui"
"github.com/firebase/genkit/go/plugins/googlegenai"
"github.com/firebase/genkit/go/plugins/middleware"
)
func main() {
ctx := context.Background()
g, err := genkit.Init(ctx,
genkit.WithExperimental(),
genkit.WithPlugins(&googlegenai.GoogleAI{}),
)
if err != nil {
log.Fatal(err)
}
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}, &a2ui.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(&a2ui.Surfaces{}),
)
if err != nil {
return err
}
for _, envelope := range a2ui.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{}, &a2ui.Surfaces{}) ensures each retry attempt gets a fresh A2UI turn.

a2ui.EnvelopesFromParts extracts envelopes from any message or chunk content, while a2ui.IsPart reports whether a given part carries A2UI data.

Every field of a2ui.Surfaces is per-call configuration:

FieldDefaultDescription
CatalognilAn 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.
SurfaceIDa fresh UUIDA 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 a2ui.SupportedVersions.

Passing &a2ui.Surfaces{} directly to ai.WithUse does not require registering a plugin. However, registering &a2ui.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{}, &a2ui.A2UI{}),
)

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_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 the catalog file using loadCatalog:

import { loadCatalog } from '@genkit-ai/a2ui';
await loadCatalog(ai, {
id: 'dashboard',
file: './catalogs/dashboard.json',
});

You can also define catalogs directly in memory, extending basicCatalog:

import { loadCatalog, basicCatalog, type A2uiCatalog } from '@genkit-ai/a2ui';
const dashboardCatalog: A2uiCatalog = {
id: 'https://example.com/catalogs/dashboard.json',
components: [
...basicCatalog.components,
{
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.',
},
],
};
await loadCatalog(ai, {
id: 'dashboard',
catalog: dashboardCatalog,
});

To use it, pass the registered catalog ID to a2ui():

export const dashboardAgent = ai.defineAgent({
name: 'dashboardAgent',
model: googleAI.model('gemini-flash-latest'),
system: 'You generate executive dashboards using MetricCards and structured layouts.',
use: [
a2ui({
catalog: 'dashboard',
validate: 'strict',
}),
],
});

Load a catalog from a JSON file with LoadCatalogFile, or construct a Catalog struct in memory and register it with LoadCatalog:

myCatalog := &a2ui.Catalog{
ID: "https://example.com/catalogs/dashboard.json",
Components: []a2ui.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 := a2ui.LoadCatalog(g, myCatalog); err != nil {
return err
}

Or from a file:

if err := a2ui.LoadCatalogFile(g, "./catalogs/dashboard.json"); err != nil {
return err
}

Reference the registered catalog by ID in a2ui.Surfaces:

resp, err := genkit.Generate(ctx, g,
ai.WithModelName("googleai/gemini-flash-latest"),
ai.WithPrompt("Show dashboard metrics."),
ai.WithUse(&a2ui.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.

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.