Using Macros
Macros let you define composite UI components on the server that can programmatically inject server-side data and expand into the flexible, reusable catalog components your client supports (from the Basic Catalog, your own custom catalogs, or both) before any messages reach the renderer. Because the renderer only receives components the client already implements, you can add or change macros on the server without shipping new client code.
How macros work
When a language model builds UI from low-level primitives on every turn, repeated patterns like flight summaries or product cards take many lines of nested components. With a macro, you write a function once on the server to assemble that pattern.
Macros are useful when you want to:
- Reduce output tokens and latency by letting the model write one call instead of a nested component tree.
- Keep layouts consistent across turns because your server code constructs the component hierarchy.
- Look up data from server-side databases and APIs, accepting an identifier from the model and populating the components on the server.
- Ship new domain components with a server deployment alone, using catalog components the client already renders.
In the sequence below, MacroExpander works on both sides of the model call. Before inference, it adds each macro’s component schema to the catalog in the system prompt. After inference, it intercepts each macro call, runs your macro function, and outputs flat catalog components for the renderer.
sequenceDiagram participant Model participant Agent as Agent server participant Expander as MacroExpander participant Renderer Agent->>Expander: Add macro schemas to catalog Expander-->>Agent: Inference catalog Agent->>Model: System prompt with FlightCard(...) signature Model-->>Agent: root = FlightCard("OS 87", ...) Agent->>Expander: Expand macro calls in messages Expander-->>Agent: Expanded catalog components Agent->>Renderer: createSurface with catalog componentsExample: macro, model call, and expanded output
Here is a FlightCard macro. The flight and route parameters take static strings, while status uses DynamicString so it can bind to the data model:
from a2ui.builder.v1_0 import Card, Column, DynamicString, Textfrom a2ui.transformers.macros import macro
@macrodef FlightCard(flight: str, route: str, status: DynamicString) -> Card: """Summary card for a flight with a live status line.
Args: flight: Flight number, such as "OS 87". route: Departure and arrival cities, such as "Vienna to New York". status: Current status text or data binding. """ return Card( child=Column( children=[ Text(text=f"### {flight}"), Text(text=route, variant="caption"), Text(text=status), ] ) )Using the Express inference format, the model calls FlightCard in a single line:
Your flight is boarding.<a2ui>$/flight/status = "Boarding"root = FlightCard("OS 87", "Vienna to New York", $/flight/status)</a2ui>MacroExpander runs FlightCard on the server and sends the expanded components over the wire, with static arguments baked in and the /flight/status data binding preserved:
[ { "version": "1.0", "createSurface": { "surfaceId": "main", "catalogId": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json", "components": [ { "component": "Text", "id": "root__text_1", "text": "### OS 87", "variant": "body" }, { "component": "Text", "id": "root__text_2", "text": "Vienna to New York", "variant": "caption" }, { "component": "Text", "id": "root__text_3", "text": {"@path": "/flight/status"}, "variant": "body" }, { "component": "Column", "id": "root__column_1", "children": ["root__text_1", "root__text_2", "root__text_3"], "justify": "start", "align": "stretch" }, { "component": "Card", "id": "root", "child": "root__column_1" } ], "dataModel": { "flight": {"status": "Boarding"} } } }][ { "version": "v0.9", "createSurface": { "surfaceId": "main", "catalogId": "https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json" } }, { "version": "v0.9", "updateComponents": { "surfaceId": "main", "components": [ { "component": "Text", "id": "root__text_1", "text": "### OS 87", "variant": "body" }, { "component": "Text", "id": "root__text_2", "text": "Vienna to New York", "variant": "caption" }, { "component": "Text", "id": "root__text_3", "text": {"path": "/flight/status"}, "variant": "body" }, { "component": "Column", "id": "root__column_1", "children": ["root__text_1", "root__text_2", "root__text_3"], "justify": "start", "align": "stretch" }, { "component": "Card", "id": "root", "child": "root__column_1" } ] } }, { "version": "v0.9", "updateDataModel": { "surfaceId": "main", "path": "/", "value": { "flight": {"status": "Boarding"} } } }]Configure a macro
The steps below integrate a macro into a Python agent using BasicCatalog("1.0"). You do not need to use the Basic Catalog: the same workflow applies to your own custom catalogs, as shown in Macros with custom catalogs.
Step 1: Install the Python agent SDK
Install a2ui-agent-sdk with pip (or uv add a2ui-agent-sdk):
pip install a2ui-agent-sdkStep 2: Define a macro
Decorate a Python function with @macro and return a single root builder component. @macro inspects the parameter type hints and the Args: block in the docstring to generate the component schema that the model sees:
from a2ui.builder.v1_0 import ( Action, Button, Card, Column, DynamicString, Text,)from a2ui.transformers.macros import macro
@macrodef FlightCard( flight: str, route: str, status: DynamicString, on_rebook: Action,) -> Card: """Summary card for a flight with a live status line and rebook button.
Args: flight: Flight number, such as "OS 87". route: Departure and arrival cities, such as "Vienna to New York". status: Current status text or data binding. on_rebook: Action sent when the user clicks Rebook. """ return Card( child=Column( children=[ Text(text=f"### {flight}"), Text(text=route, variant="caption"), Text(text=status), Button(child=Text(text="Rebook"), action=on_rebook), ] ) )Because a macro is regular Python running on your server, it can also take an identifier (such as flight_id: str), look up the record in a database or service, and populate the returned components directly.
Step 3: Set up the expander and request processor
Create a MacroExpander with your macro list, create an inference catalog that includes the macro schemas, and pass that catalog to A2uiRequestProcessor:
from a2ui.core.basic_catalog import BasicCatalogfrom a2ui.processor import A2uiRequestProcessorfrom a2ui.transformers.macros import MacroExpander
from flight_macros import FlightCard
expander = MacroExpander([FlightCard], on_error="raise")inference_catalog = expander.transform_to_inference_catalog(BasicCatalog("1.0"))
processor = A2uiRequestProcessor(catalogs=[inference_catalog])system_prompt = processor.prompt_snippet # Append to your model's system instruction.MacroExpander runs alongside A2uiRequestProcessor rather than as a CatalogConfig.transformers entry because it acts both before inference (adding macro schemas to the catalog) and after parsing (expanding macro calls in outgoing messages). Because MacroExpander tracks active surface catalogs across turns, create one MacroExpander per session (or pass surface_catalogs to transform_to_transport) in multi-session servers. A2uiRequestProcessor uses the Express inference format by default, and works with any format factory you pass via format_factory.
Step 4: Parse model output and expand macros
After your model responds, parse the response with processor.parse_response and pass each A2uiPart through expander.transform_to_transport before serializing the messages for the client:
from a2ui.parser import A2uiPart, TextPart
# llm_output = call_your_model(system_prompt, user_message)parts = processor.parse_response(llm_output)
text_reply: list[str] = []transport_messages = []
for part in parts: if isinstance(part, TextPart): text_reply.append(part.text) elif isinstance(part, A2uiPart): transport_messages.extend(expander.transform_to_transport(part.a2ui))
wire_payload = [ msg.model_dump(by_alias=True, exclude_none=True) for msg in transport_messages]Send wire_payload over your transport to the renderer alongside any conversational text in text_reply.
Macros with custom catalogs
Macros build component trees using typed Python builder classes. Builder classes for the Basic Catalog ship in a2ui.builder.v1_0 and a2ui.builder.v0_9. When you define a custom catalog, the A2UI CLI (a2ui) generates those Python builder classes directly from your catalog JSON file.
Step 1: Install the A2UI CLI
Install the standalone a2ui binary using the release installer script. Both scripts verify the SHA-256 checksum of the downloaded archive and do not require administrator privileges:
curl -fsSL https://raw.githubusercontent.com/a2ui-project/a2ui/main/scripts/public/install_cli.sh | shInstalls a2ui into ${XDG_BIN_HOME:-$HOME/.local/bin}. Make sure that directory is on your PATH, or pass A2UI_INSTALL_DIR (and optionally A2UI_VERSION) to sh, as in curl -fsSL ... | A2UI_INSTALL_DIR="$HOME/bin" sh.
irm https://raw.githubusercontent.com/a2ui-project/a2ui/main/scripts/public/install_cli.ps1 | iexInstalls a2ui.exe into %LOCALAPPDATA%\Programs\a2ui\bin and adds it to your user PATH. Set $env:A2UI_INSTALL_DIR or $env:A2UI_VERSION to customize the directory or version.
Step 2: Generate builder classes for your catalog
Suppose you have a travel_catalog.json file that defines a BoardingPass component (and your client renderer registers the matching BoardingPass widget).
Example travel_catalog.json
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "catalogId": "https://example.com/catalogs/travel/v1/catalog.json", "protocolVersion": "1.0", "components": { "BoardingPass": { "type": "object", "description": "A boarding pass with a scannable code.", "properties": { "component": {"const": "BoardingPass"}, "passenger": {"type": "string"}, "seat": {"type": "string"}, "gate": {"$ref": "common_types.json#/$defs/DynamicString"} }, "required": ["component", "passenger", "seat", "gate"] } }, "$defs": { "anyComponent": { "oneOf": [{"$ref": "#/components/BoardingPass"}], "discriminator": {"propertyName": "component"} }, "anyFunction": {"not": {}} }}Run a2ui codegen with --lang python to generate a Python builder module from your catalog schema:
a2ui codegen --lang python --catalog travel_catalog.json --out travel_builders.py --catalog-name travelThe generated travel_builders.py file contains a typed ComponentBuilderNode subclass for every component in your catalog:
class BoardingPass(ComponentBuilderNode): r"""A boarding pass with a scannable code."""
component: Literal["BoardingPass"] = "BoardingPass" passenger: str seat: str gate: str | DataBinding | FunctionCallRe-run a2ui codegen whenever travel_catalog.json changes rather than editing the generated file by hand.
Step 3: Write a macro with the generated builders
Import your generated builder classes into a macro. In A2UI v1.0, a single surface can combine components from multiple catalogs, so a macro can mix your custom BoardingPass with layout components from BasicCatalog("1.0"):
from a2ui.builder.v1_0 import Card, Column, DynamicString, Textfrom a2ui.transformers.macros import macro
from travel_builders import BoardingPass
@macrodef TripSummary(passenger: str, seat: str, gate: DynamicString) -> Card: """Trip summary card containing a boarding pass.
Args: passenger: Passenger full name. seat: Assigned seat number. gate: Departure gate text or data binding. """ return Card( child=Column( children=[ Text(text=f"### Have a good trip, {passenger}"), BoardingPass(passenger=passenger, seat=seat, gate=gate), ] ) )Load your custom catalog with Catalog.from_json and pass all transport catalogs to expander.transform_catalogs. If your agent uses only your custom catalog without the Basic Catalog, pass [travel_catalog] instead:
import jsonfrom pathlib import Path
from a2ui.core import Catalogfrom a2ui.core.basic_catalog import BasicCatalogfrom a2ui.parser import A2uiPartfrom a2ui.processor import A2uiRequestProcessorfrom a2ui.transformers.macros import MacroExpander
from trip_macros import TripSummary
travel_catalog = Catalog.from_json( json.loads(Path("travel_catalog.json").read_text()))basic_catalog = BasicCatalog("1.0")
expander = MacroExpander([TripSummary], on_error="raise")inference_catalogs = expander.transform_catalogs([basic_catalog, travel_catalog])
processor = A2uiRequestProcessor(catalogs=inference_catalogs)system_prompt = processor.prompt_snippet
# After receiving llm_output from the model:parts = processor.parse_response(llm_output)inference_messages = [ msg for part in parts if isinstance(part, A2uiPart) for msg in part.a2ui]transport_messages = expander.transform_to_transport(inference_messages)In v1.0, transform_catalogs registers your base catalogs on the expander and appends an inference-only sidecar catalog for the macros. When transform_to_transport expands TripSummary, it removes the sidecar catalog and stamps the owning catalogId onto each expanded Card, Column, Text, and BoardingPass component automatically.
Reference
Supported parameter types
Parameter annotations control both the JSON Schema shown to the model and whether a value is static or reactive:
- Plain Python types (
str,int,float,bool) accept only static literal values, which are baked into the expanded components when the macro runs. Passing a data binding to a plainstrparameter fails validation. - Reactive A2UI types (
DynamicString,DynamicNumber,DynamicBoolean,DynamicStringList) accept either a literal value or a@path/@callbinding. When you pass that parameter to a child component property, the binding remains live on the renderer.
Always treat macro arguments as untrusted model output, and validate identifiers against your backend data rather than trusting arbitrary strings.
The table below lists the most common parameter annotations (showing v1.0 schema definitions):
| Python annotation | Schema exposed to the model | Value passed to your macro |
|---|---|---|
str, int, float, bool | string, integer, number, boolean | Static literal value (data bindings rejected). |
Literal[...] or Enum (with str values) | enum | Static literal value (Enum parameters arrive as raw str, not Enum members). |
Sequence[V], dict[str, V] | array, object | List or dict with elements converted to V. |
DynamicString, DynamicNumber, DynamicBoolean, DynamicStringList | DynamicString and related types | Literal value, DataBinding, or FunctionCall. |
Action | Action | Action built from an event object or function call. |
Child or component class | Child (ComponentId in v0.9.1) | ComponentRef pointing to another component ID. |
ChildList or Sequence[Child] | ChildList | List of ComponentRef items; ChildList (not Sequence[Child]) also accepts DynamicChildList bindings. |
CheckRule or Sequence[CheckRule] | CheckRule (or array of CheckRule) | CheckRule instance(s) for input validation. |
AccessibilityAttributes | AccessibilityAttributes | AccessibilityAttributes model. |
@macro reads parameter descriptions from Google-style (Args:) docstrings, Sphinx-style (:param name:) docstrings, or typing.Annotated[T, Field(description="...")]. For additional annotations, including DynamicValue, Extensions, catalog sub-models (TabItem, ChoicePickerOption), and union types, see the parameter type mapping table in the macros README.
Protocol versions
To target A2UI v0.9.1 instead of v1.0, import builders from a2ui.builder.v0_9 and pass a single catalog (such as BasicCatalog("0.9.1")) to expander.transform_to_inference_catalog. Do not mix v1_0 and v0_9 builders or catalogs on the same MacroExpander.
Additional configuration options
Custom macro names and descriptions
Override the inferred PascalCase component name or docstring description by passing arguments to @macro, such as @macro("CustomCard") or @macro(name="CustomCard", description="...").
Error handling (on_error)
With the default on_error="passthrough", a macro that raises an argument or runtime exception during expansion is logged and left unexpanded so sibling components still expand (note that client renderers do not implement unexpanded macro components). Pass on_error="raise" to MacroExpander to raise A2uiValidationError on the server so an agent retry loop can send the error back to the model; structural errors (A2uiRecursionError, A2uiCatalogError, and protocol-version mismatches) always raise.
Filtering base components (passthrough_components)
By default, all components from your base catalogs remain visible to the model alongside your macros. Pass passthrough_components=["Button"] to MacroExpander to expose only specific base components plus your macros, or passthrough_components=[] to expose only your macros.
Explicit protocol_version
MacroExpander normally infers the protocol version from your macro parameter and return annotations or registered catalogs. If a v1.0 macro uses only version-neutral annotations (such as str, int, or Child, with no v1.0 return type) and you export a standalone macro catalog with expander.to_catalog() before registering any v1.0 catalogs, pass protocol_version="1.0" to MacroExpander(...).
Disambiguating duplicate component names (catalog_id)
When multiple registered v1.0 catalogs define a component with the same name, specify the owning catalog when instantiating that builder class, such as BoardingPass(..., catalog_id=TRAVEL_CATALOG_ID).
Related resources
- Macros README for full
@macroandMacroExpanderAPI details. - Macros community sample for a runnable FastAPI agent and React client using server-side macros.
- Inference Formats for details on Express and Direct JSON.