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:
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:
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_Startor 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, androotis 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:
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:
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]+$"orr"""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 example42,3.14,-1,.5,1e-7or2.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 as1e999) is a parse error. Integers beyond ±(2^53−1) are not portable across SDKs, because an implementation may read them as doubles.3and3.0are the same value. Decompilers write numbers the way ECMAScriptNumber.prototype.toStringdoes (exponent notation only below 1e-6 or at or above 1e21, written2.5e-7or1.5e+21), and may keep a.0on an integral double. - Booleans are represented by
trueorfalse. - 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:
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:
Where value_expression is any valid literal primitive, array, or map. For example:
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
Eventsignature to declare backend actions, for exampleEvent("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:
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); seedart/a2ui_agent/test/conformance/multi_catalog_formats_conformance_test.dartfor current conformance status.
Deleting a surface¶
When the compiler encounters the standalone deleteSurface command, it produces a standard deleteSurface lifecycle message:
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:
{
"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:
- 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.
- Agent-side micro-refinement. The agent packages the invalid line, the targeted component signature, and the parser error message into a tiny correction prompt.
- 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.
- 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.
- The compiler looks up the component or function name in the catalog schema, discarding structural keys like
componentandid. - It reads the declared properties in their strict definition order.
- It maps the positional arguments of the A2UI Express statement to these property keys in sequence.
- Trailing optional arguments can be omitted from the end of the statement. For example, if a component signature is
Button(child, action?)whereactionis optional,Button(label_text)is compiled withactionas null/omitted. - Skipped optional arguments (where a subsequent argument must be specified) are represented by an underscore
_placeholder. For example, ifjustifyis optional inColumn(children, justify?, align?),Column([icon, title], _, "center")maps the array tochildrenand"center"toalign, leavingjustifyunspecified.
Adjacency list flattening¶
Standard A2UI v1.0 requires a flat array of components where nesting is represented by referencing IDs in an adjacency list.
- The compiler traverses the variable references starting at the
rootvariable. - It generates a unique, stable string ID for each sub-component variable, replacing variable names with these IDs.
- It packages child arrays into standard
ChildListstructures. - It outputs a single flat
componentsarray.
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.