Prerequisites
- You can create policy rules through
/v1/policies. - You know which user ids will approve. Name them; an empty approver list means anyone may resolve.
- Read approvals for what the pause can and cannot cover.
API=http://localhost:8000 and a bearer token in $TOKEN.Steps
There are two routes to the same enforcement. Both end as an agent-scopedAPPROVAL policy rule that the workflow gate reads.
Route A — tick the tool in the agent configuration
Set
requires_user_confirmation on the tool inside the agent’s tools config.
On save, AgentService reconciles the flags into agent-scoped rules: each ticked
tool becomes PolicyRule(subject_type=agent, target="tool:<name>", effect=approval), and unticking deletes the row rather than disabling it.
The flag is stripped before the tools JSON is stored and reconstituted from the
rules on read, so it has exactly one home.The name in the rule is the LLM-facing one. A code toolset collapses its
namespace — agentarea/shell becomes tool:shell — while an MCP tool keeps the
raw name it advertises, which is its allowed_tools entry.This route cannot name approvers. It produces a rule with no approvers
parameter, which means anyone may approve. If that is not acceptable, use
route B, or patch the generated rule’s params afterwards.Route B — write the rule directly
Everything below is this route. Choose per-tool or global.Per tool is the usual choice. Target the tool by name and the approvers
attach to that tool alone:Global stops every capability tool call the agent makes. Reach for it only
when the agent is new or untrusted and you want a human in front of everything,
because it pauses on every single call:
target: "tool:*" behaves the same as * for this effect.Approver references must be typed. user:<id> is the only form that grants
approval at resolution time. group:<id> and userset references such as
team:eng#member are validated and stored but match nobody — see
troubleshooting.Confirm it merged
escalation_rules plus a key in
approvers_by_tool. A global rule sets requires_human_approval: true and fills
the flat approvers list.Run a task and catch the escalation
Start a task that will reach the tool, then read the event stream for the pending
request. The escalation id is in the event data:You can also stream them live from
GET /v1/agents/{agent_id}/tasks/{task_id}/events/stream.Resolve it
resolved_by field to send, and none would be honoured. Call
this endpoint with the approver’s own token.Send "approved": false to deny. The comment is appended to the conversation as
a tool message so the model can react rather than retry blindly.Verify
Before resolving, the task reports the pause:status field is the persisted task’s lifecycle state, which the workflow
updates when it pauses. execution_status in the same response is the live
Temporal view and still reads as running.
After resolving, an approval.response event carries the outcome and the
approver. Approve and deny share that one event type; the decision is in the
payload:
running.
Troubleshooting
The response said resolved but the task is still waiting
The response said resolved but the task is still waiting
This is the failure that costs the most time. The endpoint returns
Resolve again as one of the named approvers.
{"status": "resolved", ...} when the signal was delivered, not when
the approval was accepted. The workflow ignores a signal from a caller
who is not in the approver list, deliberately, so that the API boundary
cannot bypass policy. Check the worker log for:Anyone can approve
Anyone can approve
An approval rule created without an
approvers parameter produces an empty
list, and an empty list permits any caller. If the approval is meant to mean
something, name the approvers explicitly. Rules generated from the agent
configuration toggle (route A) always land in this state, because that route
has nowhere to express approvers.Unticking the toggle deleted my rule
Unticking the toggle deleted my rule
Working as designed. Route A treats the agent configuration as the source of
truth and reconciles to exactly the ticked set: missing targets are created,
unticked ones are deleted rather than disabled. If you hand-edited a
generated rule’s
params to add approvers, unticking and re-ticking loses
that edit.A hand-written rule and a toggle fight each other
A hand-written rule and a toggle fight each other
Reconciliation only considers agent-scoped rules with effect
approval , so
a rule you wrote at that same scope for a tool that is not ticked will be
deleted on the next agent save. Put hand-written approval rules at the
workspace or user scope, or manage that tool entirely through the toggle.Nobody can approve and the task hangs forever
Nobody can approve and the task hangs forever
Approver matching resolves only direct
user:<id> subjects. A list
containing solely group: or userset references matches nobody, and because
the list is non-empty the “anyone may approve” default does not apply
either. Replace them with explicit user references.The task waits indefinitely
The task waits indefinitely
There is no timeout, no auto-deny and no escalation to a second approver. A
task blocked on an approval nobody answers stays in
waiting_for_approval
until you cancel it with DELETE /v1/agents/{agent_id}/tasks/{task_id} .No notification arrived
No notification arrived
Core emits the
approval.request event and updates the task row; it does
not send mail, post to a channel or page anyone. Consume the event stream if
you need to notify approvers.A proxied MCP call was refused instead of pausing
A proxied MCP call was refused instead of pausing
Tool calls arriving at
POST /v1/mcp/{instance_id}/mcp are checked against
the same decision, but anything that is not a plain allow becomes HTTP 403
with no escalation created. Approval only works for tools invoked inside an
agent task.Setting requires_human_approval on the task did nothing
Setting requires_human_approval on the task did nothing
TaskCreate accepts a requires_human_approval boolean, and it is stored
in the task’s metadata. It does not feed the approval policy and no
enforcement point reads it. Use an approval policy rule, or send an
approval block inside task_policy .A destructive command was blocked rather than escalated
A destructive command was blocked rather than escalated
A separate pattern gate denies literal strings such as
DROP TABLE ,
rm -rf / and TRUNCATE TABLE , and marks DELETE FROM , ALTER TABLE ,
rm -rf and chmod 777 for escalation. That gate runs at the activity
boundary, which cannot pause, so its escalation fails the activity instead
of creating an approval. It is a guardrail against an accident, not part of
this flow.Related
Approvals
Why the pause lives in the workflow and not in the interceptor pipeline
Authorize a tool call
Deny, which is cheaper when the answer is always no
Review the audit trail
Where approval outcomes are, and are not, recorded