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 server plugin
Section titled “Install the server plugin”Install @genkit-ai/a2ui along with your core Genkit packages:
npm install @genkit-ai/a2ui genkit @genkit-ai/google-genai @genkit-ai/expresspnpm add @genkit-ai/a2ui genkit @genkit-ai/google-genai @genkit-ai/expressyarn add @genkit-ai/a2ui genkit @genkit-ai/google-genai @genkit-ai/expressbun add @genkit-ai/a2ui genkit @genkit-ai/google-genai @genkit-ai/expressConfigure the agent
Section titled “Configure the agent”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 UIconst 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 prosebrief and place the primary information in the UI components.`, tools: [getWeather], use: [a2ui()], store: new InMemorySessionStore(),});
// Serve the agent over HTTPconst 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()],});Options
Section titled “Options”The a2ui() middleware accepts the following options:
| Option | Default | Description |
|---|---|---|
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. |
surfaceId | undefined | Surface 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. |
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/a2uiConfigure the agent
Section titled “Configure the agent”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.
Options
Section titled “Options”Every field of a2ui.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 a2ui.SupportedVersions. |
Register as a plugin
Section titled “Register as a plugin”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{}),)Install the server package
Section titled “Install the server package”Add genkit_a2ui alongside your core Genkit packages:
dart pub add genkit genkit_a2ui genkit_google_genai genkit_shelf shelf_routerConfigure the agent
Section titled “Configure the agent”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 registryfinal 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 shelffinal 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()],);Options
Section titled “Options”Pass configuration options to a2ui(...):
| Option | Default | Description |
|---|---|---|
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. |
surfaceId | a fresh UUID | Surface 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. |
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 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 turnsawait 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(),);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 Express setup and client connectivity in detail.
- Sessions and state explains session stores, history, and client state.
- Define agents covers agent definitions, tools, and configurations.
- Explore the
a2uitestapp for a complete runnable sample with Express and Lit.
- 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.
- Deploying agents with Shelf covers server endpoints and client connectivity.
- Sessions and state covers Dart session stores and history.
- Define agents covers agent definitions and tools in Dart.
- Explore the
a2uitestapp for a complete runnable Flutter + Shelf sample.
- A2UI specification provides the full protocol and catalog specification.