Agentic Workforce ME Developer PortalDocs 1.1 · Widget 0.1.0

Backend integration

Human-in-the-loop approvals

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.

1. Declare what needs a human

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": "…",                     omit for tenant-wide
  "tool_pattern": "orders.refund",     exact | "orders.*" | "*"  (also "acme-orders.orders.refund";
                                        a2a:<slug>.<tool> and connector:<slug>.<tool> target those sources)
  "source": "mcp",
  "risk": "high",
  "required_approvals": 1,
  "approver_role": "admin",
  "sla_minutes": 1440,
  "escalate_to_role": "owner",
  "enabled": true
}
FieldMeaning
tool_patternExact tool name, <server-slug>.<tool>, prefix.* or *. Most specific wins: agent-scoped over tenant-wide, source-pinned over any source, exact over prefix over *.
sourcemcp, builtin, a2a, connector, or omit to match any source.
risklow · medium · high — a label carried on the approval and the webhook, useful for routing in your UI.
required_approvalsDistinct approvers needed before the tool runs. A second approval must come from a different principal.
approver_rolemember · admin · owner · manager (a node-roles manager on the agent’s org chain; admins and owners also satisfy it).
sla_minutesHow long the request stays pending (default 1440 = 24 h). On expiry the approval becomes `expired` and the run resumes as if rejected.
escalate_to_roleWhen 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.

2. How a request reaches you

  • Webhookapproval.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.
  • SSEapproval.required then run.waiting on the run’s event stream, if you are streaming it.
  • APIGET /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.
in your webhook handlerTypeScript
case 'approval.requested': {
  const { approval_id, run_id, tool, risk, expires_at } = event.data;
  await db.pendingApprovals.upsert({ approval_id, run_id, tool, risk, expires_at });
  await notify('#refunds', `Approval needed: ${tool} on run ${run_id} (expires ${expires_at})`);
  break;
}

3. Decide

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.

actionEffect
approveExecute the tool call with the original arguments. Terminal once required_approvals distinct approvers approved.
editExecute with edited_args (re-validated against the tool’s JSON Schema). Counts like an approve.
rejectTerminal immediately. The model receives a failed tool result { rejected: true, feedback } and may adapt or finish.
respondTerminal 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.

4. Decide from your own UI — through your backend

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 offers
  try {
    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 instanceof HiveApiError && err.code === 'ALREADY_DECIDED') return res.status(409).json({ code: err.code });
    throw err;
  }
});

Rules for the proxy

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. Audit twice. The platform records the decision under the key; record the human in your own log with the approval id.
  6. Handle races. Someone may decide in the console first — treat 409 ALREADY_DECIDED as a benign refresh.

5. Timeouts, expiry and escalation

  • 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.

Endpoints

GET/v1/approvalsPending (or historical) approvals the caller is allowed to decide.
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)
Errors
UNAUTHORIZED TENANT_REQUIRED VALIDATION

Request

FieldTypeDescription
statusstring?pending, approved, edited, rejected, responded, expired, cancelled.
run_id / agent_iduuid?Narrow to one run or agent.
risk`low` | `medium` | `high` | `critical`?From the matching approval policy.
  • An API key acts as admin: it sees items assigned to member, admin and manager, not to owner.
GET/v1/approvals/:idOne approval with the requested tool call (redacted args) and the per-approver decision ledger.
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)
Errors
UNAUTHORIZED TENANT_REQUIRED NOT_FOUND
POST/v1/approvals/:id/decisionDecide. A terminal decision resumes the paused run.
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)
Errors
UNAUTHORIZED TENANT_REQUIRED FORBIDDEN VALIDATION NOT_FOUND ALREADY_DECIDED

Request

FieldTypeDescription
action`approve` | `edit` | `reject` | `respond`See the table below.
edited_argsobjectRequired for edit; re-validated against the tool schema.
feedbackstring?Up to 4000 chars; fed to the model on reject/respond.

Response

FieldTypeDescription
statusstringapproved, rejected, … or still pending when more approvers are required.
approvals_so_farnumberDistinct approvers so far vs required_approvals.
  • Audited as approval.decide. Deciding twice (or an already-terminal item) is 409 ALREADY_DECIDED; a role below assignee_role is 403 FORBIDDEN.
POST/v1/approvals/bulk-decisionApprove, reject or respond to many approvals; each is decided in its own transaction.
Auth
API key + X-Tenant-Id (any member; an API key acts as admin)
Errors
UNAUTHORIZED TENANT_REQUIRED VALIDATION

Request

FieldTypeDescription
idsuuid[]Approval ids.
action`approve` | `reject` | `respond`No edit in bulk.
feedbackstring?Applied to every item.

Response

FieldTypeDescription
results[]{ id, ok, terminal?, error? }Per-id outcome.
POST/v1/approval-policies201Make a tool privileged: calls matching the policy pause for human approval.
Auth
API key + X-Tenant-Id (tenant admin)
Errors
UNAUTHORIZED TENANT_REQUIRED FORBIDDEN VALIDATION

Request

FieldTypeDescription
namestringDisplay name.
tool_patternstringExact (orders.refund, acme-orders.orders.refund), orders.*, or *.
source`mcp` | `builtin` | `a2a` | `connector`?Pin to a tool source.
agent_iduuid?Agent-scoped (wins over tenant-wide).
risk`low` | `medium` | `high` | `critical`Shown to approvers and on webhooks.
required_approvals1–5Distinct approvers needed before the run resumes.
approver_role`owner` | `admin` | `manager` | `member`Who may decide (default admin).
sla_minutesnumber?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_rolerole?Re-assign when the SLA window is mostly consumed.
  • Most-specific match wins: agent-scoped > tenant-wide, source-pinned > any source, exact tool > prefix.* > *.