Skip to main content

7. Approvals

- id: approval
kind: human.approval
with:
reason: confirmation # the only value allowed (see below)
roles: [approver] # who may decide; inheriting roles qualify (admin ⊃ approver)
four_eyes: true # whoever started the run may not decide it
expires_in_hours: 72 # after this the approval expires (task approval_expiry)
auto_approve:
enabled: false
when:
all:
- { key: validation_issues, op: empty }
- { key: match.score, op: gte, value: 0.95 }
- { key: invoice.total, op: lte, value: 500 }
always_require_when:
any:
- { key: match.bank_details_changed, op: eq, value: true }
  • Who may decide: the caller needs the approvals.resolve permission and a role named in roles (or one inheriting from it).
  • Four-eyes: with four_eyes: true, the user who started the run cannot decide it, whatever their roles.
  • Auto-approval: when auto_approve.enabled is true and when holds, the run continues without a person and the decision is recorded as policy:auto_approve.
  • Forcing a person: when always_require_when holds, auto-approval is skipped. A changed bank account always reaches a human.
  • Edits: a decision may carry edits: { "invoice.po_number": "PO-7A" }. Only fields listed in the workflow's ui.approval_card.editable are accepted; anything else refuses the whole decision and leaves the approval open.
  • Replays: sending the same decision again is harmless. A different decision on an approval already decided is refused.
  • Why reason: confirmation: a tool-bound interrupt would make CopilotKit invent a tool message, so boot refuses any other reason.

Each decision is stored in approvals (status, decision, decided_by, decided_at) and in the audit trail (approval.approved or approval.rejected).