Actions
A2UI actions are the clickable parts of a UI — buttons that, when pressed, drive what happens next. A clickable button is a function you decorate with @a2ui_action and declare on the transport via actions=[...] — the agent stays a plain Agent. The transport advertises the button to the LLM so it can render it; a click runs your function on the server, the agent is not invoked.
Server-Side Buttons (@a2ui_action)#
Decorate a function with @a2ui_action, then declare it on the server with actions=. The button is declared to the LLM so it can render it, but a click runs the function on the server with the click's event.context as keyword arguments — the agent is not invoked. Inside the handler you can do anything: hit a backend, call a tool, update a surface.
When the LLM renders a button whose action.event.name is "schedule_posts", a click sends the button's event.context back as the handler's keyword arguments and runs it on the server. The decorator derives an example context from the function's parameter schema (here {"time": "<string>"}) and injects it into the system prompt so the LLM emits the right keys; pass example_context= to override it.
What the handler returns is mapped to the client per the A2UI spec:
- one A2UI server→client message — or a list of them (e.g.
updateComponents/updateDataModel) — is sent to the renderer as a surface update (works on every protocol version); - any other JSON value becomes an
actionResponseonly when the client asked for one (wantResponse+actionId, v1.0); otherwise it is fire-and-forget.
A turn whose only input is registered server-action clicks skips the agent entirely — the handlers' messages are the whole response.
Declaring the button on the transport — not on the agent — keeps the Agent plain and reusable across deployments. The same actions=[...] is accepted by A2UIAgentExecutor for the A2A transport, so a button is declared the same way wherever the agent is served.
Other Kinds of Buttons#
A2UI components support two button shapes the spec distinguishes by what the click does:
- Server
event— sends a client→server message; the server handles it. This is what@a2ui_actionproduces. - Client
functionCall— runs a function on the renderer with no network round-trip (e.g.openUrl, copy-to-clipboard).
Client functionCall buttons execute entirely in the client, so they are not registered on the server. The LLM learns which client functions it may target from the active catalog and from the client's advertised capabilities — not from server-side code. You can also render a button whose event.name isn't backed by a registered action: a click on it is rewritten generically into the next prompt so the LLM simply continues the conversation.
The Click Round-Trip#
The server is stateless, so a click is just another request. The client posts the conversation plus an a2ui envelope describing what was pressed; the server runs the matching handler — or, for an unregistered button, rewrites the click into the current turn:
When the action name matches a registered @a2ui_action, the click runs that handler on the server (the agent is not invoked); otherwise the click is rewritten into a plain instruction so the LLM continues the conversation.
v1.0 round-trip
With protocol_version="v1.0", a client's reply to a server callFunction arrives as a functionResponse envelope in the next request and is rewritten to a continuation prompt — the stateless transport needs no pause/resume bookkeeping. The same applies to actionResponse.
Observing Client Interactions#
Each client→server interaction the server receives — a button action, a v1.0 functionResponse, or an error — is surfaced on the turn's stream as an A2UIClientEvent, the inbound mirror of A2UIMessageEvent. Subscribe to react to or log client interactions (beyond what the LLM sees in its prompt):
Note
A purely client-side functionCall button (e.g. openUrl) runs on the renderer with no network round-trip per the A2UI spec, so it produces no envelope and therefore no A2UIClientEvent.
Observing Validation Failures#
A2UI has no server→client error message, so when the model can't produce schema-valid UI within validation_retries + 1 attempts, the agent gracefully degrades to prose and emits no A2UIMessageEvent. To tell "couldn't build UI" apart from "intentionally answered with text", subscribe to A2UIValidationFailedEvent on the stream: