Skip to main content
Version: v5.6.1

10.1 Approval policies and the change funnel

packs:change-governance

After this you can: write an approval policy, propose a change it holds, decide it with a quorum, read the decision history, and export the evidence.

Before you start — tick each one:

  • Lesson 3.1 — Your first change (opschain learn 3.1)
  • Lesson 7.2 — Users, groups, rules, policies (opschain learn 7.2)

In the GUI: Governance → Reviews & approvals — the inbox (the sidebar badge counts what waits on you), each row's checklist with its approval context, and the Decided tab for the history; Governance → Policies & owners holds the policies themselves.

Base OpsChain lets workflow authors ask for approval (the legacy requires_approval_from step key, lesson 4.1 — still enforced, but superseded by what this lesson covers). Change governance inverts the authority: a change manager imposes approval on a path, and no workflow author can code around it — CAB-style approvals as policy, not authoring. Workflows still carrying the step key convert with opschain approval-policies convert-step-gates (the YAML is never edited). The model reference is opschain docs change-governance; the RPC surface is ChangeGovernanceService on opschain docs api-v2.

Licence entitlement. Everything here requires the packs:change-governance entitlement (apply licences as in opschain docs entitlements). On lapse, strict policies refuse covered creates by name; non-strict matches degrade loudly (warn:approval_policies:degraded) and the create passes ungated.

This lesson follows the Lime Group demo estate (opschain-governance-demo: personas jessica the requester, sarah the CAB approver, robert the strict-line approver, ryan the change governance administrator). Any estate works — substitute your own paths and groups. Where a step shows an output, yours differs only in ids and timestamps.

1. Hold the policy-writer role​

Policy writes require the delegable change-governance determination (updatable on /governance/change<scope>) plus an elevated session — never mere admin status. Ryan's grant, as an admin:

cat > change-governance-admins.yaml <<'YAML'
name: Change governance administrators
description: Owns approval policies and break-glass
authorisation_rules:
- path: /governance/change
readable: true
updatable: true
YAML
opschain admin authz policies create -f change-governance-admins.yaml
opschain admin authz policies assign "Change governance administrators" --user ryan

2. Write a policy (the matrix row)​

An ApprovalPolicy is one row of your change matrix: a path pattern, optional action-name globs and metadata predicates choosing what it covers, then who must approve and under what discipline. As ryan (in an elevated session — opschain learn 7.4):

