Export to A2UI

Exporting Polyxd UI documents to A2UI v1.0 messages with a custom Polyxd catalog, what is lossy, and why Polyxd keeps its own schema.

A2UI (Agent-to-User Interface) is an open protocol for agents to send UI to renderers as streamed JSON messages. @polyxd/a2ui exports any Polyxd UI document to A2UI v1.0 messages, so Polyxd surfaces can travel over A2UI transports (A2A, AG-UI, MCP) and, eventually, be drawn by A2UI renderers.

The target is v1.0 as a release candidate: the official schemas at a2ui-project/a2ui commit 04e6f07 (2026-09-18), vendored unmodified in packages/a2ui/vendor/.

Exporting#

import { exportToA2UI, createValidator, checkComponentTree } from "@polyxd/a2ui";

// One createSurface message with components and dataModel inline
const { messages, lossy, idMap } = exportToA2UI(doc);

// Streamed: createSurface, then updateComponents, then updateDataModel
const streamed = exportToA2UI(doc, { mode: "stream" });

// Options
exportToA2UI(doc, { surfaceId: "send-7f3a", extensions: false, sendDataModel: true });

// Validate against the official schemas
const v = createValidator();
v.message(messages[0]);          // [] when valid against agent_to_renderer.json
checkComponentTree(components);  // root exists, ids unique, references resolve

lossy lists JSON Pointers into the Polyxd document for every field A2UI can't represent. idMap lists renamed ids.

How the projection works#

Polyxd's document shape was designed to mirror A2UI where A2UI has an equivalent, so export is a projection rather than a translation:

Polyxd A2UI
surface.id createSurface.surfaceId
data dataModel (or updateDataModel in stream mode)
Flat components list with ids The same adjacency list. The root component is renamed root, which is how A2UI mounts a surface; every reference is remapped
Collection.items {path, componentId} A2UI templated ChildList, with the same relative-path scoping
{ "path": ... } bindings Copied as they are; A2UI's DataBinding has the same shape
action.event {name, context} Copied as the A2UI agent action
ui.dismiss, ui.back, ui.next A2UI local actions: {functionCall: {call: "dismiss" | "back" | "next"}}
accessibility {label, description, live, hidden} Copied into A2UI's accessibility block

Polyxd-only metadata goes under metadata.extensions.com_polyxd, on the surface (specVersion, title, intent, pattern, journey, dismissible, the original root id) and on each component (key). Pass extensions: false to leave it out.

A custom catalog#

A2UI's Basic catalog is explicitly optional, and the protocol encourages custom catalogs mapped to a design system's own components. Polyxd uses one.

The rule was: a component maps to a Basic component only if every Polyxd prop has a Basic equivalent with the same meaning. None of the 24 does. For example, Basic ChoicePicker needs literal options (Polyxd options can come from data), Basic Image takes a URL (Polyxd media is a host-data reference), and Basic has no chart, table, stepper or comparison. So all 24 are custom components in the Polyxd catalog, with the same names and props.

The catalog is packages/a2ui/catalog/catalog.json (catalogId https://polyxd.com/catalog/0.1/a2ui, protocolVersion 1.0). It is generated from the spec's component files, and its instructions carry each component's usage, rendering, accessibility and agent guidance for LLMs. Each entry records its nearest Basic analog.

What is lossy#

These fields have no A2UI representation. They are carried in the extension metadata (which A2UI renderers must ignore) and reported in lossy:

Polyxd field Why
specVersion A2UI versions messages, not documents
surface.title createSurface has no title
surface.intent, surface.pattern, surface.journey A2UI has no task, pattern or journey semantics
surface.dismissible No surface-level dismiss flag
Component key A2UI ids are surface-local; keys are for interface memory

Two things are dropped outright: $schema, and the context of a ui.* action (A2UI local function calls take no context). Everything else, including formats, visible, validation, options from data, tones and nested item keys, survives as props of the custom components.

Status#

MCP Apps (planned)#

MCP Apps is the MCP extension for interactive UIs, delivered as a ui:// HTML resource in a sandboxed iframe. The planned @polyxd/mcp server would ship one prebuilt @polyxd/react bundle as that resource, pass the generated UI document as tool input, and map Polyxd's semantic tokens onto the CSS variables MCP hosts provide, so the surface inherits the host's theme. The generated content stays data; only Polyxd's renderer is code. None of this is built yet.

Why Polyxd keeps its own schema#

Decision 0001 (docs/decisions/0001-a2ui-and-foundations.md) chose to own the schema and export to A2UI, rather than extend A2UI's catalog directly:

  1. Stability. A2UI is pre-1.0 and has just made a breaking v0.9 to v1.0 change. Owning the schema lets Polyxd version with specVersion and semver, while the exporter absorbs A2UI churn. Training data and the constrained-decoding grammar don't have to be regenerated each time A2UI changes.
  2. Tokens. A2UI deliberately has no design tokens. Polyxd's components need emphasis and tone semantics tied to its semantic tokens, and its verifier checks token-only values.
  3. Product semantics. Design Direction, capability risk levels, journeys and analytics events are outside A2UI's scope. As first-class schema they can be validated and used in constrained decoding.
  4. Memory. Consistency across generations needs stable semantic keys and pattern ids, which A2UI's surface-local ids don't provide.
  5. Agent semantics. A2UI's accessibility block is a minimum. Polyxd needs roles, task and done semantics, and recoverable state in the core schema.

Reach comes through a faithful export instead: A2UI's renderers (Lit, Angular, React, Flutter) and transports become available without making A2UI the internal model.

Polyxd is an early preview. Found something unclear? It will get better with your feedback.