Interfaces
An .a2ui artifact is an interface the reader uses: a form, a dashboard, a checklist, a picker, a control panel. The assistant describes it as data in the A2UI protocol, version 1.0, and the workspace draws it with its own components, in the reader's theme, light or dark. The file holds no code and no styling, and it can run only what its document declares.
What the reader does in an interface comes back to the conversation as a message from them, so the assistant can answer, change the interface and publish it again.
The document
A .a2ui file is a JSON object. Only messages is required, and a bare array of messages also works.
{
"title": "Expense approvals",
"files": { "invoices": "/work/invoices.csv" },
"sources": [ { "id": "sales", "tool": "Sales Report", "function": "RegionalSales", "into": "/sales" } ],
"messages": [
{ "version": "1.0", "createSurface": { "surfaceId": "main", "catalogId": "https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json", "sendDataModel": true } },
{ "version": "1.0", "updateComponents": { "surfaceId": "main", "components": [ ] } },
{ "version": "1.0", "updateDataModel": { "surfaceId": "main", "path": "/amount", "value": 120 } }
]
}
| Key | Meaning |
|---|---|
title |
The name on the chip. |
createSurface |
Names the surface and the catalog, which is always the basic catalog above. sendDataModel: true makes every action carry the whole data model. |
updateComponents |
A flat list of components that point at each other by id. One of them has the id root. |
updateDataModel |
Sets a value at a JSON pointer such as /user/name. |
sources |
Tool and sandbox calls run when the interface opens. |
files |
Sandbox files published with the interface. |
publish checks the document and reports what is wrong with it: no surface, a tool the conversation does not have, a missing file. Nothing is shown until it passes.
Components
The basic catalog has Text (Markdown), Image, Icon, Video, AudioPlayer, Row, Column, List, Card, Tabs, Modal, Divider, Button, TextField, CheckBox, ChoicePicker, Slider and DateTimeInput. Extension catalogs add the components that tie an interface to the workspace. A component from an extension catalog names it with @catalogId.
| Catalog | Components | Use |
|---|---|---|
https://curiosity.ai/a2ui/catalogs/tesserae/1.0 |
Table, Grid, Metric, Banner, Badge, PropertyGrid, Accordion, Toggle, Rating, TagsInput, DateRange, JsonViewer, CodeDiff, Mermaid |
Reports, dashboards and richer inputs. |
https://curiosity.ai/a2ui/catalogs/flint-chart/1.0 |
FlintChart |
A chart drawn by Flint from rows and an encoding. |
https://curiosity.ai/a2ui/catalogs/search-area/1.0 |
SearchArea |
The workspace search box, facets and results, searched as the reader. |
https://curiosity.ai/a2ui/catalogs/graph-view/1.0 |
GraphView |
Nodes and the edges between them, from a list of UIDs. |
https://curiosity.ai/a2ui/catalogs/nodes/1.0 |
Node, NodePicker |
Nodes drawn from UIDs, and a picker for choosing nodes of given types. |
The children of an extension container, such as a Grid or an Accordion, are ordinary basic-catalog components.
{ "id": "sales_chart", "component": "FlintChart", "@catalogId": "https://curiosity.ai/a2ui/catalogs/flint-chart/1.0",
"chartType": "Bar Chart", "title": "Sales per product", "height": 280,
"data": { "@path": "/sales" },
"encodings": { "x": "Product", "y": "Sales" } }
Data binding
A value that should follow the data is {"@path": "/pointer"} in place of a literal.
- Inputs are two-way. A
TextFieldbound to/amountwrites what is typed there and shows whatever/amountholds. - Lists come from data.
"children": {"componentId": "row", "path": "/items"}drawsrowonce per item. Inside the template, a path without a leading slash is relative to the item. - Functions are
{"@call": "name", "args": {...}}:formatString,formatNumber,formatCurrency,formatDate,pluralize,openUrl(in an action only) and the checksrequired,regex,length,numeric,email,and,or,not. - Validation.
@checkson an input or a button shows the failing message and disables the button.
Actions
A button's action is an event or a function call.
An event is a message to the conversation. Give it a userMessage, which is what the transcript shows as the reader's message; without one the reader sees the bare event name.
"action": { "event": { "name": "approve_expense", "userMessage": "Approve the ${/vendor} invoice",
"context": { "amount": {"@path":"/amount"} } } }
context is resolved when the reader clicks. The assistant receives a message that names the interface, the action and the context as JSON, and answers it as it would any other turn. Only the owner's conversation receives events; see who runs what.
A function call runs inside the interface and tells nobody: openUrl, or invokeTool.
Tools and data
An interface can call the AI tools and sandboxes that its conversation has. publish records them on the artifact. A tool the conversation does not have is refused at publish time.
Load data when the interface opens with sources. Each entry names a tool, optionally a function, its arguments, and the data-model path to write the result to:
"sources": [
{ "id": "sales", "tool": "Sales Report", "function": "RegionalSales", "arguments": { "region": "EMEA" }, "into": "/sales", "refreshSeconds": 30 },
{ "id": "rows", "tool": "Ledger", "script": "ledger list --json", "into": "/rows" }
]
refreshSecondsis 5 or more and runs the call again while the interface is open.- A sandbox call carries a
script. The script is bash, andpython3is available through bash. Each call gets a fresh session, so nothing one call writes is there for the next. - The outcome is at
/_tools/<id>as{ "status": "loading" | "done" | "error", "error": "...", "value": ... }. - A button runs a call with
invokeTool, withintofor the result:{"@call":"invokeTool","args":{"id":"refresh","tool":"Sales Report","function":"RegionalSales","into":"/sales"}}.
For anything with a side effect, such as sending, deleting or paying, use an event and let the assistant do it after the reader confirms. A button that runs a destructive tool directly runs it by accident.
Who runs what
Every call comes from the viewer's browser and runs as the person looking at the interface, not as its author. Access to the tool is checked for them, and a tool they may not use fails with an error shown above the interface.
The server also checks each call against the artifact. It accepts the tool and function that the document names, a script that is a literal string in the document, and arguments where a literal value is unchanged and a value bound with @path may be whatever the reader chose. It rejects a key the document did not write. A script therefore cannot be built from what a reader types, and publish refuses a script or function that is bound to data.
Do not put a key or a secret in the document. Everyone who can open the artifact can read it.
Files
"files": { "sales": "/work/sales.csv" } publishes sandbox files with the interface, up to 24 files of 20 MB each. The file name may use letters, digits, ., - and _. Anyone who can open the interface can read its files, which appear in the data model:
| Path | Holds |
|---|---|
/files/<name>/url |
Where to load it from, for an Image, Video or AudioPlayer. |
/files/<name>/text |
The text of a text file up to 2 MB. |
/files/<name>/json |
The parsed value of a .json file. |
/files/<name>/rows |
An array of objects keyed by the header, for .csv and .tsv. |
/files/<name>/type, /size |
The content type and the size in bytes. |
Revising an interface
Writing the same file name and publishing again replaces the interface in place and keeps its link. When an event arrives, the assistant answers it and, if the interface should change, rewrites the file and publishes again. The surface id stays the same.
Sharing
The owner shares a published interface from their Artifacts space. A person it is shared with can open and use it, with the tools running as them. They cannot change it or delete it, and what they do in it does not reach the owner's conversation, because they have no seat in it. An interface meant for other people therefore puts its work in tools and sandboxes that each person can run, not in events.
Check it before publishing
render people.a2ui draws a published interface with its data loaded and returns the pictures. See Look at it before publishing.