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.resolvepermission and a role named inroles(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.enabledis true andwhenholds, the run continues without a person and the decision is recorded aspolicy:auto_approve. - Forcing a person: when
always_require_whenholds, 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'sui.approval_card.editableare 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).