Approvals & halts

What happens when work reaches a step that needs a person, who can decide, and how the run continues.

When an agent reaches a step rated above low, the run does not fail and does not proceed. It halts: the intended call is recorded with its exact arguments, nothing is executed, and the run waits for a person with approval rights. That person sees precisely the call that will be made, not a summary of it, and decides.

Two kinds of halt, one queue

Work pauses for a person in two ways, and both land in the same inbox:

Raised byWhat is waiting
Agent stepAn agent calling a tool rated moderate or high (see risk levels)The run, parked on that step
Handler requestYour own handler calling sdk.awaitApprovalThe handler, waiting for a decision on a request it raised

An approver does not need to know which kind it is. One permission, Approve paused work, covers both, and the inbox shows them side by side. The rest of this page is about agent steps; for handler requests, see sdk.awaitApproval.

What halts, and what does not

A step halts when its risk is above low and the agent is not set to auto-approve risky actions. The agent's role must already grant it: a halt is a pause on something the agent is allowed to do. A step the role does not grant is refused outright, with no halt and nothing to approve.

Auto-approve is a set of pauses an agent may skip, chosen on its page under Behavior:

  • Risky actions skips approval halts for steps above low.
  • Budget checkpoints lets it run past its effort checkpoints, up to a ceiling no setting can remove.

Each one removes one kind of pause and nothing else. Policy, authorization, payload validation and the audit trail still apply to every step.

External callers can only tighten

An autoApprove sent in an API request is never honored. An API key can instead be set to Force human approval, which halts its runs on every risky step even when the agent itself would auto-approve. A caller from outside can add friction, never remove it.

From the caller's side

Poll the run. When status is pending_approval, pendingSteps says exactly what is waiting:

{
  "ok": true,
  "status": "pending_approval",
  "runUid": "0199f1d2-4b6a-7c3e-8a1f-2d9e6b0c4a77",
  "message": "I found the order and prepared a refund of $240.00.",
  "resumable": false,
  "pendingSteps": [
    {
      "stepIndex": 4,
      "tool": "refund_order",
      "riskLevel": "high",
      "input": { "order_uid": "0199a3c2-7f1e-7b4a-9c3d-5e8f2a1b6c90", "amount_cents": 24000 }
    }
  ],
  "actions": {
    "poll": { "method": "GET", "href": "/{appUid}/agent/runs/0199f1d2-4b6a-7c3e-8a1f-2d9e6b0c4a77" },
    "message": { "method": "POST", "href": "/{appUid}/agent/runs/0199f1d2-4b6a-7c3e-8a1f-2d9e6b0c4a77/messages", "body": { "message": "" } },
    "approve": { "method": "POST", "href": "…/agent-approve-step", "auth": "approver", "body": { "targetAppUid": "…", "runUid": "0199f1d2-…", "decision": "approve" } },
    "reject": { "method": "POST", "href": "…/agent-approve-step", "auth": "approver", "body": { "targetAppUid": "…", "runUid": "0199f1d2-…", "decision": "reject" } },
    "resume": { "method": "POST", "href": "…/agent-resume-run", "auth": "approver", "body": { "targetAppUid": "…", "runUid": "0199f1d2-…" } },
    "dashboard": { "method": "GET", "href": "https://console.example.com/console/apps/…/approvals?run=0199f1d2-…" }
  }
}

A value the step would write into a redacted column is shown as [redacted] here. The stored step keeps it, so approving still writes it.

Treat this as a normal outcome

A halt is not an error and not a timeout. Expect pending_approval, slow your polling down, and hand actions.dashboard to whoever needs to decide. An agent's halt stays open until someone decides.

If the halt happened in a sub-agent the coordinator handed work to, the decision is made on the sub-agent's run. A ?wait=true invoke that ends on such a halt says "delegated": true, with its runUid and actions already pointing at the sub-agent's run, and GET /agent/runs?includeDelegated=1 lists sub-agent runs alongside your own.

Deciding, in the console

Halts appear in Govern › Approvals (every app your organization owns, plus Arbiter's own), in each app's App approvals, and inline in the Arbiter conversation that raised them.

The approvals inbox listing paused agent runs, each with Approve, Reject and Stop run controls
Every run awaiting a decision, across Arbiter and every app your organization owns.

Each entry shows the call it will make (tool, risk and arguments), the task the run was started for, the steps it has already taken, and how long it has been waiting. The earlier steps matter: three reasonable approvals in a row can add up to something nobody would have approved as one request.

A paused run showing the tool name, its risk level, and the full arguments of the call it is waiting to make
A single halted step, as an approver sees it: the arguments are shown, not summarized.
  • Approve runs that step and the run carries on. Approving is not authorization: the step still passes every policy check when it runs.
  • Reject runs nothing. You may give a reason, and you are not required to. The run then carries on with the step recorded as denied and your reason attached, so the agent answers your objection instead of retrying blindly. A caller polling the run sees the reason as that step's detail.
  • Stop run ends the run. Rejecting answers the step; stopping answers the run, for when you never want any version of what it proposed.

Two refusals are worth knowing:

CodeWhen
409 STEP_ALREADY_DECIDEDSomeone decided it first, on another screen. Refresh.
409 STALE_APPROVALYou approved a write, but the rows it would change were changed by something else since. Nothing ran; the agent reads them again and proposes it afresh.

Deciding over the API

An API key can never approve or resume its own agent's halt. The key that started a run is never the one that clears it. An automation that needs to decide uses an approver access token: a token that acts as one member of your organization for approvals only. It can approve or reject a halted step, continue a run, and read that run; anything else returns 403 TOKEN_PURPOSE_DENIED, even where the member could do it. Each decision still needs the member's own authority. Members mint their own on their page under Govern › Access › Members, in Approver access tokens. A token lasts 90 days unless you choose otherwise, and a year at most.

Use the href and body from the run's actions block, with the approver token as the bearer. Decide first, then resume. Resume after a rejection too: that is what delivers the rejection to the agent.

# Decide (use actions.approve or actions.reject from the poll)
curl -X POST "$AUTHORIC_URL$APPROVE_HREF" \
  -H "Authorization: Bearer $APPROVER_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{ \"targetAppUid\": \"$APP_UID\", \"runUid\": \"$RUN_UID\", \"decision\": \"reject\", \"note\": \"Refunds over \$200 go to finance.\" }"

# Then resume (actions.resume)
curl -X POST "$AUTHORIC_URL$RESUME_HREF" \
  -H "Authorization: Bearer $APPROVER_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{ \"targetAppUid\": \"$APP_UID\", \"runUid\": \"$RUN_UID\" }"

decision is approve or reject; note is optional and carried to the agent on a rejection.

Changing course instead

A message sent to a halted run supersedes the step it is waiting on: that step can then never run, the record says it was superseded rather than rejected, and the agent re-plans from your message. An API key may do this to its own run, because a message never approves anything; any risky step the agent proposes next halts again. See Messages to a live run.

Long runs pause too

Effort is the other reason a run stops. When an agent reaches its effort checkpoint with work left, the run reports budget_paused with resumable: true and actions.continue. Continuing takes the approver's credential, like approving, so a script cannot loop past the checkpoint on its own. With Budget checkpoints auto-approved, the agent runs on to a ceiling that is never lifted. See Runs.

What's next

On this page