DocsRenderA2UIA2UI Overview

A2UI Overview

A2UI is an open standard for agent-driven user interfaces. It lets an AI agent describe a structured UI — components, layout, and live data — using a simple JSON protocol, and have that UI rendered automatically inside a chat session.

When an agent response begins with the ---a2ui_JSON--- sentinel, the chat switches to A2UI rendering mode automatically — provided you supply a component catalog via the views input.

Spec version

The implementation follows the A2UI v0.9 specification. For the full protocol reference, see a2ui.org.

How It Works in ChatComponent

When content arrives with the ---a2ui_JSON--- prefix, the streaming pipeline switches to JSONL mode. Each newline-delimited JSON object is parsed as an A2UI message and handed to the surface store.

AI response starts with ---a2ui_JSON---
  → ContentClassifier sets type = 'a2ui'
  → createA2uiMessageParser() parses JSONL line-by-line
  → Each A2uiMessage is applied to A2uiSurfaceStore
  → Surfaces signal updates reactively
  → A2uiSurfaceComponent renders each surface via json-render

The surface store maintains a Map<string, A2uiSurface> keyed by surface ID. Each surface holds a flat component map, a data model, theme metadata, and the catalog ID used to resolve components.

To render A2UI surfaces, pass a ViewRegistry to the ChatComponent via the views input. The a2uiBasicCatalog() function provides the 18 built-in components.

The Four Message Types

The A2UI protocol is built on four message types. Agents compose entire interfaces by sending sequences of these messages.

MessagePurpose
createSurfaceCreates a new named surface with a catalog ID and optional theme
updateComponentsAdds or replaces components on a surface (merged by component ID)
updateDataModelSets or patches a value in the surface data model via JSON Pointer
deleteSurfaceRemoves a surface and all its components

createSurface

Declares a new surface. Must be sent before any other message for that surface ID.

{"createSurface": {"surfaceId": "order-status", "catalogId": "basic"}}

updateComponents

Sends one or more component definitions. Each component has a unique id, a component type name, and any component-specific props.

{"updateComponents": {
  "surfaceId": "order-status",
  "components": [
    {"id": "root", "component": "Column", "children": ["title", "status"]},
    {"id": "title", "component": "Text", "text": "Your Order"},
    {"id": "status", "component": "Text", "text": {"path": "/state"}}
  ]
}}

updateDataModel

Sets values at a JSON Pointer path in the surface's data model. Components whose props reference these paths re-render automatically.

{"updateDataModel": {"surfaceId": "order-status", "path": "/state", "value": "Shipped"}}

Omit path (or use /) to replace the entire data model at once.

deleteSurface

Removes a surface from the store and dismisses its rendered output.

{"deleteSurface": {"surfaceId": "order-status"}}

Action-Event Bridge

A2UI actions (button clicks, form events) now map directly to render-spec on bindings. When surfaceToSpec() converts a surface to a spec, each component's action prop is translated into an on binding on the corresponding spec element. This means A2UI actions flow through the render-lib event system rather than being handled ad-hoc by individual components.

The bridge provides two default handlers:

  • a2ui:event -- dispatches named events (e.g., submit, cancel) back to the agent
  • a2ui:localAction -- executes local function calls (e.g., openUrl)

This unified event flow means consumers can observe all A2UI interactions through the RenderEvent stream on RenderSpecComponent, including handler execution, state changes, and lifecycle signals. See the Render Events page for the full event type reference.

A2UI vs json-render

Both A2UI and json-render produce rendered UIs inside chat messages, but they serve different purposes.

