Skip to content

for-each

for-each - execute a mini state machine for each item in a list

state-name:
tool: for-each
arguments:
items-from: <command>
on-empty: <continue|fail>
on-iteration-error: <continue|stop>
start-at: <first-state>
states:
<state-name>:
tool: <tool>
arguments:
...
next: <next-state>

The for-each tool iterates over a list of items (one per line) from a command’s output and executes a nested state machine for each item. This enables processing multiple files, records, or values without writing shell loops.

Within the loop body, two special variables are available:

  • {{ item }} - The current item value
  • {{ index }} - The zero-based iteration index

The nested states support the same features as top-level states: tool execution, output capture, goto branching, and error handling.

ArgumentRequiredDefaultDescription
items-fromYes-Shell command that outputs items (one per line)
on-emptyNocontinueBehavior when no items: continue (skip) or fail
on-iteration-errorNostopBehavior on error: stop (abort) or continue

The states block defines a mini state machine. Each state supports:

FieldDescription
toolThe tool to execute
argumentsTool arguments (with variable substitution)
outputOutput capture (var(name) or file(path))
nextNext state to transition to
gotoConditional branching based on output
on-errorError handling with specific exit codes
CodeMeaning
0All iterations completed successfully
1items-from command failed, empty list with on-empty: fail, or iteration error with on-iteration-error: stop
-1Workflow paused during iteration
process-files:
tool: for-each
arguments:
items-from: find ./src -name "*.ts" -type f
start-at: lint-file
states:
lint-file:
tool: bash
arguments:
command: eslint "{{ item }}"
rename-files:
tool: for-each
arguments:
items-from: ls *.jpg
start-at: rename
states:
rename:
tool: bash
arguments:
command: mv "{{ item }}" "photo_{{ index }}.jpg"
deploy-services:
tool: for-each
arguments:
items-from: cat services.txt
start-at: build
states:
build:
tool: bash
arguments:
command: docker build -t "{{ item }}:latest" "./{{ item }}"
next: push
push:
tool: bash
arguments:
command: docker push "{{ item }}:latest"
next: deploy
deploy:
tool: bash
arguments:
command: kubectl rollout restart deployment/{{ item }}
process-branches:
tool: for-each
arguments:
items-from: git branch -r --format='%(refname:short)'
start-at: check-branch
states:
check-branch:
tool: if-equal
arguments:
left: "{{ item }}"
right: "origin/main"
goto:
true: skip-main
false: process-branch
skip-main:
tool: bash
arguments:
command: echo "Skipping main branch"
process-branch:
tool: bash
arguments:
command: echo "Processing branch {{ item }}"
migrate-databases:
tool: for-each
arguments:
items-from: cat databases.txt
on-iteration-error: continue
start-at: migrate
states:
migrate:
tool: bash
arguments:
command: ./migrate.sh "{{ item }}"
on-error:
_: log-error
log-error:
tool: bash
arguments:
command: echo "Failed to migrate {{ item }}" >> errors.log
analyze-repos:
tool: for-each
arguments:
items-from: cat repos.txt
start-at: get-stats
states:
get-stats:
tool: bash
arguments:
command: 'cd "{{ item }}" && git log --oneline | wc -l'
output: var(commit_count)
next: report
report:
tool: bash
arguments:
command: echo "{{ item }}: {{ commit_count }} commits"
process-errors:
tool: for-each
arguments:
items-from: grep -l "ERROR" logs/*.log 2>/dev/null || true
on-empty: continue
start-at: analyze
states:
analyze:
tool: bash
arguments:
command: echo "Analyzing {{ item }}"
  • The items-from command runs in a non-interactive shell
  • Interactive tools cannot be used within the loop states
  • The pause tool can be used but will pause the entire workflow
  • Variables set inside the loop are available after the loop completes (last value wins)
batch-process:
tool: for-each
arguments:
items-from: "ls data/*.json | head -100"
start-at: process
states:
process:
tool: bash
arguments:
command: ./process-json.sh "{{ item }}"

For independent operations, consider using parallel instead:

# Sequential with for-each
process-sequential:
tool: for-each
arguments:
items-from: echo -e "a\nb\nc"
start-at: task
states:
task:
tool: bash
arguments:
command: ./slow-task.sh "{{ item }}"
# Parallel alternative (faster but unordered)
process-parallel:
tool: parallel
tasks:
task-a:
tool: bash
arguments:
command: ./slow-task.sh a
task-b:
tool: bash
arguments:
command: ./slow-task.sh b
task-c:
tool: bash
arguments:
command: ./slow-task.sh c