How approval requests reach you, deciding them via the API, proxying decisions in your own UI with a server-held key, timeouts and escalation.
Human-in-the-loop is enforced in the runtime, not in the UI. When an agent calls a tool that matches an approval policy (or a manifest HITL rule), the run is checkpointed and interrupted before anything executes; a pending approval is created with the exact tool and arguments; the run’s status becomes waiting_approval. Nothing moves until a person with the required role decides — in the console, or in your own UI through your backend.
An approval policy is tenant governance: it applies whoever edits the agent. It matches a tool pattern, optionally for one agent, and sets the risk label, how many distinct approvers must agree, which role may decide, how long the request stays open, and who to escalate to.
POST /v1/approval-policiesJSON
{"name":"Refunds need a human","agent_id":"…",←omitfortenant-wide"tool_pattern":"orders.refund",←exact|"orders.*"|"*"(also"acme-orders.orders.refund";a2a:<slug>.<tool>andconnector:<slug>.<tool>targetthosesources)"source":"mcp","risk":"high","required_approvals":1,"approver_role":"admin","sla_minutes":1440,"escalate_to_role":"owner","enabled":true}
Field
Meaning
tool_pattern
Exact tool name, <server-slug>.<tool>, prefix.* or *. Most specific wins: agent-scoped over tenant-wide, source-pinned over any source, exact over prefix over *.
source
mcp, builtin, a2a, connector, or omit to match any source.
risk
low · medium · high — a label carried on the approval and the webhook, useful for routing in your UI.
required_approvals
Distinct approvers needed before the tool runs. A second approval must come from a different principal.
approver_role
member · admin · owner · manager (a node-roles manager on the agent’s org chain; admins and owners also satisfy it).
sla_minutes
How long the request stays pending (default 1440 = 24 h). On expiry the approval becomes `expired` and the run resumes as if rejected.
escalate_to_role
When set, a still-pending request is re-assigned to this role part-way through the SLA window (deployment default: halfway) and a notification is raised.
Webhook — approval.requested with approval_id, run_id, tool, risk, assignee_role and expires_at. Subscribe your backend and upsert a pending item; notify the people who can decide.
SSE — approval.required then run.waiting on the run’s event stream, if you are streaming it.
API — GET /v1/approvals?status=pending (filter by run_id, agent_id, risk) returns the full item: the requested_action with tool, args and the tool’s JSON Schema, the model’s reasoning_summary, the matched policy, and the roles involved.
Console — the Approvals inbox, which uses the same API.
POST /v1/approvals/:id/decision with one of four actions. Every decision is recorded with the deciding principal (apikey:<id> for your backend) and audited as approval.decide; a terminal decision resumes the run through the checkpoint — the only path by which a paused run continues.
action
Effect
approve
Execute the tool call with the original arguments. Terminal once required_approvals distinct approvers approved.
edit
Execute with edited_args (re-validated against the tool’s JSON Schema). Counts like an approve.
reject
Terminal immediately. The model receives a failed tool result { rejected: true, feedback } and may adapt or finish.
respond
Terminal immediately. No tool runs; feedback is injected as a user message and the model continues.
TypeScript
const{ items }=await hive.approvals.list({status:'pending'});for(const a of items){
console.log(a.id, a.requested_action.tool, a.requested_action.args, a.risk, a.expires_at);}
TypeScript
await hive.approvals.decide(approvalId,{action:'approve'});// or, with changed arguments (re-validated against the tool schema):await hive.approvals.decide(approvalId,{action:'edit',edited_args:{order_id:'ord_1001',amount_minor:6450,reason:'Partial refund',idempotency_key:'escalation:ord_1001:1'},});// or send the model back with guidance instead of running the tool:await hive.approvals.decide(approvalId,{action:'respond',feedback:'Offer store credit first.'});
edit re-validates edited_args against the tool’s JSON Schema (422 INVALID_ARGS on failure) — a reviewer can lower a refund amount without rejecting.
Deciding an item that is no longer pending is 409 ALREADY_DECIDED; treat it as success when your decision matches the recorded one, otherwise read the approval.
POST /v1/approvals/bulk-decision applies approve, reject or respond to up to 200 ids and reports per-id results — the right tool for a morning queue.
Cancelling the run (POST /v1/runs/:id/cancel) cancels its pending approvals.
The Dubai Police portal lets duty officers approve escalations inside the portal itself. The browser never talks to Agentic Workforce ME: it calls the portal’s own API with the officer’s session, and the portal server calls the platform with a server-held privileged key. Copy the shape exactly:
routes/approvals.tsTypeScript
// Your server. The browser calls THESE routes with your own session cookie;// the privileged platform key stays here (exactly what the Dubai Police portal does).
app.get('/api/approvals',requireRole('manager'),async(_req, res)=>{const{ items }=await hive.approvals.list({status:'pending'});
res.json({items: items.map((a)=>({id: a.id,run_id: a.run_id,tool: a.requested_action.tool,args: a.requested_action.args,risk: a.risk,expires_at: a.expires_at,})),});});
app.post('/api/approvals/:id/decision',requireRole('manager'),async(req, res)=>{const action = req.body.action ==='approve'?'approve':'reject';// only what YOUR UI offerstry{const decided =await hive.approvals.decide(req.params.id,{ action,feedback: req.body.feedback });audit('approval.decided',{by: req.user.id,approval: req.params.id, action });
res.json({status: decided.status });}catch(err){if(err instanceofHiveApiError&& err.code ==='ALREADY_DECIDED')return res.status(409).json({code: err.code });throw err;}});
TypeScript
// Browser side — no platform API key anywhere hereconst res =awaitfetch('/api/approvals/'+ approvalId +'/decision',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({action:'approve'}),credentials:'same-origin',});
The key never leaves the server. Not in a bundle, not in a cookie, not in a “config” endpoint. If it ever shipped to a browser, rotate it immediately.
Your session, your roles. Gate the proxy routes with your own authentication and authorisation (the example uses a placeholder header; the demo uses the portal’s staff session). The platform only knows the key’s role, so the “who” is yours to enforce and log.
Offer only what your UI should offer. Map UI buttons to explicit actions server-side (approve/reject); do not forward an arbitrary body to the platform.
Show the real request. Render requested_action.tool and args from the platform, not your own reconstruction, so the approver sees exactly what will run.
Audit twice. The platform records the decision under the key; record the human in your own log with the approval id.
Handle races. Someone may decide in the console first — treat 409 ALREADY_DECIDED as a benign refresh.
Every approval has expires_at = creation + sla_minutes. A timer fires at expiry: if still pending, the approval becomes expired and the run resumes as if rejected (the model receives a failed tool result and may finish without the action). A manifest HITL rule can instead declare on_timeout: cancel_run, in which case the run ends cancelled and its other pending approvals are cancelled too.
With escalate_to_role, a governance sweep re-assigns still-pending requests to that role once a configurable fraction of the SLA has elapsed (0.5 by default), raises an in-console notification and audits approval.escalated. The assignee_role you read from the API changes accordingly.
required_approvals greater than one means the run stays paused after the first approval; the API response shows approvals_so_far against required_approvals, and your UI can display “1 of 2”.
The run’s run.completed / run.failed webhook fires when it finally ends, whatever path it took — so a backend that only cares about outcomes can ignore the approval lifecycle entirely and let people decide in the console.
For gates that are not tool calls — “a human must sign off before the next step” — a workflow gate or human_task node creates the same kind of approval (tool workflow:gate / workflow:human_task), decided the same way; for human_task the decision’s edited_args or feedback becomes the node’s output, and a rejection fails the workflow (GATE_REJECTED). See workflows.
Exact (orders.refund, acme-orders.orders.refund), orders.*, or *.
source
`mcp` | `builtin` | `a2a` | `connector`?
Pin to a tool source.
agent_id
uuid?
Agent-scoped (wins over tenant-wide).
risk
`low` | `medium` | `high` | `critical`
Shown to approvers and on webhooks.
required_approvals
1–5
Distinct approvers needed before the run resumes.
approver_role
`owner` | `admin` | `manager` | `member`
Who may decide (default admin).
sla_minutes
number?
Expiry window (default 1440 = 24 h). On expiry the approval becomes expired and the run resumes as if rejected, unless the manifest HITL rule says on_timeout: cancel_run.
escalate_to_role
role?
Re-assign when the SLA window is mostly consumed.
Most-specific match wins: agent-scoped > tenant-wide, source-pinned > any source, exact tool > prefix.* > *.