The step.* family (8 verbs). Every verb enters through the one front door: it is capability-checked, permission-checked, scope-checked and audited before it runs (see Security & permissions). Each entry below lists its capability, risk class (which fixes the minimum autonomy level), the minimum caller permission, its availability tier, whether it is idempotent, and any outbound egress.
Activatestep.activate
| Property | Value |
|---|---|
| Capability | matter.write |
| Risk class | Sensitive - autonomy L2 |
| Min. permission | Admin |
| Availability | Core - always on (MCP tier 1) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
PENDING→ACTIVE manual override (ADMIN). { matterId, stepId }.
Call POST /v/step.activate with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool step.activate.
Liststep.list
| Property | Value |
|---|---|
| Capability | matter.read |
| Risk class | Read - autonomy L0 |
| Min. permission | Member |
| Availability | Core - always on (MCP tier 1) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
| Render schema | Yes - drives an inline chat artefact and a GUI panel |
The workspace-wide matter-step list ([2026] CC-OPBOX 24 gate (b): surfaces
the matter_step rows the matter engine writes, which NO read verb exposed - matter.get returns only ONE
matter’s steps). Slim standing (id/matterId/stepIndex/title/stepKind/status). Access = the PARENT MATTER
is readable: the permission::readable_sql("matter") fence is evaluated INSIDE the matter subquery (so
its unqualified columns visibility/owner_id/id resolve to matter, never matter_step) - exactly the
INV-17 belt-and-braces matter.list carries. { matterId?, status? }, paginated, RLS-scoped. Pure read:
no new table/enum/write/state (the lawful gate-(b) substrate-extension, CC-OPBOX 24 obiter 3).
Call POST /v/step.list with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool step.list.
Parkstep.park
| Property | Value |
|---|---|
| Capability | matter.write |
| Risk class | Sensitive - autonomy L2 |
| Min. permission | Admin |
| Availability | Core - always on (MCP tier 1) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
park an ACTIVE step BLOCKED on a runtime failure (ADMIN, SENSITIVE). { matterId, stepId, reason? }.
The workflow engine’s block-on-failure leg: an ACTIVE step whose runtime work FAILED is parked BLOCKED
WITHOUT a completion (no completed_at stamp) and WITHOUT a cascade (no progress() call). This is the
missing peer of the step-lifecycle family and is distinct from every existing verb:
step.rejectparks BLOCKED but SETScompleted_at = now()AND runsprogress()(a forward cascade) - it treats the block as a phase-completing event; a failed-while-running step has NOT completed.step.revertparks BLOCKED with NO cascade but REQUIRES a COMPLETED step (it un-does a completion); a step.park subject is ACTIVE (it never completed), so revert’s guard refuses it.step.skipsets SKIPPED +completed_at+ cascades. None fit “an ACTIVE step failed: BLOCK it, leavecompleted_atNULL, run NO cascade so downstream stays exactly where it is”.
Modelled on step.revert (same no-cascade shape): the step must be ACTIVE (else Conflict), the matter must
be non-terminal (guard_non_terminal, INV-8), engine-owned cross-matter waits are refused, the write is
workspace/CAS-scoped, an INV-9 0-rows belt guards a phantom audit. SENSITIVE/ADMIN (a trust-boundary
lifecycle override, the same tier as step.reject/revert/reopen/skip); idempotent
Call POST /v/step.park with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool step.park.
Rejectstep.reject
| Property | Value |
|---|---|
| Capability | matter.write |
| Risk class | Sensitive - autonomy L2 |
| Min. permission | Admin |
| Availability | Core - always on (MCP tier 1) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
park a step BLOCKED with a reason (ADMIN). { matterId, stepId, reason? }.
Call POST /v/step.reject with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool step.reject.
Reopenstep.reopen
| Property | Value |
|---|---|
| Capability | matter.write |
| Risk class | Sensitive - autonomy L2 |
| Min. permission | Admin |
| Availability | Core - always on (MCP tier 1) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
a terminal step → ACTIVE, DATA-PRESERVING (COMPLEX reopen is first-class,
ADR-0012). { matterId, stepId, resetDownstreamRange? }. ADMIN. Clears completed_at; keeps data.
resetDownstreamRange (optional bool, default false = the original behavior, BACK-COMPAT, [2026]
VJS-CC-OPBOX 60): when TRUE, reopening also RESETS the lane-isolated downstream range. After the same
terminal-target guard, every LANE-ISOLATED step with step_index > target.step_index in the matter is
reset to PENDING (status=PENDING, activated_at=NULL, completed_at=NULL - DATA-PRESERVING, the data
column is NEVER cleared), the target is ACTIVATED, then the ONE progression engine runs ONCE (the same
progress() / next-candidate path the engine uses on advance - never a second hand-written progression).
This MIRRORS matter.goto’s backward-reset (goto_skip_or_reset backward branch + the progress engine):
“lane-isolated” = the SAME (container_id, lane_id) scope goto resets, so reopening one lane does not
disturb a parallel lane. Without the flag, reopen is the original single-step data-preserving reactivate.
Call POST /v/step.reopen with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool step.reopen.
Revertstep.revert
| Property | Value |
|---|---|
| Capability | matter.write |
| Risk class | Sensitive - autonomy L2 |
| Min. permission | Admin |
| Availability | Core - always on (MCP tier 1) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
UN-COMPLETE a just-completed step (ADMIN, SENSITIVE). { matterId, stepId, reason? }.
[2026] VJS-CC-OPBOX 97 (group step-lifecycle, SITE 1). The coverage gate completes a step, then discovers the step failed its required-coverage check AFTER the fact, so the completion must be UNDONE: the step returns COMPLETED -> BLOCKED, the completion stamp (completed_at + completed_by) is CLEARED, and - the whole point of this verb vs step.reject - there is NO cascade (no progress() call). This is distinct from every existing step verb: step.reject sets completed_at = now() (it does NOT clear it) AND runs progress() (a forward cascade a revert must not do); step.reopen goes terminal -> ACTIVE (not -> BLOCKED) and may reset a downstream range; step.skip sets SKIPPED + completed_at + cascades. “Un-complete back to BLOCKED, clear the completion stamp, NO cascade” has no other verb. SENSITIVE/ADMIN (a trust-boundary lifecycle override, the same tier as step.reject/reopen/skip); idempotent
(the COMPLETED guard refuses a re-revert).Call POST /v/step.revert with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool step.revert.
Set Configstep.setConfig
| Property | Value |
|---|---|
| Capability | matter.write |
| Risk class | Sensitive - autonomy L2 |
| Min. permission | Member |
| Availability | Core - always on (MCP tier 1) |
| Idempotent | Yes |
| Egress | None - in-box (pure Postgres, no outbound call) |
The matters-engine reconfigure path ([2026] VJS-CC-OPBOX 60).
{ matterId, stepId, config, expectConfigCrc? }. An idempotent CAS PATCH of the engine-authored
matter_step.config JSONB: it overwrites the WHOLE config (+ updated_at) for the named step, optionally
gated on expectConfigCrc matching a config_crc of the CURRENT stored config (fail-closed Conflict on
mismatch = a concurrent write landed between the caller’s read and this write). Status is UNTOUCHED:
config is the AUTHORED step shape, ORTHOGONAL to the PENDING/ACTIVE/…/terminal state machine, so a
reconfigure never advances/reopens/blocks the step. This is DISTINCT from the rejected generic
step.setData (CC-OPBOX 54 HEAD 2 refused it): config is authored, data is runtime - the engine
reads config for visibility/onComplete/typed-step shape, while data is the caller’s runtime payload.
Guarded non-terminal (a terminal matter is immutable, INV-8); ONE INV-8 event at dispatch level.
Call POST /v/step.setConfig with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool step.setConfig.
Skipstep.skip
| Property | Value |
|---|---|
| Capability | matter.write |
| Risk class | Sensitive - autonomy L2 |
| Min. permission | Admin |
| Availability | Core - always on (MCP tier 1) |
| Idempotent | No |
| Egress | None - in-box (pure Postgres, no outbound call) |
→SKIPPED audited override (ADMIN). { matterId, stepId }. INV-3: completedAt set.
Call POST /v/step.skip with a JSON body; returns JSON (or { error, code }). Also exposed as the MCP tool step.skip.