Skip to main content
Flag a tool needsApproval and the run pauses before calling it, until a human (or your code) approves or rejects the call. The pause is saved in an ApprovalStore, so the decision can come minutes or days later, from another request or another process, and the run then continues where it stopped.

Which calls pause

needsApproval is true, or a predicate that receives the validated arguments, typed from the tool’s zod input:
The predicate can also deny a call or approve it only once per session; see Approve, deny or ask. When the model asks for several tools in one turn, the first call that needs approval stops the batch: the calls before it run, the run pauses on it, and the calls after it run once it is decided (see Approvals in the middle of a tool batch). MCP tools set needsApproval from the server’s tool annotations: readOnlyHint: true runs, while destructiveHint true or absent (the MCP default) asks. Choose per server with approval: 'annotations' | 'always' | 'never' or a function; see MCP tool approval.

Permission policies

permissions sets rules for the whole agent instead of tool by tool: a list of { tool, when?, action, reason? } rules, checked in order for every tool call before the tool’s own needsApproval. The first rule that matches decides:
  • allow runs the call, without approval even if needsApproval would ask. A needsApproval that denies the call still denies it.
  • deny does not run it. The model gets a tool error with kind: 'denied' and the rule’s reason (see Tool errors), and streams see tool.error.
  • ask pauses the run for approval, exactly like needsApproval (or asks the approve callback).
When no rule matches, the tool’s own needsApproval decides, as before. tool is a name, a list of names, a RegExp tested against the name, or '*' for every tool. when narrows a rule to some calls: it gets the validated arguments (after preToolCall hooks) and { toolName, toolCallId, sessionId }, and may be async; when it throws, the call fails with that error. allow(tools), deny(tools, reason?) and ask(tools) build the common rules.
onPermissionDecision is the audit log: it is called once per tool call (except calls already refused for invalid arguments) with { toolName, toolCallId, decision, rule?, args?, at }. decision is the matching rule’s action or 'default' when none matched, rule is that rule’s { index, reason? }, at is an ISO timestamp, and args is left out when the run sets redactContent. Streams get the same entry as a permission.decision event (see Streaming). Both are only produced when the agent sets permissions or onPermissionDecision. Both options also exist on AgentExecutor.execute() / stream() and resumeAfterApproval(). Sub-agents inherit the lead agent’s rules, checked before the sub-agent’s own, and report their decisions to the lead’s onPermissionDecision.

Approve, deny or ask

A needsApproval function may return more than a boolean. It gets the validated arguments and { toolName, toolCallId, sessionId, messages }, and returns (or resolves to):
  • 'ask' or true: pause for approval, as before.
  • 'approve' or false: run the call.
  • 'deny' or { deny: reason }: do not run it and do not pause. The model gets a tool error with kind: 'denied' and the reason, so it can try something else; streams see tool.error, and onPermissionDecision records decision: 'deny' with the reason (and no rule).
Three helpers cover the common policies: always() (the same as true), never() (false) and once(). once() asks the first time the tool is called in a session; once a human approves a call, later calls of that tool in the same session run without asking. A rejection is not remembered, and a new session asks again. once({ per: 'args' }) remembers approvals per tool and arguments instead, so a call with different arguments asks again. The memory lives in the transcript (the approved call’s tool message carries metadata.approval), so it is saved wherever the session, checkpoint or approval snapshot is, and holds after a resume in another process. Without a session, each send() is its own transcript. Compacting away that message makes the tool ask again.
With permissions, a deny rule always wins, and the tool’s needsApproval is not called. Otherwise the tool’s own 'deny' (or { deny }) always denies, whatever rule matched. An allow rule replaces the tool’s ask (so it skips 'ask', true and once()), and an ask rule pauses even when the tool would approve. With no matching rule, the tool’s needsApproval outcome applies. A preToolCall hook runs before all of these and can deny the call first (see Hook outcomes). MCP tools keep their annotation-derived needsApproval (a boolean). Sub-agents evaluate their tools’ needsApproval the same way, and a once() approval given through the lead agent is remembered for the rest of that sub-agent’s task.

createAgent() agents

  • A paused send() resolves (it does not throw) with finishReason: 'awaiting-approval' and an approvalId.
  • agent.approvals.list() returns the pending calls this agent paused on in this process, oldest first: { id, toolCallId, toolName, args, createdAt }, plus subagentPath when the call belongs to a sub-agent.
  • agent.approvals.resolve({ id, approved, note? }) runs the call (approved) or gives the model a rejection with your note (rejected), continues the run, and resolves with the continued run’s result, which may pause again. It throws when id is unknown or already resolved.
  • A run that paused inside agent.session() continues in that session: the tool call, its result and the final answer join the session’s transcript.
  • agent.stream() and session.stream() end at the pause with an approval.requested event and run.done ('awaiting-approval'); resolve it the same way.
