Approver Chains
Multi-step and quorum approvals — one canonical shape shared by approval rules, access-review rules and the reviewer-chain registry.
One canonical shape, shared by all consumers
ApprovalRule.then_branch / else_branch and CertRule.reviewer_chain share the same JSONB schema since Sprint cert-redesign Phase 2 (migration 0133 converted legacy flat lists):
{
"steps": [
{
"sequence": 1,
"quorum": "all",
"approvers": [{"kind": "manager"}]
},
{
"sequence": 2,
"quorum": "any",
"approvers": [
{"kind": "approver_role", "value": "security"},
{"kind": "approver_role", "value": "compliance"}
]
}
]
}
Approver kinds — who becomes an approver?
manager— the subject's direct manager (per identity)manager_of_manager— the manager's managerapprover_role+ value — all members of anIGAGroupwith that role tagapprover_identity+ identity_id — a fixed, named identityiga_group+ group_id — all members of a specificIGAGroupresource_owner— owner of the entitlementapplication_owner— owner of the applicationobject_owner+{reference_kind, facet}— owner of the referenced object via the ownership registry; facet =business/technical/either/both(bothfans out to two approvers)auto_approve— the step passes automatically
Which kinds are valid for a given chain depends on its target: the step-vocabulary endpoint (GET /api/v1/reviewer-chains/step-vocabulary?target=<key>) drives the editor dropdowns — the facets come live from the ownership registry, so the vocabulary never drifts from what is resolvable.
Reviewer-chain registry — named chains + one resolver
Since Sprint reviewer-chain-registry (2026-06-10) a reviewer_chain row is a named, reusable chain in the same {steps:[…]} shape. A chain can also be the default for a key (default_for, e.g. cert:grant or approval:access_request). Every consumer (approval rules, access-review rules, role formalization) resolves through one resolver, in a fixed order:
inline override → named chain (FK) → default-per-key → manager fallback
A rule without a chain therefore always gets a reviewer (never a silent no-reviewer). Two built-in chains are seeded per tenant: rc-manager-default (default for grant / identity-validity / access-request) and rc-composition-owners (default for role / policy / ownership — the object owner for the whole object, then item owners per item). The per-step scope (whole_object / per_item) drives the composition fan-out.
Quorum — how many must approve?
-
all (AND) — every approver must approve. One reject is enough to reject the step.
-
any (OR) — the first approval wins the step. Use this for "n-of-m" with
n=1. -
n_of_m +
quorum_n— exactly n approvals are needed (out of m total). For example, 2-of-3 for high-risk grants.
Multi-step — sequence semantics
Steps are sequential — step 2 only spawns once step 1's quorum is reached. Lazy step activation happens in _advance_or_finalize:
- Step 1 quorum reject → the whole chain rejects.
- Step 1 quorum approve → tasks for step 2 are spawned.
- Tasks from the same fan-out group share a
fan_out_group_idso that group-quorum evaluation works atomically.
Delegation (out-of-office)
ApprovalDelegation (Sprint m34y) lets approvers temporarily hand over their decision-making authority. The period and scope are set per rule kind. The resolver follows active delegations at task-spawn time — the delegate receives the decision task in their inbox with a "delegated by X" badge.
Fan-out per approver
Each step spawns a separate decision task per approver, explicitly grouped via fan_out_group_id. Group quorum is evaluated on every decision; the step is complete as soon as the quorum is reached. Fan-out gives per-approver visibility plus a clean audit trail (who decided what, and when).
Further reading: