📘 Public beta · Endpoints are stable; OpenAPI specs and SDKs ship monthly. See changelog →
Products
Orchestration
Human approvals

Human approvals

A human_approval node pauses execution until someone (analyst, MLRO, ops lead) explicitly approves or rejects. The most common shape in compliance-heavy flows: "AML hit → wait for MLRO to clear → proceed."

In the workflow spec

{
  "id": "wait_for_mlro",
  "type": "human_approval",
  "actionLabel": "AML hit on customer ${trigger.customerId} — MLRO review required",
  "assignTo": "role:mlro",
  "timeoutHours": 24,
  "onTimeout": "route_to:notify_escalation",
  "data": {
    "customerName": "${step.kyc.output.customer.fullName}",
    "screeningResults": "${step.screening.output.matches}"
  }
}
FieldNotes
actionLabelHuman-readable description that surfaces on the approver's queue. Templating allowed.
assignTorole:<role-name> · user:<userId> · team:<teamId>. Anyone in the assignment scope can act.
timeoutHoursIf no one acts within this window, onTimeout fires (or the execution fails).
dataExtra payload surfaced to the approver UI — pass any context they need to decide.

When the execution reaches this node, status becomes awaiting_approval and a workflow.human_approval_pending webhook fires. The approver acts via the dashboard or the API.

Approve via API

POST/api/executions/{id}/approve
Auth · API keyScope · executions:write
{
  "decision": "approve | reject",
  "notes": "Verified counterparty via voice call; OK to proceed."
}

notes are required (min 20 chars) and persist on the step's audit row.

Response:

{
  "data": {
    "execution": {
      "id": "exe_01HXY...",
      "status": "running",
      "steps": [ ..., { "id": "stp_...", "nodeId": "wait_for_mlro", "status": "succeeded", "output": { "decision": "approve", "notes": "...", "actorUserId": "usr_..." } } ]
    }
  }
}

The execution resumes with the next downstream node. The decision is available to downstream nodes as ${step.wait_for_mlro.output.decision} — useful when both approve and reject branches need different routing:

{
  "id": "route_after_approval",
  "type": "decision",
  "expression": "step.wait_for_mlro.output.decision",
  "cases": {
    "approve": { "next": "continue_onboarding" },
    "reject":  { "next": "notify_failure" }
  }
}

Reject

A rejection routes the same way as an approval — the difference is just the value in step.<id>.output.decision. Most workflows have a downstream decision node that routes accordingly.

If you don't want to handle rejection explicitly, configure onReject: fail on the human_approval node and the execution will fail-out cleanly with failureReason: "rejected by approver".

{
  "id": "wait_for_mlro",
  "type": "human_approval",
  ...,
  "onReject": "fail | route_to:<nodeId>"
}

Timeout behaviour

If no one acts within timeoutHours, one of:

  • onTimeout: "route_to:<nodeId>" — jump to a recovery node (escalate, notify ops, etc.).
  • onTimeout: "fail" — execution fails with failureReason: "approval timeout".

Default is fail. Most production workflows use route_to: so an escalation path is always defined.

Approver assignment

FormMeaning
role:<role-name>Anyone in the org with this role can act. E.g. role:mlro.
user:<userId>Specific person. Use when approval routing is deterministic.
team:<teamId>Anyone in the team. Use for round-robin manual queues.
dynamic:<expression>Evaluate at runtime: dynamic:step.shape.output.suggestedApproverId

Audit guarantees

Every approval action writes a row to the immutable audit log:

actorUserId · timestamp · executionId · stepId · decision · notes · ip · userAgent

The audit log is enforced by Postgres triggers — even DB-admin queries can't silently rewrite history. Export quarterly for examiner audits via the dashboard's audit-log export.

Approvals don't pause downstream side effects retroactively

If a service_call step before the approval already wrote data to a sibling product (e.g. created a customer in Identity Platform), an approval reject doesn't undo that write. Model compensation explicitly: after a rejection, add a service_call to delete the customer (or whatever rollback is appropriate). Don't assume "reject = nothing happened."

Approver UX

Approvers act from the Orchestration dashboard's Approvals queue. The queue:

  • Lists every awaiting_approval execution they're assigned (via role / team / direct).
  • Shows the actionLabel + data payload.
  • Provides Approve + Reject buttons with a required notes field.
  • Auto-refreshes — multiple approvers don't collide (first-action-wins).

For programmatic approvers (rare — usually CI bots or AI-Automation flows), the API endpoint is the same.