Skip to content

Control flow

Encapsulates steps into a single unit. Internal results are hidden unless exported.

- id: gather
type: group
steps:
- id: log
type: shell
with:
command: git log --oneline -20
result: text
- id: diff
type: shell
with:
command: git diff --stat
result: text
exports:
log: ${{ steps.log.result }}
diff: ${{ steps.diff.result }}

Downstream steps access exports via steps.gather.result.log.

Repeats until a condition is met or max iterations is reached.

- id: fix_loop
type: loop
max: 5 # required
until: ${{ steps.judge.result.done }} # required
steps:
- id: review
type: codex
with:
action: review
target: uncommitted
prompt: Review changes.
- id: judge
type: claude
with:
action: prompt
prompt: Evaluate findings.
output_schema:
type: object
required: [done]
additionalProperties: false
properties:
done:
type: boolean
exports:
done: ${{ steps.judge.result.done }}

Inside loop bodies, run.* variables are available:

VariableTypeDescription
run.iterationintegerCurrent iteration (1-based)
run.max_iterationsintegerMax iterations value
run.node_pathstringNode path

Conditional execution. First matching if wins. else is the fallback.

- id: decide
type: branch
cases:
- if: ${{ steps.check.result.has_issues }}
steps:
- id: fix
type: codex
with:
action: exec
prompt: Fix the issues.
exports:
outcome: ${{ steps.fix.result }}
- else:
steps: []
exports:
outcome: "no issues"

Rules:

  • All cases must export the same shape, or none at all
  • If any case has exports, an else case is required
  • else: takes no value (just else: on its own)

Runs branches concurrently.

- id: checks
type: parallel
branches:
- id: unit # id required on each branch
steps:
- id: run_unit
type: shell
with:
command: cargo test --lib
- id: lint
steps:
- id: run_lint
type: shell
with:
command: cargo clippy
exports:
unit: ${{ steps.run_unit.result }}
lint: ${{ steps.run_lint.result }}

Exports can reference steps.* from any branch in the parallel block.

All control flow types (group, loop, branch, parallel) support exports to promote internal results to the outer scope:

exports:
name: ${{ steps.inner_step.result }}
field: ${{ steps.inner_step.result.nested_field }}

Without exports, inner step results are invisible to downstream steps.