Skip to content

How to Integrate A2UI into a Client

Integrate an A2UI renderer into a Flutter, React, Lit, Angular, SwiftUI, or Jetpack Compose application using the Renderer SDKs to display interactive UIs from an AI agent and send user actions back.

For the agent side, see How to Integrate A2UI into an Agent. If your agent and renderer live in the same binary or codebase, combine both guides, skip the transport step, and pass messages directly from agent output into the rendering pipeline (the Agent and Renderer SDKs share the same Core SDK data structures). To register custom widgets beyond the built-in Basic catalog, see How to Define a Catalog and Authoring Universal Web Components.


Step 1: Install dependencies

Install the Renderer SDK and Core SDK packages for your target UI framework:

Terminal window
flutter pub add a2ui_flutter a2ui_core url_launcher

Step 2: Initialize MessageProcessor

Create a MessageProcessor instance inside a session controller or service with the built-in Basic catalog and an action callback that forwards user events to the transport method you will add in Step 4.

The scope of the rendering session determines which surfaces the agent can modify. In a chat app, you can use one MessageProcessor per conversation to allow the agent to update or delete any surface in the conversation at any time by its surfaceId, or use one MessageProcessor per chat turn so the agent only modifies surfaces in the active turn and does not need unique surface IDs across turns. To register custom components or local functions alongside the Basic catalog, see How to Define a Catalog.

import 'dart:convert';
import 'package:a2ui_core/a2ui_core.dart';
import 'package:a2ui_flutter/a2ui_flutter.dart';
import 'package:a2ui_flutter/basic_catalog.dart';
import 'package:url_launcher/url_launcher.dart';
class AgentSession {
AgentSession({required this.sendOverTransport}) {
processor = MessageProcessor<ComponentImplementation>(
catalogs: [
basicCatalog(
openUrl: (url) async {
await launchUrl(url);
},
),
// Add custom domain catalogs here (see /guides/defining-a-catalog/).
],
onAction: (action) {
// Optional (default: null)
sendToAgent(ActionMessage(version: '1.0', action: action).toJson());
},
);
}
final void Function(Map<String, Object?> message) sendOverTransport;
late final MessageProcessor<ComponentImplementation> processor;
}

Step 3: Mount surface views

Create a container view that subscribes to the active surfaces on MessageProcessor and renders an A2UI surface component for each one. Your container mounts new surfaces when createSurface arrives and unmounts them when deleteSurface arrives, while each mounted surface observes its own component tree and DataModel internally to update child components in place as updateComponents and updateDataModel messages arrive.

import 'package:a2ui_core/a2ui_core.dart';
import 'package:a2ui_flutter/a2ui_flutter.dart';
import 'package:flutter/material.dart';
class AgentSurfacesView extends StatefulWidget {
const AgentSurfacesView({super.key, required this.processor});
final MessageProcessor<ComponentImplementation> processor;
@override
State<AgentSurfacesView> createState() => _AgentSurfacesViewState();
}
class _AgentSurfacesViewState extends State<AgentSurfacesView> {
late final Subscription _createdSub;
late final Subscription _deletedSub;
@override
void initState() {
super.initState();
_createdSub = widget.processor.model.onSurfaceCreated.subscribe((_) {
setState(() {});
});
_deletedSub = widget.processor.model.onSurfaceDeleted.subscribe((_) {
setState(() {});
});
}
@override
void dispose() {
_createdSub.unsubscribe();
_deletedSub.unsubscribe();
super.dispose();
}
@override
Widget build(BuildContext context) {
final surfaces = widget.processor.model.allSurfaces;
return ListView(
children: [
for (final surface in surfaces)
KeyedSubtree(
key: ValueKey(surface.id),
child: A2uiSurface(surface: surface),
),
],
);
}
}

Step 4: Connect transport and handle actions

Add inbound and outbound transport methods to your session controller or service to decode incoming JSON payloads into MessageProcessor and send resolved action messages back to your agent. A2UI is transport-agnostic, so these methods can exchange messages over HTTP responses, Server-Sent Events (SSE), WebSockets, A2A, or MCP.

void onTransportChunk(String rawJson) {
final Object? decoded = jsonDecode(rawJson);
processor.processMessages(decoded);
}
void sendToAgent(Map<String, Object?> wireMessage) {
sendOverTransport(wireMessage);
}
Wire format: agent-to-renderer messages

Agents send createSurface followed by incremental updateComponents and updateDataModel messages, and remove finished surfaces with deleteSurface:

[
{
"version": "1.0",
"createSurface": {
"surfaceId": "order_summary",
"catalogId": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json"
}
},
{
"version": "1.0",
"updateComponents": {
"surfaceId": "order_summary",
"components": [
{"id": "root", "component": "Card", "child": "col"},
{"id": "col", "component": "Column", "children": ["headline", "confirm_btn"]},
{"id": "headline", "component": "Text", "text": {"@path": "/summaryText"}},
{
"id": "confirm_btn",
"component": "Button",
"child": "confirm_label",
"action": {
"event": {"name": "confirm_order", "context": {"orderId": {"@path": "/orderId"}}}
}
},
{"id": "confirm_label", "component": "Text", "text": "Confirm order"}
]
}
},
{
"version": "1.0",
"updateDataModel": {
"surfaceId": "order_summary",
"path": "/",
"value": {"orderId": "ORD-4821", "summaryText": "2 items ready for pickup"}
}
}
]
Wire format: outbound action message

