Actions

Interactive controls on tool results.

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.

Mental Model

The model chooses a tool and sends structured arguments.

Chat
Show last month's travel expenses.
Calling getExpenses with args:
{
  "month": "2026-01",
  "category": "travel"
}
Step 1: the model chooses a tool call.
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.

Table data shown as expandable cards. Each card represents one row. Columns: Merchant, Amount.
Delta Airlines
Amount:847
Acme Hotel
Amount:312
render: ({ result }) => {  const parsed = safeParseSerializableDataTable(result);  if (!parsed) {    return null;  }  return <DataTable {...parsed} />;},

The user clicks an action control associated with the surface.

Table data shown as expandable cards. Each card represents one row. Columns: Merchant, Amount.
Delta Airlines
Amount:847
Acme Hotel
Amount: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).

Order Summary

Wireless Keyboard$89.00
Mouse$49.00
Subtotal
$138.00
Shipping
Free
Tax
$12.42
Total
$150.42

No decision committed yet.

const decision = createDecisionResult({  decisionId: "order-123",  action: { id: "confirm", label: "Purchase" },});await addResult?.(decision);

Action Surfaces

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.

Display Components + 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.

Table data shown as expandable cards. Each card represents one row. Columns: Merchant, Amount.
Delta Airlines
Amount:847
Acme Hotel
Amount:312

Try a local action below.

LocalActions handlers should not commit durable result state.

Decision Components + DecisionActions

When user intent is consequential (approve, confirm, purchase, publish, delete), use DecisionActions. This makes decision payload shape explicit and commit behavior auditable.

Order Summary

Wireless Keyboard$89.00
Mouse$49.00
Subtotal
$138.00
Shipping
Free
Tax
$12.42
Total
$150.42

No decision committed yet.

DecisionActions handlers should return a typed envelope, then commit in the commit phase.

Action-Centric Components (Intentional Exception)

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.

Shared Embedded Contract

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: OptionListSelection
  • ParameterSlider: SliderValue[]
  • PreferencesPanel: PreferencesValue

Component Examples + Mock Outputs

Each example below shows the live component on the left and the corresponding mock runtime output on the right.

OptionList

Mock output

onAction(actionId, state)

{
  "actionId": "confirm",
  "state": "merge"
}

ParameterSlider

Exposure+0.2 EV
Contrast+12 %

Mock output

onAction(actionId, state)

{
  "actionId": "apply",
  "state": [
    { "id": "exposure", "value": 0.2 },
    { "id": "contrast", "value": 12 }
  ]
}

PreferencesPanel

Notification Preferences

Notifications

Receive product announcements and feature updates.

How often we send summary emails.

Mock output

onAction(actionId, state)

{
  "actionId": "save",
  "state": {
    "marketing-email": true,
    "digest-frequency": "weekly"
  }
}