Order Summary
- Subtotal
- $138.00
- Shipping
- Free
- Tax
- $12.42
- Total
- $150.42
By default, tool results are read-only. The assistant calls a tool, a component renders the data, and that's it. Actions change that: they let users respond to what the assistant shows them.
An action might be a small utility (exporting a table as CSV, copying a code snippet) or a consequential decision like approving a deploy or confirming a purchase. Tool UI splits these into two categories: local actions for in-context side effects, and decision actions for choices that return to the assistant and produce a permanent receipt.
For background on how components relate to the assistant and the user, see Design Guidelines.
The model chooses a tool and sends structured arguments.
getExpenses with args:{
"month": "2026-01",
"category": "travel"
}tools: { getExpenses: tool({ description: "Return travel expenses for a month", execute: async ({ month }) => ({ id: "expenses-jan", month }), }),}The returned args are parsed and rendered as a display surface.
| Delta Airlines | 847 |
| Acme Hotel | 312 |
render: ({ result }) => { const parsed = safeParseSerializableDataTable(result); if (!parsed) { return null; } return <DataTable {...parsed} />;},The user clicks an action control associated with the surface.
| Delta Airlines | 847 |
| Acme Hotel | 312 |
No action clicked yet.
const localActions = [{ id: "export-csv", label: "Export CSV" }];async function handleLocalAction(actionId: string) { // side effects only}Your action handler runs side effects (navigation, export, API call, etc.).
Runtime status
Waiting for a user action.
async function handleLocalAction(actionId: string) { if (actionId === "export-csv") await exportCsv(); if (actionId === "open-report") router.push("/reports/monthly");}If the action is consequential, commit a durable outcome (typically via addResult(...) in your renderer runtime).
No decision committed yet.
const decision = createDecisionResult({ decisionId: "order-123", action: { id: "confirm", label: "Purchase" },});await addResult?.(decision);Tool UI has two action surfaces:
ToolUI.LocalActions: Non-consequential side effects. Exporting, copying, opening a link. The assistant doesn't need to know it happened.ToolUI.DecisionActions: Consequential choices that produce a durable decision envelope. Approving, confirming, selecting. The result returns to the assistant and the component shows a receipt.When to use which: If the user's action changes the conversation, if the assistant should know about it and respond, use DecisionActions. If it's a utility that stays local to the UI, use LocalActions.
For display-first components, keep rendering and actions separate: the display component goes inside ToolUI.Surface, and local actions go inside sibling ToolUI.Actions.
| Delta Airlines | 847 |
| Acme Hotel | 312 |
Try a local action below.
LocalActions handlers should not commit durable result state.
When user intent is consequential (approve, confirm, purchase, publish, delete), use DecisionActions. This makes decision payload shape explicit and commit behavior auditable.
No decision committed yet.
DecisionActions handlers should return a typed envelope, then commit in the commit phase.
Some components are action-centric by design. Their action handling is part of component semantics, so actions stay embedded on the component rather than using sibling ToolUI.Actions surfaces.
All three action-centric components use the same embedded action interface:
actions: action buttons rendered by the component.onAction(actionId, state): runs after the action behavior and receives post-action state.onBeforeAction(actionId, state): guard evaluated before an action runs.Only the state type changes by component:
OptionList: OptionListSelectionParameterSlider: SliderValue[]PreferencesPanel: PreferencesValueEach example below shows the live component on the left and the corresponding mock runtime output on the right.
Mock output
onAction(actionId, state)
{
"actionId": "confirm",
"state": "merge"
}Mock output
onAction(actionId, state)
{
"actionId": "apply",
"state": [
{ "id": "exposure", "value": 0.2 },
{ "id": "contrast", "value": 12 }
]
}Mock output
onAction(actionId, state)
{
"actionId": "save",
"state": {
"marketing-email": true,
"digest-frequency": "weekly"
}
}