opschain approval-policies create --name 'Production change control' \
--path-pattern '/projects/limebank/environments/prod/**' \
--approver-group release_managers --quorum 1
approval policy "Production change control" created (01a2f4c0-7d1e-7b3a-9c2e-5f0a1b2c3d4e) — path /projects/limebank/environments/prod/**, quorum 1
opschain approval-policies list
NAME PATH PATTERN QUORUM STRICT WINDOW STEP-UP ENABLED
Production change control /projects/limebank/environments/prod/** 1 - - - yes

The levers, briefly (opschain docs change-governance has all of them):

  • action_patterns — name globs; empty covers every action.
  • metadata_predicates — the selector grammar over effective node metadata; a predicate-carrying policy matches nothing on an unclassified estate (fail closed — classify first, lesson 11.1).
  • quorum — counts distinct identities; self_approval defaults false: neither the requester's nor the change owner's approval ever counts.
  • strict — never subsumed, and refuses covered creates on entitlement lapse.
  • require_window — covered subjects start only inside an open admitting maintenance window (lesson 5.6).
  • require_step_up — a matched create demands an elevated session before any approval gate is evaluated.
  • require_change_reference and the two clocks (quorum TTL, default 7d; approval validity, default 14d).

3. Propose — the funnel holds​

As jessica, run covered work against production:

opschain runs create workflow limebank/quarterly-security-patch -i reason=CHG-1042 \
--rationale "Quarterly patch cycle, CHG-1042: 3 CVEs open on core_banking"
created workflow 01a2f4c1-1e9b-7f10-8a6d-0b1c2d3e4f50 (quarterly-security-patch on /projects/limebank) — pending_approval
opschain approvals set-metadata <run-id> --category normal --ref JSM:change:CHG-1042:implements
governance block updated on 01a2f4c1-1e9b-7f10-8a6d-0b1c2d3e4f50 — owner jessica, category normal, 1 external reference(s)
  • The run is created normally — its step tree freezes and a content fingerprint is taken — then HELD: pending_approval in every list, a checklist opened, nothing driven, no runner, no queue slot.
  • The --rationale is optional for a person and mandatory for an agent; it is what the approver reads first (lesson 10.3).
  • The governance block (set-metadata: change owner, category, typed external references) is mutable through the whole lifecycle, audited with before/after, and deliberately never fingerprinted. The GUI authors the same block: every inbox row renders its category badge and external references, and the pencil beside the badge opens the metadata editor (category plus structured reference rows) for the requester, the change owner and superusers — everyone else reads it; the server authorises every write.
  • Children a held run's parent spawns never re-gate; every retry re-gates as a new subject. Nothing grandfathers.

Jessica trying to wave her own change through is refused by name:

opschain approvals approve <run-id>
Error: self-approval is off for this checklist - neither the requester nor the change owner may approve

4. Decide — approve or reject​

As sarah (a release_managers member), you do not have to go looking: the moment the run is held, every approver the inbox would flag it waiting on you to gets an in-app notification — "Change awaiting your approval: … — requested by jessica" — that opens the inbox, and the GUI's Reviews & approvals sidebar entry carries the number of checklists waiting on you as a red bubble (the same bubble Manage activity raises for waiting steps). Jessica, as the requester, is never told about her own request.

opschain approvals list
RUN KIND TARGET ACTION STATUS REQUESTED BY WHY WAITING ON YOU
01a2f4c1-1e9b-7f10-8a6d-0b1c2d3e4f50 workflow /projects/limebank quarterly-security-patch pending_approval jessica Quarterly patch cycle, CHG-1042: 3 CVEs… yes

The inbox row carries the rationale's first line in its WHY column; approvals show prints the whole checklist — the frozen line, the quorum deadline, and the approval context (who raised it, the rationale verbatim, the chain it belongs to). Lesson 10.3 reads that context as an approver; here, decide:

opschain approvals show <run-id>
opschain approvals approve <run-id> --comment "CAB 2026-10-27, CHG-1042"
approval recorded — the checklist completed and 01a2f4c1-1e9b-7f10-8a6d-0b1c2d3e4f50 was released to run

When a line is still short of quorum the same verb says so instead (the checklist on … is still pending further approvals), and when a require_window line holds the subject it names the hold (the checklist completed; … is held: …).

That holds for the subject itself, too. When the inbox asks you to decide an agent's trust raise — sarah is the approver on Felix's SUPERVISED patch grant in the demo — click the row's subject (or the context panel's "agent" / "trust review" link): the agent sheet opens in place with Felix's identity, disclosures, release and trust review, including the pending grant's tier, scope, actions and caps, because you are an approver on its open checklist — not because you hold the /governance/agents reviewer role (you do not, and the agents governance page stays closed to you). A quiet line on the sheet says so. You see that one agent and nothing else of the fleet, and the admission lapses once the checklist is decided. The same applies to a release promotion's release and a transport grant (the agent walk is opschain learn 12.2).

Rejection finalises the subject as rejected with a mandatory reason and sweeps its frozen tree aborted:

opschain approvals reject <run-id-2> --reason "wrong window - resubmit for Tuesday"
rejected — 01a2f4c3-5a7e-7c22-b1d0-6e7f8a9b0c1d finalised as rejected: wrong window - resubmit for Tuesday

The clocks decide too: an unanswered checklist times out at the quorum TTL (finalised rejected, audited); a completed checklist whose validity lapses re-gates — every approval row voids and the subject returns to pending_approval.

Your decision history​

A decision is never lost in the inbox's past. Sarah's "what did I approve last month?" is one verb — her own decisions (approved, rejected, break-glassed), newest first, each with when, the subject, and her note:

opschain approvals history --mine
WHEN STATUS KIND TARGET ACTION RUN DECIDED BY NOTE
2026-10-27T09:12:40Z approved workflow /projects/limebank quarterly-security-patch 01a2f4c1-1e9b-7f10-8a6d-0b1c2d3e4f50 sarah CAB 2026-10-27, CHG-1042
2026-10-27T09:10:02Z rejected workflow /projects/limebank quarterly-security-patch 01a2f4c3-5a7e-7c22-b1d0-6e7f8a9b0c1d sarah wrong window - resubmit for Tuesday
opschain approvals history --mine --status rejected --since 30d
opschain approvals history --mine -o json # the full checklists ride along

The GUI's Reviews & approvals → Approvals inbox page carries the same history on its Decided tab beside the Inbox tab: the Mine toggle (on by default for an approver) narrows it to your decisions, the Decided by box names another decider, the Outcome select picks approved / rejected / timed out / break-glass, and every row expands to the full checklist — each line's approvers with their rows (who, when, comment) and, for a rejection or break-glass, the reason. An approver sees what they decided even after their read on the target is withdrawn: the row is theirs.

Oversight: who decided what​

Ryan, the change governance administrator, has the opposite question — not "what did I approve?" but "have approvals been performed, by whom?". The oversight determination is the role's own rule path: readable on /governance/change (the grant from step 1; updatable on /admin counts too) lists every decision in the estate regardless of target read and regardless of being named on a line — sarah's, robert's and the asset owners' alike:

opschain approvals history # everything, newest first
opschain approvals history --decided-by sarah # one approver's record
opschain approvals history --target /projects/limebank/environments/prod --since 7d

In the GUI the Mine toggle starts off for the governance administrator, so the Decided tab opens on the whole history with each row's deciders named; the Decided by filter narrows it to one person. opschain docs cli/approvals-history has every flag.

5. Release — approved is not run-now​

Checklist completion attempts release through the maintenance gate. A require_window line holds the subject until an open window admits its declared work, and the refusal names the next scheduled window:

opschain approvals start <run-id>
opschain maintenance start /projects/limebank/environments/prod \
--reason "Tuesday prod patch window" \
--allow-workflow quarterly-security-patch --allow-action patch_preflight \
--allow-action patch_apply --allow-action patch_report

Held subjects release automatically on the next sweep tick once a window opens — or explicitly via opschain approvals start.

Break-glass is the only discretionary lever: the change-governance determination on the subject's target, a mandatory reason, and the loud warn:approvals:break_glass event (it also implies the audited maintenance override — and is deliberately not on the step-up floor):

opschain approvals break-glass <run-id> \
--reason "SEV1 INC-2041 - payments gateway degraded"
break-glass release recorded — 01a2f4c1-1e9b-7f10-8a6d-0b1c2d3e4f50, checklist break_glass

6. Evidence​

The submission packet renders from authoritative rows — fingerprint-stamped, inputs only in their masked recorded form:

opschain approvals export <run-id> > submission.md
head -12 submission.md
# Change submission — workflow 01a2f4c1-1e9b-7f10-8a6d-0b1c2d3e4f50

**Fingerprint:** `sha256:9f2c…e41b`

## Subject

- Kind: workflow
- Target: /projects/limebank
- Action: quarterly-security-patch
- Project: /projects/limebank
- Requested by: jessica
- Status: success

The packet continues with the governance block (owner, category, each external reference), the approval checklist (status, clocks, every approval row with who, when and comment) and the subject's audit slice. Every funnel transition is an event:

opschain audit --type-prefix audit:approvals:
WHEN TYPE USER SOURCE NODE CORRELATION
2026-10-27T09:12:40Z audit:approvals:approve sarah workflow quarterly-security-patch /projects/limebank 01a2f4c1-1e9b-7f10-8a6d-0b1c2d3e4f50
2026-10-27T09:12:40Z audit:approvals:release - workflow quarterly-security-patch /projects/limebank 01a2f4c1-1e9b-7f10-8a6d-0b1c2d3e4f50
2026-10-27T09:04:11Z audit:approvals:pending jessica workflow quarterly-security-patch /projects/limebank 01a2f4c1-1e9b-7f10-8a6d-0b1c2d3e4f50

The family is audit:approvals:pending, approve, reject, timeout, lapsed, release, and warn:approvals:break_glass. The full catalogue is on opschain docs events.

Check yourself​

  • Does opschain approval-policies list show your policy's row with the path pattern that names your environment — the api:approval_policies:create event the Academy's first-approval-policy exercise reads?
  • Does opschain approvals list show the run pending, and after the second identity approves, does the record show the release — audit:approvals:pending then audit:approvals:release on your project, the pair the funnel-held exercise reads?
  • What state does a held change sit in from the moment it is created until its checklist completes, and which command renders the fingerprint-stamped packet that carries every decision on it?
  • Verified version: opschain academy runs the first-approval-policy and funnel-held exercises against this lesson.

What you learned​

  • A policy is an imposed matrix row — path, actions, predicates, approvers, quorum — written by the delegable change-governance role under step-up, never by editing anyone's workflow.
  • A covered create freezes, fingerprints and holds as pending_approval; self-approval never counts; retries re-gate.
  • Approved is not run-now: require_window holds for maintenance windows, and break-glass is the single, loud, reasoned exception.
  • opschain approvals history --mine is your decision record; the governance administrator's opschain approvals history is everyone's.
  • opschain approvals export is the CAB packet, generated from rows.

Next: lesson 10.2 — Asset owners and impacted-owner approval (opschain learn 10.2), or back to your track (opschain learn).