Skip to main content

Overview

Custom actions let the frontend send structured commands (undo, rollback, edit, regenerate) that modify the conversation state. Actions are not turns: they fire hydrateMessages (if set) and onAction only. No turn lifecycle hooks (onTurnStart / prepareMessages / onBeforeTurnComplete / onTurnComplete), no run(), no turn-counter increment. The trace span is named chat action. Actions wake the agent from suspension the same way a new message does, run their handler against the latest accumulator state, and emit a trigger:turn-complete chunk so the frontend’s useChat knows the action has been applied.

Defining an action handler

Define an actionSchema for validation and an onAction handler that uses chat.history to modify state:
Lifecycle flow: Wake → parse action against actionSchemahydrateMessages (if set) → onAction → apply chat.history mutations → emit trigger:turn-complete → wait for next message.

Returning a model response from an action

onAction can return a StreamTextResult, string, or UIMessage to produce a response. The returned stream is auto-piped to the frontend just like a normal turn, but the rest of the turn machinery (onTurnStart, onTurnComplete, etc.) still does not fire.
This is useful for actions that both mutate state and want a fresh model response (regenerate-from-here, retry-with-different-style). Persistence is your responsibility inside onAction itself; you have access to the streamed response object.

Gating actions on HITL state

If you have a human-in-the-loop tool waiting on addToolOutput, you usually want to refuse competing actions like regenerate until the answer arrives. chat.history.getPendingToolCalls() gives you exactly that signal:

Sending actions from the frontend

The action payload is validated against actionSchema on the backend; invalid actions throw and surface as a stream error. The action parameter in onAction is fully typed from the schema.
For silent state changes that should never appear as a turn (e.g. injecting background context), use chat.inject() instead. Actions are explicit user-driven mutations; injections are agent-side context updates.

See also