Skip to main content
An agent can stop in the middle of a run and ask a person: approve these payments, check this extracted field, confirm this email before it goes out. AgentOS calls this a hold. While the hold is open the run is 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 next seq, a new run token, and:
  • data is each item as decided: the reviewer’s full data for an edit, the item as you asked it for every other action.
  • comment on an item is the reviewer’s comment for that item. The Review queue has one comment box, which is copied onto every item decided edit, reject or feedback, so read the item’s comment.
  • When expires_in passes before anyone decides, resolved_by is "expired" and items is 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_id plus a stable step name (r_501:pay:INV-1), not by invocation.id: the invocation id changes on every resume.
  • Start child runs with an Idempotency-Key derived the same way, so a resumed run gets the same children.

With the SDKs

The SDKs create the hold and answer suspended 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).
Using LangGraph, the OpenAI Agents SDK, Agno, the Vercel AI SDK or Mastra? Their own pause (interrupt(), tools that need approval, suspend()) becomes a hold through the framework adapters.

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 in context.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.