Skip to content

A2UI Express technical specification

A2UI Express is a compact, model-optimized declarative syntax designed for dynamic generative user interfaces. It acts as an intermediate, highly compressed representation that on-device large language models generate to describe user interfaces. A host-side compiler parses this syntax and compiles it into standard A2UI v1.0 wire protocol payloads.

Core design goals

The design of A2UI Express focuses on four main requirements:

  • Token footprint reduction. Generative models spend excessive output tokens when producing verbose JSON structures. A2UI Express removes structural keys, brackets, and repeated quotes, reducing output tokens by 55% to 70% compared to native A2UI wire payloads.
  • On-device model optimization. Small local models, such as Gemma 4 E2B and E4B, operate with limited context windows and reasoning budgets. The syntax uses clean positional signatures that fit into prompt contracts without consuming excessive context.
  • Streaming compatibility. The line-oriented grammar allows the client host to parse and build the component hierarchy line-by-line, enabling progressive rendering of the interface before the model finishes its output.
  • Protocol alignment. A2UI Express preserves full semantic compatibility with standard A2UI v1.0, supporting data bindings, client validation rules, and local event handling.

Syntax and grammar

A2UI Express layout blocks must be enclosed inside the <a2ui> and </a2ui> sentinel tags to cleanly separate the user interface DSL from any accompanying conversational text:

<a2ui>
variable_name = ComponentName(arg1, arg2, param=value)
</a2ui>

Every instruction in A2UI Express is a variable assignment statement or standalone lifecycle command. Statements are separated by newlines, and a single assignment can span multiple lines:

variable_name = ComponentName(arg1, arg2, param=value)

The syntax rules define how types, structures, data paths, and validation actions are expressed.

Variable declarations and component nesting

Every component definition is assigned to a unique variable identifier, or constructed inline directly within a parent component's argument list. A reserved variable named root acts as the primary entry point for the interface tree.