A2UIjson-render
ProtocolMulti-message JSONL streamSingle JSON object
StateSurfaces with live data modelsStateless spec
InteractivityTwo-way bindings, button actions, validationRead-only display
Detection---a2ui_JSON--- prefixFirst character is {
Use caseAgent-driven dynamic UIsRich content cards

Use A2UI when the agent needs to build an interface incrementally, bind it to live data, or respond to user interactions. Use json-render for rich one-shot content cards — formatted results, structured data displays, and similar read-only output.

Quick Setup

Pass a2uiBasicCatalog() to the views input to enable A2UI rendering:

import { ChatComponent, a2uiBasicCatalog } from '@ngaf/chat';
 
@Component({
  template: `<chat [agent]="agentRef" [views]="catalog" />`,
  imports: [ChatComponent],
})
export class AppComponent {
  catalog = a2uiBasicCatalog();
  agentRef = agent({ /* your agent options */ });
}

When the AI response starts with ---a2ui_JSON---, the chat renders the surfaces using the provided catalog.

Custom catalogs

To extend or replace the built-in components, compose catalogs with withViews() or views(). See the Custom Catalogs guide for details.

Automatic Event Routing

When an A2UI button fires an a2ui:event action, ChatComponent automatically routes it back to the agent as a human message. No manual (renderEvent) wiring is needed for standard A2UI event flows.

The event is sent as a JSON-encoded human message:

{
  "type": "a2ui_event",
  "surfaceId": "contact",
  "name": "formSubmit",
  "context": { "formId": "contact" }
}

The renderEvent output still fires for all events, so consumers can observe or log events without intercepting the routing.

Minimal Consumer Setup

import { ChatComponent, a2uiBasicCatalog } from '@ngaf/chat';
import { agent } from '@ngaf/langgraph';
 
@Component({
  template: `<chat [agent]="agentRef" [views]="catalog" />`,
})
export class MyComponent {
  agentRef = agent({ apiUrl: '/api', assistantId: 'my-agent' });
  catalog = a2uiBasicCatalog();
}

Custom Function Call Handlers

When an A2UI button has a functionCall action, the call name is looked up in the [handlers] map on ChatComponent. This lets you define client-side behavior triggered by agent-built UI:

@Component({
  template: `<chat [agent]="agentRef" [views]="catalog" [handlers]="handlers" />`,
})
export class MyComponent {
  agentRef = agent({ apiUrl: '/api', assistantId: 'my-agent' });
  catalog = a2uiBasicCatalog();
 
  handlers = {
    addToCart: async (args: Record<string, unknown>) => {
      const cart = inject(CartService);
      return cart.add(args['sku'] as string);
    },
  };
}

The agent sends a button with {"action": {"functionCall": {"call": "addToCart", "args": {"sku": "ABC"}}}}. When clicked, the addToCart handler runs in Angular's injection context — inject() works for accessing services.

If no consumer handler matches the call name, built-in handlers are used as fallbacks (e.g., openUrl opens a URL in a new tab).

Handler return values are emitted on the RenderHandlerEvent — observe them via the renderEvent output on ChatComponent.

Validation

A2UI v0.9 uses CheckRule objects for client-side validation. Input components and buttons can define a checks array — each check has a condition (a DynamicBoolean) and an error message.

CheckRule Shape

{
  "checks": [
    {
      "condition": { "call": "required", "args": { "value": { "path": "/name" } } },
      "message": "Name is required"
    }
  ]
}

The condition can be:

  • A boolean literal: true or false
  • A path reference: { "path": "/agreed" } — resolves to a data model value
  • A FunctionCall: { "call": "required", "args": { ... } } — invokes a named function
  • A composite: { "call": "and", "args": { "values": [...] } } — combines multiple conditions

Built-in Functions

CategoryFunctions
Validationrequired, regex, length, numeric, email
Logicand, or, not
FormattingformatString, formatNumber, formatCurrency, formatDate, pluralize
NavigationopenUrl

Input Component Behavior

Input components (TextField, CheckBox, ChoicePicker, Slider, DateTimeInput) validate continuously — errors display inline as the user interacts. The input border changes color to indicate validation state.

Button Behavior

Per the v0.9 spec: if any check fails, the button is automatically disabled. Error messages display below the button.

Composite Conditions

Use and, or, and not to compose complex validation rules:

{
  "condition": {
    "call": "and",
    "args": {
      "values": [
        { "call": "required", "args": { "value": { "path": "/name" } } },
        { "call": "or", "args": { "values": [
          { "call": "required", "args": { "value": { "path": "/email" } } },
          { "call": "required", "args": { "value": { "path": "/phone" } } }
        ]}}
      ]
    }
  },
  "message": "Name required, plus email or phone"
}

Custom Catalog Components

Custom catalog components receive a pre-computed validationResult prop:

interface A2uiValidationResult {
  valid: boolean;
  errors: string[];
}

Use the shared A2uiValidationErrorsComponent for consistent error display:

import { A2uiValidationErrorsComponent } from '@ngaf/chat';
 
@Component({
  imports: [A2uiValidationErrorsComponent],
  template: `
    <input [value]="value()" />
    <a2ui-validation-errors [result]="validationResult()" />
  `,
})
export class MyCustomInputComponent {
  readonly value = input('');
  readonly validationResult = input<A2uiValidationResult>({ valid: true, errors: [] });
}

Theming

Validation styling uses CSS custom properties:

PropertyDefaultUsage
--a2ui-error#ef4444Error text and invalid border color
--a2ui-borderrgba(255,255,255,0.1)Default input border
--a2ui-input-bgrgba(255,255,255,0.05)Input background
--a2ui-labelrgba(255,255,255,0.6)Label text color

Events & Data Model Transport

When a user triggers an event action (e.g., clicking a button with action.event), the Angular renderer builds a v0.9-compliant action message and sends it back to the agent. Local actions (action.functionCall) execute client-side only — the agent never sees them.

Action Message Shape

The outbound message follows the v0.9 spec:

{
  "version": "v0.9",
  "action": {
    "name": "formSubmit",
    "surfaceId": "contact",
    "sourceComponentId": "submit-btn",
    "timestamp": "2026-04-10T14:30:00.000Z",
    "context": {
      "name": "Alice",
      "email": "alice@example.com"
    }
  }
}
FieldDescription
versionAlways "v0.9"
action.nameThe event name from the component's action.event.name
action.surfaceIdThe surface that owns this component
action.sourceComponentIdThe id of the component that triggered the event
action.timestampISO 8601 timestamp of when the action was dispatched
action.contextResolved values from action.event.context — path refs and function calls are evaluated against the current data model

Context Resolution

Context values in action.event.context are DynamicValues — they can be literals, path references, or function calls. They are resolved at dispatch time against the current data model:

{
  "action": {
    "event": {
      "name": "formSubmit",
      "context": {
        "name": {"path": "/name"},
        "email": {"path": "/email"},
        "total": {"call": "formatCurrency", "args": {"value": {"path": "/amount"}}}
      }
    }
  }
}

When the user clicks the button, the renderer resolves /name and /email from the data model and calls formatCurrency on /amount, producing a flat context object with concrete values.

sendDataModel

Set sendDataModel: true on createSurface to attach the full data model snapshot to every outbound action:

{"type": "createSurface", "surfaceId": "contact", "catalogId": "basic", "sendDataModel": true}

When enabled, the action message includes a metadata field:

{
  "version": "v0.9",
  "action": { "..." : "..." },
  "metadata": {
    "a2uiClientDataModel": {
      "version": "v0.9",
      "surfaces": {
        "contact": {
          "name": "Alice",
          "email": "alice@example.com",
          "department": "Engineering"
        }
      }
    }
  }
}

The data model is only sent with event actions — there are no passive change notifications on input changes. This matches the v0.9 spec requirement that the data model piggybacks on outbound messages.

Angular Integration

A2uiSurfaceComponent exposes two outputs:

OutputTypeDescription
(action)A2uiActionMessageAgent-bound action messages — the complete v0.9 envelope
(events)RenderEventAll render events (state changes, handler calls, lifecycle) for observation

ChatComponent auto-routes (action) events to the agent as human messages. For standalone usage, bind (action) directly:

<a2ui-surface
  [surface]="surface()"
  [catalog]="catalog"
  (action)="sendToAgent($event)"
  (events)="logEvent($event)"
/>

Data Model Bindings

When the agent sets component properties using path references ({ "path": "/name" }), the surface component tracks these as bindings — a mapping from prop name to JSON Pointer path. These bindings are passed to catalog components as the _bindings prop.

How Bindings Work

  1. Agent sends components with path references: { "value": { "path": "/form/name" } }
  2. surfaceToSpec resolves the path to a current value AND records the binding in _bindings
  3. Catalog component reads the resolved value normally. When the user changes the value, it emits an a2ui:datamodel event via the emit callback
  4. The event format is a2ui:datamodel:{path}:{value}

Using emitBinding

Custom catalog components can use the emitBinding utility for consistent binding emission:

import { emitBinding } from '@ngaf/chat';
 
// In your component's change handler:
onInput(event: Event): void {
  const val = (event.target as HTMLInputElement).value;
  emitBinding(this.emit(), this._bindings(), 'value', val);
}

Known Limitations

The current binding mechanism is client-side only — the a2ui:datamodel events are emitted but do not yet flow through the render lib's StateStore. Data model updates from user input are not reflected back to other components in real time. Full StateStore integration is planned for a future release.

Data model state is refreshed when the agent sends an updateDataModel message.

What's Next