Pauses are kept in a per-agent InMemoryApprovalStore unless you pass approvalStore or a store with approvals. To decide a pause after a restart, give the agent a durable store, such as the SQLite one (see Choosing a store):
list() only knows the pauses made by this agent object; keep the approvalId (or read the store) to resolve a pause from somewhere else. A continued run joins a session only when it is resolved through the agent that owns that session object.

Resuming with a changed agent

An approval snapshot carries the paused agent’s fingerprint, and resolving it with an agent whose model, tools or instructions differ warns ('warn', the default), rejects with LOUSHO_AGENT_DRIFT (onAgentDrift: 'error', the approval stays pending) or carries on ('ignore'). An approved call whose tool no longer exists always rejects with LOUSHO_RESUME_TOOL_MISSING. See Resuming with a changed agent. On a resumed approved call the pre-tool hooks run again, and the arguments they leave (whether through { input } or by changing ctx.args in place) must equal what was approved, key order aside; otherwise the call is refused with a kind: 'validation' tool error. A hook that redacts or normalizes its input should run on the paused run too, so the human approves the redacted input.

Deciding in code

Pass approve to decide each call as it comes up instead of pausing: true runs the tool, false sends the model a rejection. It applies to send(), sessions and agent.approvals.resolve(); stream() still ends at the pause.

Streaming the continued run

agent.approvals.streamResolve(decision, { signal }) and agent.approvals.streamAnswer({ id, answer }, { signal }) continue the run like resolve() and answer(), but return the AgentRun that agent.stream() returns (see Streaming): run.start, the decided call’s tool.start and tool.done (tool.error for a rejection), then the continuation’s events up to run.done. run.result is what resolve() resolves with. A continuation that pauses again ends with approval.requested and run.done ('awaiting-approval'), even with an approve callback, as stream() does. A pause made inside a session continues in that session, and run.done comes once the transcript is saved.

Asking the user a question

createAgent({ askQuestion: true }) gives the agent the built-in ask_question tool (off by default; askQuestionTool() returns the same tool to pass in tools yourself). Its input is { question: string; options?: string[]; allowFreeText?: boolean }. A call pauses the run through the approval mechanism, so it waits exactly like an approval: in sessions, in durable stores and across a restart.
  • The pending record has kind: 'question' and question: { text, options?, allowFreeText? }, in agent.approvals.list(), in the approve callback and on the approval.requested event, so a UI can render a question instead of an approve button.
  • agent.approvals.answer({ id, answer }) (the same as resolve({ id, approved: true, note: answer })) continues the run. The model gets { answer, option? }, where option is the index of the matching entry of options (case-insensitive). With allowFreeText: false, an answer outside the options reaches the model as a tool error.
  • resolve({ id, approved: false, note? }) declines: the model gets a kind: 'rejected' tool error saying the user declined to answer.
  • An approve callback may return a string to answer in code, which is handy in tests and scripted agents.
A permission rule that allows ask_question skips the pause, so the call fails with “No answer”; leave the tool to its default.

AgentExecutor and resumeAfterApproval()

With AgentExecutor.execute() directly, pass an approvalStore. On a gated call the executor persists an ExecutionSnapshot instead of invoking the tool. Resume later, after a real restart if you like, with resumeAfterApproval():
resumeAfterApproval(decision, store, registry, provider, options?, checkpointStore?) takes most of the execution options of execute() (for example signal, onAgentEvent, exporter). With sessionId + checkpointStore, the pause is also checkpointed, and execute() with that sessionId throws SessionAwaitingApprovalError until the approval is decided, so a pending approval cannot be bypassed (see Durable execution). streamResumeAfterApproval() takes the same arguments and streams the continued run as an AgentRun (see Streaming after an approval).

Elsewhere

  • Sub-agents. A gated tool inside a sub-agent pauses the lead run; the lead’s agent.approvals resolves it. See Approvals inside a sub-agent.
  • Workspace tools. The shell tool is approval-gated by default. See Workspace tools.
  • MCP. Approval-gated tools cannot be approved over MCP; see Serve an agent over MCP.
  • Agent Forge shows pending approvals as inline cards in its chat; see Agent Forge.