Overview
Vigilo's approval system turns "who needs to sign off on this change?" from a Slack thread into a deterministic, auditable workflow. Each ChangeRequest that enters the review state produces one or more ApprovalStep rows. The change can only advance to approved once every step has been decided. Policies, SLAs, segregation of duties, and CAB-as-gate are all driven from a single workspace-scoped configuration.
The approval queue lives at /ws/{slug}/approvals. Approvers see their pending decisions, the SLA countdown chip, the requesting engineer, the affected assets, and one-click Approve / Reject actions. Bulk approve and bulk reject ship on the toolbar for batched CAB-day decisions.
Why it exists
Auditors want two things: who signed off, and when. A free-form Slack approval gives them neither. The ApprovalStep model writes both, plus the SLA window the decision was made within, plus the policy that produced the step, plus the comment the approver left. This is the artifact that survives an audit.
Key concepts
- ApprovalPolicy — Workspace-scoped rule. Has
priority(lower wins, use 10/20/30 so you can insert between later),is_active, aconditionsJSON blob, and astagesJSON array. Stored uniquely per(workspace, name). - ApprovalPolicy.conditions — Any subset of
change_type[],priority[],risk_level[],min_affected_assets,min_risk_score,tags_any[]. Missing keys mean "don't check that dimension". A policymatches(change)only when every declared condition is satisfied. - ApprovalPolicy.stages — Ordered list of
{order, kind, role|cab, sla_hours}.kind="role"picks the first eligible workspace member with that role (respecting OOO delegation and SoD).kind="cab"produces a step that waits for a CAB meeting decision. - ApprovalStep — One gate on one change. Status is
pending,approved,rejected, orskipped. Carriesapprover,order,decision_at,comments,stage_type,cab_meeting,sla_due_at,warned_at,breached_at, and a back-reference to the originatingpolicy. - SLA window —
sla_due_atis set from the policy'ssla_hoursat step creation. The Celery sweepvigilo.approvals.sla_breach_sweepwarns 4 hours before breach (settingwarned_at) and emits abreachevent after the deadline (settingbreached_at). Both fields are checked first so each step warns and breaches exactly once (T1.1). - Segregation of duties (SoD) — When
Workspace.enforce_sodisTrue, a step where the picked approver matcheschange.requested_byis silently dropped at creation. If every candidate step is dropped this way, the change still lands inreviewso an admin can intervene out-of-band rather than the submit hard-failing.
Common workflows
Approving a pending change
- Open Approvals in the sidebar. Your pending steps are at the top, sorted by
sla_due_atascending so anything close to breach surfaces first. Each row shows the SLA countdown chip (T2.3) — green > 24h, amber 24h to 4h, red < 4h or already breached. - Click a row to expand the change preview: title, description, affected assets, risk score, similar past changes, and the full approval chain so far.
- Click Approve (with an optional comment) or Reject (comment required). The decision stamps
decision_at, setsstatus, and either advances the chain to the next step or — if this was the last step — callschange.approve()on the FSM. Rejection terminates the chain and callschange.reject().
Bulk approving
- From the approvals list, tick the checkboxes on multiple rows.
- Click Approve selected. A single dialog lets you attach the same comment to all decisions. Each step is approved individually so SoD and per-step audit entries still apply. Rows you don't have permission for are skipped with a banner explaining why.
Configuring an approval policy
- Navigate to Settings → Approvals.
- Click New policy. Give it a name (unique per workspace) and a priority. Use 10 for catch-all most-restrictive, 90 for fallback most-permissive.
- In the conditions section, pick the dimensions you want to gate on. Leave others blank for "don't care".
- In the stages section, add an ordered list of gates. For role gates, pick the role and SLA hours. For CAB gates, pick
caband the SLA hours — the actualCABMeetingis bound when the change is added to a meeting agenda. - Save. New
submit()calls will route through the policy. Existing in-flight changes keep the steps they already have.
Watching SLA escalation
The vigilo.approvals.sla_breach_sweep Celery task runs every 5 minutes. For each pending step with a non-null sla_due_at:
- If now is between
(sla_due_at - 4h)andsla_due_atandwarned_atis null, dispatch anapproval.sla_warningevent and stampwarned_at. - If now is past
sla_due_atandbreached_atis null, dispatchapproval.sla_breachedand stampbreached_at.
Both events flow through dispatch_event, so webhooks, Slack, and Playbooks fire on the same trigger.
Permissions
- Viewers can read the approvals list but see no action buttons.
- Approvers (workspace
role='approver'orchanges.approvepermission) can act on steps assigned to them or to a role they cover via OOO delegation (UserProfile.effective_approver()). - Admins / owners can re-route a stuck step to a different approver and edit policies.
- SoD is enforced for everyone — an approver cannot approve a change they requested, regardless of role.
Troubleshooting
The change shows "blocked by SLA" — The first pending step is past sla_due_at. The change is still in review; the SLA breach is informational and surfaces in dashboards and webhooks. Approve the step or reassign it.
My step doesn't appear in my queue — Either it's not yours (the picker chose a different eligible member), or you're the requester (SoD dropped it). Ask an admin to reassign via the step's row menu.
The policy I configured isn't firing — A higher-precedence policy is matching first. Check Settings → Approvals sorted by priority and verify the conditions on each policy above yours against the change.
Bulk approve skipped some rows — You don't have permission on those steps, or they were CAB steps awaiting a meeting decision (CAB steps cannot be approved individually).