SECTOR 03 // REFERENCE

Step envelope

The one step shape, every reserved key, exactly what it does, in evaluation order.

Every step everywhere (sections, phases, composed-provider bodies) is the same shape. The verb is a value, never a key:

- run: <provider>.<verb>        # required (or an unprefixed language builtin)
  with: <scalar | list | map>   # args, validated against the verb's arg spec
  as: <name>                    # capture the whole result value
  read: <jq expr>               # transform the result value first
  capture: { <name>: <jq expr>, ... }   # named captures from the result
  desc: <text>                  # narrative; used in reports and failure messages
  when: <jq predicate>          # value-gated SKIP: falsey skips the step
  onAbsent: skip                # tri-state SKIP instead of failure
  skipReason: <text>            # rendered when skipped
  finding: <text>               # mark an expected failure (assertion-kind checks only)
  kind: action|probe|assertion  # override, for usage-dependent verbs (exec.run)
  effect: outage|degradation    # declare a fault when injecting it via a polymorphic verb

Unknown keys are a parse error, not ignored.

Key semantics #

keydetail
runresolved against the configured providers’ verb specs, or the builtin table when unprefixed. Unresolvable ⇒ failure (or SKIP under onAbsent: skip)
witha scalar or list binds to the verb’s primary arg (with: worker-awith: { service: worker-a }); a map is checked key-by-key: unknown and missing-required args are errors
readjq over the result value, applied before as:/capture:. The result’s other side-channels are bound as jq variables $meta and $output, so a transform can reach a status code that is not in the value: $meta.status. Real jq: .state, .items | length
asbinds the result as an Observation envelope {value, output, meta} (the post-read payload under .value, plus meta.durationMs and provider facts) into the scenario-global capture scope. Read it with jq: ${.outputs.name.value}, ${.outputs.name.meta.durationMs}
captureeach entry binds name := jq(expr, value) over the payload, with $meta/$output also in scope (code: "$meta.status"), for plucking several fields at once
whena jq predicate over the scope (when: "${.outputs.n > 1}"); evaluated first, before resolution. Falsey ⇒ the step is SKIP (with skipReason). jq truthiness applies: only false and null are falsey. This is a guard, not a branch — there is no then/else and no nested body. A when:-guarded exactly-once assertion does not satisfy the recovery invariant (validate rule 7 still fires)
onAbsent: skipwhen resolution or interpolation fails, the check becomes SKIP with skipReason instead of FAIL
findinginverts the contract: failure ⇒ FINDING (green), success ⇒ FAILED with “promote this”. Allowed only on assertion-kind checks (validate rule 5)
kindoverrides the verb’s declared kind for this step. In practice: exec.run defaults to action; mark a read-only script kind: probe so dry-run and steadyState treat it correctly
effectdeclares the fault this step injects (outage drops/blocks work, degradation slows it). Native fault verbs already carry it; set it on a step when the fault rides a polymorphic verb (exec.run running tc/iptables, or http.post to a chaos endpoint) so fault tracking and validate’s recovery rule see it
timeoutoptional per-step deadline in seconds; the step FAILs and is marked timed-out if the verb runs longer (the verb’s context is cancelled)

Evaluation order #

when (skip if falsey) → resolve run → interpolate with → bind args → execute verb
                      → read → as / capture → verdict (finding logic last)

Flow style #

YAML flow mappings (- { run: docker.kill, with: worker-a }) parse identically and are accepted everywhere, but block style is the documented convention: it stays readable once with: nests.

- run: docker.kill
  with: worker-a
- run: app.submit
  with:
    job: sleep
  as: job