for-each
Expand one step or job into many by looping over a list at compile time.
Repeating the same step for a handful of services, regions, or shards means
copy-pasting it and keeping every copy in sync. for-each writes the step once
and loops over a list at compile time, stamping out one concrete step per
element. Reference the loop variable with the compile-time token {{ <var> }}
(or {{ <var>.<field> }} for object elements).
name: Deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- for-each:
var: svc
in: [api, web, worker]
steps:
- run: ./deploy.sh {{ svc }}name: Deploy
on:
- push
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh api
- run: ./deploy.sh web
- run: ./deploy.sh workername: Deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- for-each:
var: svc
in: [api, web, worker]
steps:
- run: ./deploy.sh {{ svc }}name: Deploy
on:
- push
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh api
- run: ./deploy.sh web
- run: ./deploy.sh workerParallel: fan out into a matrix
A job-level loop with parallel: true becomes a strategy.matrix instead of
inline serial steps, so each element runs as its own parallel leg. The
compile-time {{ var }} token is rewritten to the runtime ${{ matrix.var }}:
name: Test
on: [push]
jobs:
test:
runs-on: ubuntu-latest
for-each:
var: version
in: [18, 20, 22]
parallel: true
steps:
- uses: actions/setup-node@v4
with:
node-version: "{{ version }}"
- run: npm testname: Test
on:
- push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.version }}
- run: npm test
strategy:
matrix:
version:
- 18
- 20
- 22
fail-fast: falsename: Test
on: [push]
jobs:
test:
runs-on: ubuntu-latest
for-each:
var: version
in: [18, 20, 22]
parallel: true
steps:
- uses: actions/setup-node@v4
with:
node-version: "{{ version }}"
- run: npm testname: Test
on:
- push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.version }}
- run: npm test
strategy:
matrix:
version:
- 18
- 20
- 22
fail-fast: falseOptions
Prop
Type
Gotchas
Coexists with an explicit strategy.matrix
A job-level parallel loop can sit on a job that also declares its own
strategy.matrix. Instead of overwriting the matrix, Actio fans the job out into
one job per variant (test-turbopack, test-rspack, …), each carrying a clone
of the full author matrix — a variant axis × a shard matrix with clean, separate
status checks. If the loop var (or its as alias) collides with a matrix key,
the build fails with for-each-matrix-key-collision; the matrix's own fail-fast
/ max-parallel stay authoritative and any loop-level copies are ignored with a
warning.
See the for-each entry in the syntax reference for the
valid scopes and source forms.
for-each vs dynamic-matrix vs expand-matrix
All three turn one declaration into many runs; they differ by when the list is known and what shape you want:
for-each— the list is known at build time. Unrolls to inline serial steps, or withparallel:to a nativestrategy.matrix. Reach for it to stamp out repeated steps without a separate job per element.dynamic-matrix— the list isn't known until the run (a script's output: open PRs, changed packages, an API call). Stays a native runtime matrix.expand-matrix— a build-time-known matrix you want unrolled into individuallyneeds:-addressable named jobs (e.g. "deploy only after thelinux/x64leg").