choose — readable branching
choose runs the first branch whose if: is true (or the trailing else), and
nothing else. It replaces long chains of negative if: conditions with one
readable block.
steps:
- id: risk
command: { run: "node ./score.js" }
- id: route
choose:
- if: "${{ steps.risk.output.level == 'high' }}"
steps:
- notify: { channel: pager, message: "high risk ${{ trigger.tx_hash }}" }
- if: "${{ steps.risk.output.level == 'medium' }}"
steps:
- send_transaction:
network: ethereum
relayer: ops
contract: Vault
function: "flag(bytes32)"
args: ["${{ steps.risk.output.id }}"]
approval: { via: [cli] }
- else:
steps:
- notify: { channel: ops, message: "low risk" }Rules
- Branches evaluate in order; the first true
if:wins. Only one branch runs. - At most one
else, and it must be last (validation enforces both). - Nested branch steps are ordinary journaled steps — they retry, resume and stay
exactly-once exactly like top-level steps. A
send_transactioninside a branch keeps itsexternal_iddiscipline (no double-send). - Nested steps are journaled under the stable id
<chooseId>/<branchIdx>/<stepId>(visible inrflow runs show); they reference siblings and outer steps by their declared id (steps.<id>).
Crash-safety
The branch decision is journaled before any nested step runs, so a crash mid-branch
resumes into the same branch — even if a mutable state.* / lists.* value the
if: read has since changed. Already-settled nested steps are never re-run on resume.
Limits
To keep the branch a plain journaled sequence (no goto, no fan-out), a choose branch
may not contain wait_for, foreach, or a nested choose (validation rejects
them). Every other step kind is allowed.