When the user clicks Confirm order, MessageProcessor resolves context bindings against the surface DataModel and passes the action message to sendToAgent:

{
"version": "1.0",
"action": {
"name": "confirm_order",
"surfaceId": "order_summary",
"sourceComponentId": "confirm_btn",
"timestamp": "2026-05-01T18:30:00.000Z",
"context": {
"orderId": "ORD-4821"
}
}
}

Step 5: Wire everything at the app entry point

Instantiate your session controller or service at the application entry point and pass its MessageProcessor to the surface container view from Step 3. Sharing this session connects user prompts and bidirectional agent messages to the rendered surfaces:

import 'package:flutter/material.dart';
import 'agent_session.dart';
import 'agent_surfaces_view.dart';
void main() {
late final AgentSession session;
session = AgentSession(
sendOverTransport: (message) {
// POST {'message': message} to your agent endpoint and pass each incoming
// JSON payload back to session.onTransportChunk(chunk).
},
);
runApp(
MaterialApp(
home: Scaffold(
body: AgentSurfacesView(processor: session.processor),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Start an agent turn and pass each incoming JSON payload to
// session.onTransportChunk(chunk).
},
child: const Icon(Icons.send),
),
),
),
);
}

Additional steps

Customize Basic Catalog theme tokens on Web

Override the --a2ui-* CSS custom properties on :root or a container element to match your application’s theme, or apply the .a2ui-dark and .a2ui-light classes to lock a color scheme. Calling injectBasicCatalogStyles() in Step 2 adopts the default stylesheet into document.adoptedStyleSheets (and no-ops during server-side rendering):

:root {
--a2ui-color-primary: #0f766e;
--a2ui-color-on-primary: #ffffff;
--a2ui-color-background: #ffffff;
--a2ui-color-surface: #f8fafc;
--a2ui-color-on-background: #0f172a;
--a2ui-font-family-title: 'Plus Jakarta Sans', system-ui, sans-serif;
font-family: 'Plus Jakarta Sans', system-ui, sans-serif;
}

In React, setMarkdownRenderer(renderMarkdown) from Step 2 configures the built-in Basic catalog Text elements; if your custom React catalog components call useMarkdownRenderer(), also wrap your tree in <MarkdownContext.Provider value={renderMarkdown}> from @a2ui/react.

Call getRendererCapabilities() on MessageProcessor and attach the result to your outbound transport metadata under a2uiRendererCapabilities so the agent knows which catalogs and protocol versions your client supports. If your team owns both the agent and the client app with a fixed catalog set, you can skip runtime negotiation and target those catalogs directly on the agent. Keep includeInlineCatalogs at its default false in production when the agent already holds matching catalogId schemas; set it to true only during prototyping when the agent accepts client-supplied inline schemas.

Map<String, Object?> buildCapabilitiesMetadata() {
final capabilities = processor
.getRendererCapabilities(
const CapabilitiesOptions(
versions: [A2uiProtocolVersion.v1_0],
includeInlineCatalogs: false, // Optional (default: false)
),
)
.toJson();
return {
'a2uiRendererCapabilities': capabilities,
};
}
Wire format: renderer capabilities metadata

buildCapabilitiesMetadata() produces the transport metadata envelope below ("1.0" in v1.0, "v0.9" in v0.9.1):

{
"metadata": {
"a2uiRendererCapabilities": {
"1.0": {
"supportedCatalogIds": ["https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json"]
}
}
}
}

Synchronize client data models and forward RPCs

Call getRendererDataModel() on MessageProcessor and attach the returned snapshot under a2uiRendererDataModel in your outbound transport metadata alongside action messages and user prompts. MessageProcessor returns data models for active surfaces created with "sendDataModel": true, such as multi-field forms where local input edits update the client DataModel without firing separate action events.

If your catalogs use v1.0 remote agent functions (callAgentFunction) or agent-initiated client calls (callRendererFunction), pass outboundListener to MessageProcessor (processorOptions.outboundListener in Angular) to forward those envelopes through sendToAgent. To report malformed incoming payloads back to the agent as structured error messages instead of throwing exceptions, set reportErrorsOnly: true (paired with errorHandler on Web, or outboundListener on Flutter; SwiftUI and Jetpack Compose already emit validation errors on outboundListener and outboundEvents).

Map<String, Object?> buildDataModelMetadata() {
final dataModel = processor.getRendererDataModel();
return {
if (dataModel != null) 'a2uiRendererDataModel': dataModel,
};
}
Wire format: outbound action with data-model metadata

When the user triggers an action on a "sendDataModel": true surface, attaching buildDataModelMetadata() alongside message produces the transport envelope below:

{
"message": {
"version": "1.0",
"action": {
"name": "confirm_order",
"surfaceId": "order_summary",
"sourceComponentId": "confirm_btn",
"timestamp": "2026-05-01T18:30:00.000Z",
"context": {
"orderId": "ORD-4821"
}
}
},
"metadata": {
"a2uiRendererDataModel": {
"version": "1.0",
"surfaces": {
"order_summary": {
"orderId": "ORD-4821",
"summaryText": "2 items ready for pickup"
}
}
}
}
}