To support multi-lingual models while ensuring syntax safety for future extensions (such as math or conditional expressions), all variable identifiers MUST conform to the Unicode Identifier standard (UAX #31), matching the Catalog Entity Naming Rules:

  • The first character must be in the Unicode property class XID_Start or an underscore (_, U+005F).
  • Subsequent characters must be in the Unicode property class XID_Continue.
  • In canonical regular-expression syntax: ^[\p{XID_Start}_][\p{XID_Continue}]*$.
  • Reserved tokens (true, false, null, _) cannot be used as variable identifiers, and root is reserved for the surface root component.

A2UI Express supports flexible hybrid component nesting: child components can either be defined inline (e.g., Card(child=Text("Hello"))) or assigned to top-level variables and referenced by name (e.g., header = Text("Hello") and root = Card(child=header)).

Component IDs

By default, a component's Express variable name becomes its A2UI component "id" in the compiled JSON (for example, root = Column([grid]) compiles to a component with "id": "root").

In protocol versions before v1.0, a component "id" can be any string (such as "content-grid", which contains a hyphen -), whereas an Express variable name must be a UAX #31 identifier so - is not mistaken for subtraction. To produce an A2UI component "id" that differs from its Express variable name (such as a pre-v1.0 hyphenated ID, or a reserved word like "true"), assign the component to a valid Express variable name (such as grid), reference that variable in parent child lists, and pass the target A2UI ID via the reserved id= keyword argument:

root = Column([grid])
grid = Row([left, right], id="content-grid")

Here root compiles with "id": "root" and "children": ["content-grid"], while grid compiles with "id": "content-grid". The id= keyword argument works the same way on an inline component, and every catalog accepts it. For protocol versions before v1.0, when the decompiler encounters a component ID like "content-grid", it derives a valid variable name (content_grid) and emits id="content-grid". From protocol v1.0 onward, component IDs must themselves be valid UAX #31 identifiers, so the decompiler rejects non-UAX #31 component IDs rather than deriving a variable name (reserved words such as "true" are valid UAX #31 identifiers and continue to be derived as _true with id="true").

When updating an existing surface and referencing a child component ID that was created in an earlier turn (and is not defined in the current block), write the ID as a bare identifier if it is a valid variable name, or as a quoted string if it contains characters like hyphens:

surface("dashboard-surface-1")
main_column = Column([earlier_header, "earlier-body"])

Argument passing (positional and keyword)

Component constructors support both positional arguments and keyword argument assignments (param=value):

  • Positional Arguments: Passed in the order defined by the component signature (e.g., Text("Hello")). Optional positional arguments at the end of a signature may be omitted. If an optional argument in the middle is omitted, a placeholder underscore _ can be passed.
  • Keyword Arguments: Passed explicitly by parameter name (e.g., Button(child=Text("Submit"), action=Event("click"))). Keyword arguments can be freely mixed with positional arguments.
  • Static Parameter Annotations: Signature parameters annotated as (static) MUST be defined as literal values or arrays inline, and cannot use dynamic data binding paths ($/path).

Core primitive types

The syntax supports three literal primitive types:

  • Strings are represented in two formats:
  • Standard Strings: Enclosed in single double quotes (e.g., "Enter your name") or triple double quotes (e.g., """Line 1\nLine 2"""). Standard strings support common escape sequences: \n (newline), \t (tab), \\ (backslash), and \" (double quote). Embedded newlines are allowed.
  • Raw Strings: Prefaced by r (e.g., r"^[a-zA-Z]+$" or r"""Raw multi-line content"""). In raw strings, no escape sequences are processed, and backslashes are interpreted as literal characters. This is particularly useful for validation regex patterns containing backslashes. Embedded newlines are allowed.
  • Numbers are JSON numbers with an optional leading +, for example 42, 3.14, -1, .5, 1e-7 or 2.5E+3. The grammar is [+-]? ([0-9]+ ('.' [0-9]*)? | '.' [0-9]+) ([eE] [+-]? [0-9]+)?, the same one the core ${...} expression parsers use. A literal outside the range of a double (such as 1e999) is a parse error. Integers beyond ±(2^53−1) are not portable across SDKs, because an implementation may read them as doubles. 3 and 3.0 are the same value. Decompilers write numbers the way ECMAScript Number.prototype.toString does (exponent notation only below 1e-6 or at or above 1e21, written 2.5e-7 or 1.5e+21), and may keep a .0 on an integral double.
  • Booleans are represented by true or false.
  • Null values are represented by null.

Omitted or skipped arguments are not represented by a literal type; instead, they are handled via syntax rules (see Schema-driven key mapping).

Structural lists

Arrays are represented using square brackets, for example [component1, component2]. The compiler maps these arrays to child container slots in the target catalog components.

If a component property expects a dynamic list template (such as the children slot of a List component), it uses the compiler-reserved _template(path, templateComponent) helper:

breedList = List(_template($/breeds, breedTemplate), "horizontal")

The helper starts with an underscore _ to clearly distinguish it from components in custom catalogs and prevent naming conflicts.

Data binding and reactive paths

To connect component properties to the application data model, properties accept bound paths prefixed with the $ symbol:

  • Absolute paths start with a forward slash after the prefix, for example $/user/email. These paths resolve from the root of the shared data model.
  • Relative paths omit the slash, for example $lastName. These resolve within list iteration contexts.

Data model population

To populate or initialize values within the shared data model directly from the generated output, A2UI Express supports data model assignments. A statement with a left-hand side that represents an absolute data path will populate that path in the surface's dataModel:

$/path/to/key = value_expression

Where value_expression is any valid literal primitive, array, or map. For example:

$/icon = "check"
$/title = "Enable notification"
$/user = {firstName: "Alice", age: 30}

The compiler resolves these statements and generates the structured dataModel JSON object at the root of the createSurface payload.

If an A2UI Express block contains only data path assignments and omits the reserved root component variable entirely, the compiler produces a standalone updateDataModel protocol message instead of a createSurface layout payload:

{
  "version": "v1.0",
  "updateDataModel": {
    "surfaceId": "default_surface",
    "path": "/",
    "value": {
      "icon": "check",
      "title": "Enable notification"
    }
  }
}

Nested function calls and actions

To support catalog flexibility and avoid hardcoding specific formatting or action helpers, A2UI Express uses standard nested function call syntax.

  • Client functions are written as <FunctionName>(<args>), matching the exact function names registered in the loaded catalog.
  • If the client catalog contains a text formatting helper (such as formatString), it is called explicitly: welcomeText = Text(formatString("Welcome, ${/user/firstName}!")). This prevents failures if a client catalog uses a different naming convention for interpolation.
  • Local actions use this same signature to trigger behaviors, for example openUrl("https://example.com"). The compiler maps these to standard client function actions.
  • Server events use a reserved Event signature to declare backend actions, for example Event("save_deal", {rep: $/form/rep}).

Validation and logic expressions

Form validation checks are defined using the ? prefix. If a component expects validation rules, the compiler converts these expressions into standard client-side functions:

  • Simple checks are written with the function name, for example ?required.
  • Parameterized checks accept arguments in parentheses, for example ?regex("^[0-9]{5}$", "Must be a valid zip code").
  • Multiple checks are grouped in lists: [?required, ?email].

Standalone operations and function calls

To execute standalone lifecycle operations or invoke client-side functions directly from the server, A2UI Express supports standalone function call lines without variable assignments:

Creating or updating a surface

To create or target a user interface surface, output surface(surfaceId) before component variable definitions:

surface("dashboard-surface-1")
root = Card(...)

The optional sendDataModel=true keyword argument (surface("dashboard-surface-1", sendDataModel=true)) sets the sendDataModel flag of the createSurface message. To update components (including root) on an existing surface rather than creating a new surface, pass update=true: surface("dashboard-surface-1", update=true).

The statements after surface(...), up to the next surface statement, form one scope. By default, a scope that defines root compiles to a createSurface message (or for v0.9 and v0.9.1 targets, a bare createSurface followed by updateComponents and updateDataModel), while a scope that defines components without root compiles to an updateComponents message (plus updateDataModel if data paths are assigned). Specifying update=true compiles the scope to an updateComponents message even if root is defined, allowing updates to the root container of an existing surface. If surface() is omitted, the compiler uses the surface ID passed to it ("default_surface" by default). A scope that only assigns data paths compiles to an updateDataModel message.

Catalogs

With a single catalog, createSurface carries that catalog's catalogId (and surface(surfaceId, catalogId) is optional). With multiple catalogs (supported in v1.0 and newer), createSurface omits catalogId, naming a catalog on surface(...) is an error, and every compiled component and function call carries its own catalogId.

A component or function call written without a catalogId resolves by name across the active catalogs when exactly one catalog defines it, and requires an explicit catalogId override when multiple catalogs define that name. From v1.0, a component or function call can specify its catalog with a catalogId keyword argument, and a check takes a {catalogId: ...} map argument:

widget = Chart($/sales, catalogId="https://example.com/charts.json")
action = shareLink($/url, catalogId="https://example.com/sharing.json")
field = TextField("Zip", $/zip, checks=[?zipCode({catalogId: "https://example.com/checks.json"})])

The decompiler writes a catalogId only when the component or function name is defined in more than one active catalog (or differs from the single registered catalog). The v0.9 and v0.9.1 schemas allow catalogId only on createSurface, so for those targets the compiler rejects multiple catalogs and per-component or per-function catalogId overrides.

Note: The multi-catalog behavior described above is implemented in the Python and TypeScript SDKs. The Dart Express compiler does not yet support v1.0 (dart/a2ui_agent/lib/src/inference_formats/express/compiler.dart); see dart/a2ui_agent/test/conformance/multi_catalog_formats_conformance_test.dart for current conformance status.

Deleting a surface

When the compiler encounters the standalone deleteSurface command, it produces a standard deleteSurface lifecycle message:

deleteSurface("dashboard-surface-1")
{
  "version": "v1.0",
  "deleteSurface": {
    "surfaceId": "dashboard-surface-1"
  }
}

Executing client-side functions (RPC)

When the compiler encounters any other standalone function call, it resolves the arguments against catalog definitions and produces a callRendererFunction message with an auto-generated functionCallId. The call resolves its catalog from an explicit catalogId keyword argument, or else by function name across the active catalogs:

openUrl("https://example.com")
{
  "version": "v1.0",
  "callRendererFunction": {
    "functionCallId": "call_1",
    "callFunction": {
      "@call": "openUrl",
      "args": {
        "url": "https://example.com"
      },
      "catalogId": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json"
    }
  }
}

The n-th standalone call of a block gets the functionCallId call_<n>. To set another ID, pass the reserved functionCallId keyword argument, which every catalog accepts: openUrl("https://example.com", functionCallId="open-docs"). The decompiler writes the argument only when the ID differs from the one the compiler would generate.

The functionCallId and the callFunction both sit inside callRendererFunction, and catalogId is required on the call. See CallRendererFunctionMessage in specification/v1_0/json/agent_to_renderer.json for the normative definition. callRendererFunction exists only in v1.0, so the compiler rejects a standalone function call for v0.9 and v0.9.1 targets.

v1.0 binding keys

For v1.0 targets, the compiler writes data bindings as {"@path": ...} and function calls as {"@call": ..., "args": ...}, the reserved keys of the v1.0 common types. For v0.9 and v0.9.1 targets, it writes path and call.

Compilation pipeline

The compilation runs as part of the inference pipeline. It takes the plain text stream of A2UI Express, processes it, and emits a standard A2UI v1.0 JSON payload to send to the client.

flowchart TD
    A("Plain text stream") --> B("Lexer and line parser")
    B --> C("Abstract syntax tree")
    C --> D("Schema mapper")
    D --> E("AST flattener")
    E --> F("Standard A2UI v1.0 JSON")

Line parsing and tokenization

The compiler reads the input text line-by-line. It discards empty lines and parses assignments into tokens.

Error recovery and micro-refinement loops

If the compiler encounters a syntax error or catalog schema mismatch during parsing, it triggers a structured error recovery workflow:

  1. Isolation. The compiler flags the invalid line, discards that sub-branch of the AST, and continues parsing the remaining lines to avoid collapsing the user interface.
  2. Agent-side micro-refinement. The agent packages the invalid line, the targeted component signature, and the parser error message into a tiny correction prompt.
  3. Fast model-based correction. This correction prompt is sent to a fast foundation model before converting the A2UI Express syntax to proper A2UI. Because the prompt is small and targets a single line, execution is fast.
  4. Hot swapping. The model returns the corrected statement, which the compiler hot-swaps into the active AST before finalizing the render output.

Schema-driven key mapping

Because A2UI Express omits property keys, the compiler relies entirely on the JSON schema of the loaded catalog to map positional arguments.

  1. The compiler looks up the component or function name in the catalog schema, discarding structural keys like component and id.
  2. It reads the declared properties in their strict definition order.
  3. It maps the positional arguments of the A2UI Express statement to these property keys in sequence.
  4. Trailing optional arguments can be omitted from the end of the statement. For example, if a component signature is Button(child, action?) where action is optional, Button(label_text) is compiled with action as null/omitted.
  5. Skipped optional arguments (where a subsequent argument must be specified) are represented by an underscore _ placeholder. For example, if justify is optional in Column(children, justify?, align?), Column([icon, title], _, "center") maps the array to children and "center" to align, leaving justify unspecified.

Adjacency list flattening

Standard A2UI v1.0 requires a flat array of components where nesting is represented by referencing IDs in an adjacency list.

  1. The compiler traverses the variable references starting at the root variable.
  2. It generates a unique, stable string ID for each sub-component variable, replacing variable names with these IDs.
  3. It packages child arrays into standard ChildList structures.
  4. It outputs a single flat components array.

Protocol envelope construction

The compiled component list and any extracted default data models are wrapped in a standard A2UI message envelope:

{
  "version": "v1.0",
  "createSurface": {
    "surfaceId": "surface_id",
    "catalogId": "catalog_identifier",
    "components": [],
    "dataModel": {},
    "surfaceParams": {}
  }
}

Compilation example

This section shows an input text stream in A2UI Express and the compiled A2UI v1.0 JSON payload.

Input text stream

The input file defines a notification permission card, using positional arguments and absolute data paths enclosed within the <a2ui> and </a2ui> sentinel tags:

<a2ui>
root = Card(main_column)
main_column = Column([icon, title, description, actions], _, "center")
icon = Icon($/icon)
title = Text($/title, "h3")
description = Text($/description, "body")
actions = Row([yes_btn, no_btn], "center")
yes_btn_text = Text("Yes")
yes_btn = Button(yes_btn_text, _, Event("accept"))
no_btn_text = Text("No")
no_btn = Button(no_btn_text, _, Event("decline"))
</a2ui>

Compiled A2UI JSON output

The compiler parses the text stream, resolves the parent-child references, maps positional arguments to the catalog schema, and produces the following flat component layout inside the createSurface message:

{
  "version": "v1.0",
  "createSurface": {
    "surfaceId": "gallery-notification-permission",
    "catalogId": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json",
    "components": [
      {
        "id": "root",
        "component": "Card",
        "child": "main_column"
      },
      {
        "id": "main_column",
        "component": "Column",
        "children": ["icon", "title", "description", "actions"],
        "justify": null,
        "align": "center"
      },
      {
        "id": "icon",
        "component": "Icon",
        "name": {
          "@path": "/icon"
        }
      },
      {
        "id": "title",
        "component": "Text",
        "text": {
          "@path": "/title"
        },
        "variant": "h3"
      },
      {
        "id": "description",
        "component": "Text",
        "text": {
          "@path": "/description"
        },
        "variant": "body"
      },
      {
        "id": "actions",
        "component": "Row",
        "children": ["yes_btn", "no_btn"],
        "justify": "center"
      },
      {
        "id": "yes_btn_text",
        "component": "Text",
        "text": "Yes"
      },
      {
        "id": "yes_btn",
        "component": "Button",
        "child": "yes_btn_text",
        "variant": null,
        "action": {
          "event": {
            "name": "accept",
            "context": {}
          }
        }
      },
      {
        "id": "no_btn_text",
        "component": "Text",
        "text": "No"
      },
      {
        "id": "no_btn",
        "component": "Button",
        "child": "no_btn_text",
        "variant": null,
        "action": {
          "event": {
            "name": "decline",
            "context": {}
          }
        }
      }
    ]
  }
}

Ecosystem integration

A2UI Express is completely catalog-agnostic, making sure it integrates into any environment using custom catalogs.

Catalog-agnostic operation

The compiler does not hold hardcoded assumptions about component names, function signatures, or properties.

  • Schema loading. During startup, the host application registers the target catalog's JSON schema (such as a custom enterprise catalog or a specific system catalog).
  • Signature compilation. The prompt generation tools read the registered schema and compile its definitions directly into positional signatures for the model's system prompt.
  • Mapping execution. The parser dynamically maps generated arguments based on the loaded schema structure. If the catalog is replaced entirely, the system adapts without manual updates.

Automated catalog-to-prompt utility

To keep the model updated with the latest catalog definitions, a python utility compiles the active JSON schema into plain-text signatures:

import json

class CatalogSignatureCompiler:
    def __init__(self, schema_path: str):
        with open(schema_path, "r") as f:
            self.catalog = json.load(f)
        self.components = self.catalog.get("components", {})

    def compile_to_signatures(self) -> str:
        signatures = []
        for name, schema in self.components.items():
            props = schema.get("properties", {})
            required = schema.get("required", [])
            ordered_args = []
            for prop_name, prop_schema in props.items():
                if prop_name in ["component", "id"]:
                    continue
                is_req = prop_name in required
                optional_marker = "" if is_req else "?"
                ordered_args.append(f"{prop_name}{optional_marker}")
            sig = f"{name}({', '.join(ordered_args)})"
            signatures.append(sig)
        return "\n".join(signatures)

Local performance profiles

Local execution requires configuring the model's thinking budget based on the complexity of the interface:

  • Single-view dashboards or basic data tables run best with a reasoning budget of 70 to 140 tokens to guarantee low latency.
  • Interactive forms with nested layout structures require a reasoning budget of 280 to 560 tokens to prevent hierarchy errors and broken bindings.