The step family
Included from Basic tier
A step is one node of a run's execution tree. Two families share the shape:
change steps (JSON:API type step, owned by a change) and workflow
steps (one JSON:API type per stored STI class — the snake-cased class name,
so workflow_noop_step, workflow_change_step, workflow_wait_step,
workflow_workflow_step). This page covers all of them. Note that wait is
not a JSON:API type: wait semantics surface as the step_type attribute
on step and as the workflow_wait_step type on workflow steps.
The v2 OpenAPI document declares this resource's schema; the resource-field coverage gate holds this page to it.
Endpoints
| Route | Serves |
|---|---|
GET /api/steps | Change steps, created_at desc then id desc. Default and max limit 1000; 206 + meta.partial_response when trimmed. |
GET /api/steps/{id} | Show. Default include=pre_properties_versions,post_properties_versions,properties. |
POST /api/steps/{id}/approve / /continue / /reject | The wait/approval gates (below). |
GET /api/steps/{id}/log_lines, /artefacts (alias /artifacts), /converged_properties, /converged_settings | Per-step surfaces; the converged documents resolve to the step's change. |
GET /api/workflow_steps | Workflow steps, ordered (parent_id, step_sequence) — the model default scope. Default and max limit 1000. |
GET /api/workflow_steps/{id}, /log_lines, and the same /approve//continue//reject gates | The workflow-step twins. |
Both lists accept filter[<attribute>_eq] over the serialized attributes
plus one scope filter: filter[waiting_for_user]=<username> — steps whose
raw status is waiting and that either store no pending approval
requirements or name the user (or one of their LDAP groups) in a
requirement's user_names/ldap_groups, matched case-insensitively. Sparse
fieldsets (fields[step]=…) are honoured on both lists and shows.
step attributes (change steps)
| Attribute | Type | Meaning / origin / null semantics |
|---|---|---|
approved_by | array | Audited approve transitions ({username, message, date}, second-precision date) from the step's audit:steps:approve events. [] when none. |
rejected_by | array | As above, for audit:steps:reject. [] when none. |
continued_by | array | As above, for audit:steps:continue. [] when none. |
requires_approval_from | array/null | Stored approval requirements, each merged with its first matching approved_by response (present once requirements exist; null until matched) and, when matched, a rejected_by response. null when the step needs no approval. |
action | string | The action this step runs (colon-namespaced). |
change_id | string | The owning change's id (also the change relationship). |
child_execution_strategy | string/null | sequential/parallel child ordering; null/empty when the step has no strategy. |
full_path | string | The step's run-graph identity (what skip_steps entries match against). "" when unset. |
ignore_failure | boolean | A failure of this step does not fail the parent. |
input_arguments | array/object | The step's input arguments, stored verbatim. |
state_timing | object | Per-state timing ranges. |
step_sequence | integer | Position under its parent. |
step_type | string | Wait semantics flag — see the vocabulary below. |
description | string/null | Step description; null when unset. |
step_name | string | Display name. |
skip_requested | boolean | True when the owning change's skip_steps mask matches this step's full_path (case-folded exact, glob, or path-prefix match; Ruby extglob {a,b} alternates are a recorded gap). |
created_at / updated_at | string | Row timestamps. |
started_at / finished_at | string/null | Execution window; null until reached. |
status_code | string | The serialized step status (the upstream SQL function public.step_serialized_status_code) — vocabulary below. |
outputs | object | The step's published outputs, key → string value; absent (not null) when the step published none or the database predates the step_outputs table. Sensitive values always serialize as the redaction placeholder ******. Workflow steps carry the same attribute holding the nested run's rolled-up outputs (root-scope union, tree order, last writer wins, the implicit exit_status included), persisted on success and on a plain nested failure. |
condition_verdicts | array | The step's recorded when: evaluations, oldest first: {expression, result, operands, attempt, evaluated_at, error?} — operand values matching the run's mask set record as ******. Absent for unconditional steps. A retry's re-evaluation appends on the retried run's step. |
estate_reads | array | The estate() snapshots resolved at the step's entry: {path, versions, assets?, environments?} per referenced node, versions keyed by layer (project, environment, …) with each property document's integer version — the staleness audit. When the step's expressions enumerate, assets/environments list each RBAC-readable child as {path, version?}. The resolved values stay on the step row and never serialize in list payloads. Absent when the step reads no estate paths. Workflow steps carry the same two attributes. |
retry_attempts | array | The step's recorded automatic in-place retries (rider), oldest first: {attempt, failed_at, message?, next_attempt_at, policy_source?, nested_run_id?} — policy_source names the level that supplied the policy (step, or the settings-ladder layer: asset/environment/project/global), next_attempt_at the backoff deadline the re-dispatch honoured. Absent when the step never auto-retried. Workflow steps carry the same attribute. |
error_handled | boolean | true on a failure-path step (when: error()/always()) that executed on its failed scope. Absent otherwise. Workflow steps carry the same attribute. |
last_error_message | string | The reason recorded when the step last entered error or system_error, verbatim: a workflow step's refused nested-change creation (an action whose declarative file is invalid — the file's own validation error; an unknown or missing required input — the same text POST /api/changes would answer with), a lost runner, a timeout, a failed output roll-up. What error.message binds in an on_error: handler. Absent while the step has never failed (so a step that failed and was then retried in place keeps its last reason). The v2 Step carries it as last_error_message ("" when absent); a workflow step carries it in its attribute document. opschain runs show prints it under the failed step. |
fan_out | array | The step's recorded for_each: expansions, oldest first: {expression, as?, count, cap, cap_source, evaluated_at} — cap_source names the settings-ladder level that supplied the cap (default when none did). A retry's re-entry appends a fresh evaluation. The expansion children are ordinary steps of the tree; a template step's dynamic name: renders into each child's step_name at expansion, with the authored expression preserved on the step's graph document as name_expression; a fan-out stage's own dynamic name: renders onto each stage copy the same way, the copy's name_expression then carrying the stage's authored expression. Absent for ordinary steps. Workflow steps carry the same attribute. |
Show-only meta: property_sources (where each converged property value came
from), present only when the source supplies it — unseedable from the ports
store pre-Stage-0 (recorded gap).
step relationships and links
| Relationship | Target type | Notes |
|---|---|---|
properties | properties | The step-owned properties document. |
children | step | Child steps, step_sequence order. |
pre_properties_versions / post_properties_versions | properties_version | The properties-version pins taken before/after execution, level order. |
change | change | The owning change. |
Links: self, parent (/api/changes/{change_id}), log_lines, and
change — the owning change's node-path URL, or /api/workflow_runs/{id}
when the owning run resolves as a workflow run rather than a change.
Status vocabulary (both families)
Raw states (mirroring the upstream
StateMachine::AvailableStates):
pending, queued, running, waiting, success, skipped, error,
cancelled, aborted, system_error
system_error is an infrastructure failure rather than the action's own
verdict — a runner lost mid-step (runner lost: lease expired without a result, see opschain docs fleet-queue) lands here; the owning change
rolls up to error and the step is retried by hand.
Serialized pseudo-statuses (runstate.go SerializedStatus, mirrored by the
SQL functions): a retried step awaiting its re-run reads pending; a waiting
step with approval requirements reads waiting_for_approval; an aborted,
approval-gated, rejected step reads rejected; otherwise the raw state.
step_type vocabulary
- Postgres-backed reads emit
standardorwait_step(wait_stepwhen the action's last colon segment starts withopschain_wait_step_— the opschain-core wait-action predicate). - Ports-store-backed reads (the Go engine's own graph) emit the graph node's
kind:
standard,waitorapproval.
A waiting wait step routinely has children (it acquires them on entry to waiting), so "no children" never identifies a wait step — the status functions carry an explicit wait-step disjunct for exactly this reason.
Transition gates
POST /api/steps/{id}/approve|continue|reject (and the
/api/workflow_steps/{id}/… twins). Body attributes: message (audited,
max 1000 characters — 422 Audit message too long - cannot exceed 1000 characters beyond it) and input_arguments. Authorisation: read on the run
plus execute on the step's action path
(<node path>/actions/<action>), 404-on-deny. Refusals:
- 403
You are not permitted to <gate> this step— acontinueagainst awaitingstep that requires approval, or an approval gate the actor may not pass. - 422
Cannot <gate> step because it is in the "<status>" state— the step is not in a gateable state.
Success answers 200 with the re-read step document. Outcomes land in
approved_by/rejected_by/continued_by and the status moves accordingly
(reject on an approval gate drives the pseudo-status rejected).
Workflow step types
Workflow steps serialize per stored STI class; the wire type is the snake-cased class name. Attributes common to every type:
| Attribute | Type | Meaning |
|---|---|---|
lock_version | integer | Optimistic-lock counter of the row. |
created_by | string | The run creator's username. |
name | string | The step's name in the workflow definition. |
step_sequence | integer | Position under its parent. |
status_code | string | Serialized step status: the raw vocabulary above, with the approval pseudo-statuses applied exactly as for change steps — an approval-gated waiting step reads waiting_for_approval, an approval-gated aborted one reads rejected. |
skip_requested | boolean | Always false on this read surface (the workflow skip mask is not evaluated per step here — recorded gap). |
created_at / updated_at | string | Row timestamps. |
started_at / finished_at | string/null | Execution window; null until reached. |
child_execution_strategy | string/null | Child ordering; null when none. |
Per-type extras:
workflow_change_step— addsaction(string/null) andproperty_overrides(object,{}when none); relationshipschange(the spawned change),change_parent(the target node, typed by its node kind) andaction_template_version.workflow_workflow_step— addscode(the child workflow's code, string/null); relationshipschild_workflow_versionandchild_workflow_run.workflow_noop_step— no extra attributes beyond the common set.workflow_wait_step— adds the audited transition responses change steps carry:approved_by,rejected_byandcontinued_by(arrays of{username, message, date},[]when none), read from the step's audit events so the actor who answered the gate is on the record. An approval-gated step additionally carriesrequires_approval_from: the stored requirement object (the same shape the run'sstep_treenode carries — e.g.{"user_names": ["alice"]}), absent when the step needs no approval. Unlike the change-step attribute, matched responses are not merged into it (upstream serializes the raw column here too) — read them from the three response arrays, and the gate outcome from the serialized status (rejected).
Common relationships: workflow_run, children (each child ref typed by its
own STI wire type). Links: log_lines
(/api/workflow_steps/{id}/log_lines) and — when the owning run resolves —
self (…/runs/{run}/steps/{id}), parent and workflow_run.
Related pages
opschain docs resources/change— the owning change run.opschain docs resources/workflow_run— the owning workflow run.opschain docs resources/properties— the documents and version pins steps carry.opschain docs fleet-queue— how a ready step is dispatched to a runner.