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: the sidebar's Inbox → Approvals — the approval checklists waiting on you (the Inbox count sums exactly what its perspectives show), each row's checklist with its approval context; Governance → Reviews & approvals keeps the record — Campaigns, Cadences and the Decided tab for the history; Governance → Policies & owners holds the policies themselves. A pending row's Evidence — prior runs opens the runs of the same action on the same pinned content on any target — or the first-run-anywhere flag when there are none — the evidence informs your decision and never gates it.
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-governanceentitlement (apply licences as inopschain 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_approvaldefaults 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_referenceand 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_approvalin every list, a checklist opened, nothing driven, no runner, no queue slot. - The
--rationaleis 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
Inbox → Approvals (an approval notification lands on the checklist
itself, no longer on the audit page), and the GUI's Inbox sidebar
entry counts the checklists waiting on you into its red bubble, beside
the steps waiting on you.
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 page carries the same history on its Decided tab (the waiting checklists live on Inbox → Approvals): 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
In the GUI the same lever is Break-glass release on the Inbox → Approvals row — shown to superusers and to principals holding the change-governance determination at the root scope — with the mandatory reason and a confirmation that names the event it lands; the server's refusal renders on the dialog verbatim.
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 listshow your policy's row with the path pattern that names your environment — theapi:approval_policies:createevent the Academy'sfirst-approval-policyexercise reads? - Does
opschain approvals listshow the runpending, and after the second identity approves, does the record show the release —audit:approvals:pendingthenaudit:approvals:releaseon your project, the pair thefunnel-heldexercise 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 academyruns thefirst-approval-policyandfunnel-heldexercises 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_windowholds for maintenance windows, and break-glass is the single, loud, reasoned exception. opschain approvals history --mineis your decision record; the governance administrator'sopschain approvals historyis everyone's.opschain approvals exportis 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).