gate · awaiting · approver

Pause a run for a human. In one call.

An agent calls sb.approval.request(). The run suspends. The request lands in the Studio HITL inbox, an approver decides, and the run resumes with their decision — no queue to poll, no webhook delivery to stand up. It is in the platform and the SDK.

  • one call
  • built-in inbox
  • resumes with the decision
approval.ts
const res = await sb.approval.request({  topic:    'refund.over_limit',  payload:  { orderId, amount, currency },  onDecide: '/hooks/refund-decided',})// → Result<ApprovalRequest>//   { approvalId, status: 'pending', callbackUrl, expiresAt }
envelopeapprovalIdstatus: 'pending'callbackUrlexpiresAt
studio · hitl inboxawaiting
refund.over_limit7f3a…pending · in 23h
01 · The gate lifecycle

One run, five stops. The rail and the inbox below are the same run — step the rail and the inbox row moves with it.

  1. 01requested
  2. 02suspended
  3. 03awaiting
  4. 04decided
  5. 05resumed

The request is in the Studio HITL inbox; an approver must decide. The run stays suspended.

the same run, belowstudio · hitl inbox
refund.over_limit7f3a…pending
02 · The inbox
~ studio / hitl · inboxillustration · seeded data

Pending approvals land here as job rows. The real screen is live — it syncs via SSE; this is a seeded still of it, not a live feed.

Topic Run Age Timeout Status Action
refund.over_limit 7f3a… 2m in 23h pending Decide
deploy.prod a91c… 9m in 11h suspended Decide
refund.over_limitpending
Run
7f3a…
Age
2m
Timeout
in 23h
Fallback
reject
Payloadseeded
{
  "orderId": "ord_9f3",
  "amount": 420,
  "currency": "usd"
}
Run context
  • tool·lookup_order done
  • approval·refund.over_limit awaiting
Audit log

No prior decisions.

CommentOptional reason
Approve / Reject — decide in Studio

The live screen decides here. Decide in Studio.

03 · How a decision comes back

When an approver decides, the control plane comes back to your agent — you read the decision and resume.

onDecideA path on your agent

The control plane POSTs the decision to it, resolved against the agent’s public URL server-side.

onDecide: '/hooks/refund-decided'
DecisionOne of four

On the callback you verify it before trusting it.

approved · rejected · timed_out · cancelled
timeoutDefaults to 24h

onTimeout decides for you when nobody does.

onTimeout: reject | approve | ignore
refund-decided.ts
const decision = await sb.approval.verify(request)// → Promise<Result<Decision>>

Same topic + idempotencyKey returns the same approvalId — never a duplicate. payload is required by the type

04 · Who decides
hitl:decide
ownerhitl:decideallowed
adminhitl:decideallowed
memberhitl:decidedenied
approverhitl:decideallowed

A dedicated approver role exists for compliance and stakeholders — read-only plus the HITL decision (approve / reject / edit payload). It does not run /invoke, edit config, or touch secrets. Roles are mutually exclusive.

one call out. one decision back.

05 · From the CLI · scaffolded

The verbs exist so the surface tree is stable — but they are not live yet.

stackbone hitl listscaffolded List pending tasks for the targeted agent installation.
stackbone hitl get <id>scaffolded Inspect a single task by id.
stackbone hitl approve <id> --yesscaffolded Approve a pending task. --yes is the global confirmation flag for destructive verbs, not an approve-specific arg.

Scaffolded so the surface tree is stable; every verb returns not_implemented until the runtime ships. The inbox and the SDK call are live today — decide in Studio while the CLI lands.

Wire a human into the loop.

The platform for agent developers
Write the agent.
Skip the plumbing.

© 2026 · STACKBONE BUILT WITH ❤️ FROM CANADA AND SPAIN