Skip to main content
A chat agent can wait for a user to supply a tool result or approve a tool before it runs. Choose the pattern based on what the user needs to provide: The examples below use AI SDK 6 or later and build on the Quick start, including its session and token server actions. For approve/deny buttons, see Approve a tool before execution.

How it works

When the model calls a tool without execute, streamText finishes with a pending tool call. Your frontend renders the question. Calling addToolOutput supplies the answer, and sendAutomaticallyWhen starts the next turn. The assistant message keeps its ID across the pause. After resuming, onTurnComplete receives the full merged responseMessage, including the question, tool result, and follow-up text.

Duration and cost while paused

After the turn finishes, the run stays active for idleTimeoutInSeconds (30 seconds by default), then suspends and releases compute. Set it to 0 to suspend immediately. The active idle window counts toward compute usage and maxDuration; suspended time doesn’t. turnTimeout controls how long a run waits for the next message (default "1h"). When it expires, the run ends. A later answer starts a continuation run that restores the conversation from transcript storage. Raising maxDuration isn’t necessary for time spent suspended.

Backend: define the tool

Define an inputSchema for the question and omit execute so the frontend can supply the answer. When the model calls askUser, streamText returns control to your agent.
trigger/my-chat.ts
Declaring tools on the config (and reading them back from the payload) is the recommended shape for any agent with tools. See Tools.

Frontend: render the question and collect the answer

Render the question when its tool part reaches input-available. Use the AI SDK’s lastAssistantMessageIsCompleteWithToolCalls helper to submit the answer automatically once every pending tool call has a result.
app/components/chat.tsx
addToolOutput patches the assistant message locally with state: "output-available" and fills in output. lastAssistantMessageIsCompleteWithToolCalls detects that every pending tool call now has a result, and useChat fires a new sendMessage. The backend picks it up as the next turn.

Choose from tool results

When a tool returns several candidates, run the search first, then ask the user to choose one before taking action. For example, a search might find several matching contacts, files, or brands. Keep the search, selection, and action as separate tool calls:
  1. A search tool with execute returns candidates and saves them under a searchId.
  2. A selection tool without execute asks the frontend to show those candidates.
  3. An action tool with execute uses the selected ID after checking it on the server.
trigger/tools/select-result.ts
Render this tool’s input-available part using the same approach as askUser above. Load the candidates from the saved search, display their original labels, and call addToolOutput with the selected ID. Return { selectedId: null } for a “None of these” option. If a search has no results, ask for a different query before requesting a selection. Save search results durably and scope them to the authorized conversation so the picker still works after a reload or continuation. Use stable candidate IDs; names can be ambiguous. Before executing the action, read the completed selection from chat.history, validate its output, and check that the requested ID matches the user’s answer and belongs to that saved search. A null answer must prevent the action. Enforce these checks in server code; tool descriptions alone don’t enforce the choice. Use the selection’s toolCallId, scoped to the chat, as the action’s idempotency key. That keeps retries of the same choice from creating duplicate work, even if the model makes another action call. If the next step must be the action, use prepareStep to set toolChoice on the first model step after a valid selection. You can also require approval on the action tool. The selection supplies an input; the approval confirms whether to execute with that input.

Approve a tool before execution

With AI SDK 6 or later, set needsApproval: true on a tool that has an execute function. The SDK requests approval before calling execute. Approving the call runs it on the next turn; denying it skips execution.
trigger/tools/send-email.ts
sendEmail is your application’s email function. Keep authorization and idempotency checks there. Declare the tool on the agent and pass it to the streamText supplied to run, as in the question example. On the frontend, render approve/deny buttons for approval-requested parts. Call addToolApprovalResponse with part.approval.id and the approved boolean, and use lastAssistantMessageIsCompleteWithApprovalResponses for automatic submission. The frontend approval example shows the full wiring. For conditional approval, needsApproval also accepts a function that checks the tool input. Approval happens before execute, so a tool that searches inside execute can’t use that approval prompt to display its search results. Run the search first, collect the selection, then call the action tool. If your agent uses both questions and approvals, combine the helpers:
app/components/chat.tsx

Detecting a paused turn in onTurnComplete

Use chat.history.getPendingToolCalls() to find unanswered tool calls on the latest assistant message:
trigger/my-chat.ts
Each entry contains toolCallId, toolName, and messageId. Approval requests use a separate state: inspect responseMessage.parts for approval-requested when tracking approvals. finishReason === "tool-calls" describes why the model stopped. It can also occur when a step limit ends a loop of server-executed tools, so check the tool states before treating it as a human pause. Check stopped separately for interrupted turns.

Acting once per net-new tool result

chat.history.extractNewToolResults(message) returns resolved tool parts whose toolCallId isn’t already resolved in the current history. Call it before the incoming message is merged. By onTurnComplete, the result is already in history and the helper returns []. This filters results against the current transcript; durable side effects still need an idempotency key. Use chatId and toolCallId to deduplicate a sync or audit write across retries. See Tool result auditing for existing hydrateMessages integrations.

Persistence: one message vs one record per pause

The default transcript storage saves the pending call and its resolved result. Use a storage adapter if you need the conversation in your own database. The pause and continuation share the assistant message ID. Upsert by message.id so the resolved message replaces the pending version. For an audit trail, keep separate snapshots alongside that canonical message and record whether it has pending questions, approval requests, or a stopped response.

Multi-pause turns

A conversation can ask another question after receiving an answer. Each pause and continuation fires onTurnComplete; inspect the message’s tool states at each callback. The merged assistant message includes the answers collected so far.

Verify the flow

Send a request that needs clarification. Confirm that the question appears, choose an option, and check that the agent continues without another click on Send. The question’s tool part should change from input-available to output-available. For a selection from search results, check that only the selected item is used. Choosing “None of these” must leave the action unexecuted. Reload a pending selection and confirm that the same candidates appear. For an approval tool, deny the first request and confirm that execute doesn’t run. Approve a fresh request and confirm that it runs with the displayed inputs.

Common mistakes

  • Give question tools no execute function. For tools that execute after approval, keep execute and set needsApproval.
  • addToolOutput updates local state. Configure sendAutomaticallyWhen or explicitly send the updated message to start the next turn.
  • Only show interactive controls for pending calls, and disable them while a request is streaming or submitted.
  • To add custom response parts, use chat.response.append() in onBeforeTurnComplete while the stream is open. Treat responseMessage in onTurnComplete as a snapshot.