held: nothing runs, no token exists, and the agent’s container may even be stopped. When a reviewer decides, AgentOS invokes the same run again with the decision.
Reviewers decide in the panel’s Review queue or through the Holds API. See Review queue for the reviewer’s side.
The flow
1
Ask
With the run token,
POST {api_url}/v0/holds with what you want decided. It answers 201 {"hold_id": "h_7", "status": "pending"}.2
Suspend
Answer the invoke with
{"status": "suspended", "hold_id": "h_7"}. The run becomes held.3
Resume
Once a person decides (or the hold expires), AgentOS calls
POST /invoke again for the same run, on the same version, with invocation.kind: "resume" and the decision in resume. Your code continues and answers completed or failed as usual.Asking
An ask is at most 1 MiB (413). A wrong one is 422 with
errors naming the fields.
A run has one open hold at a time (a second ask is 409
hold_open). To get several approvals at once, put them in one hold as several items. For decisions that are independent of each other, start child runs and let each ask on its own.The resume
The resume invocation has the nextseq, a new run token, and:
datais each item as decided: the reviewer’s full data for anedit, the item as you asked it for every other action.commenton an item is the reviewer’s comment for that item. The Review queue has one comment box, which is copied onto every item decidededit,rejectorfeedback, so read the item’s comment.- When
expires_inpasses before anyone decides,resolved_byis"expired"anditemsis empty.
Write the code before the ask so it can run twice
On the resume your handler runs again from the top. Everything before the ask runs a second time, so keep it idempotent:- Keep progress in run state before you ask, and read it back on the resume.
- Key side effects that must happen once per run by
run_idplus a stable step name (r_501:pay:INV-1), not byinvocation.id: the invocation id changes on every resume. - Start child runs with an
Idempotency-Keyderived the same way, so a resumed run gets the same children.
With the SDKs
The SDKs create the hold and answersuspended for you. ask() ends the invocation; on the resume, inv.resume holds the decision.
inv.resume has approved (approved or edited items, with data as decided), edited, rejected, feedback, decided_items / decidedItems, item(id), comment, resolved_by / resolvedBy and expired. Each decided item has id, action, data and comment.
ask() also takes details, expires_in / expiresIn and files. With suspend=False (Python) it returns the hold id instead of ending the invocation, which a streaming handler uses to end its stream first (see Streaming).
Who may decide
Each capability names the roles that may decide its holds, in agentos.yaml:approvers applies to people and service accounts alike. A person who started the run may decide it too, if their role is an approver.
Holds in evals
An experiment never sends a hold to a person. Each run gets the holds recorded for its dataset item incontext.eval.recorded_holds (with the decisions people made back then); an experiment run that asks anyway ends at once, counted as “asked for a human”. Check context.eval (or inv.eval in the SDKs) to use the recorded decisions or skip the human step, and to keep side effects such as payments or emails out of eval runs.
