Actio
Macros

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).

.actio.yml
name: Deploy
on: [push]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - for-each:
          var: svc
          in: [api, web, worker]
        steps:
          - run: ./deploy.sh {{ svc }}
generated .yml
name: Deploy
on:
  - push
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh api
      - run: ./deploy.sh web
      - run: ./deploy.sh worker
.actio.yml
name: Deploy
on: [push]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - for-each:
          var: svc
          in: [api, web, worker]
        steps:
          - run: ./deploy.sh {{ svc }}
generated .yml
name: Deploy
on:
  - push
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh api
      - run: ./deploy.sh web
      - run: ./deploy.sh worker

Parallel: 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 }}:

.actio.yml
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 test
generated .yml
name: 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: false
.actio.yml
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 test
generated .yml
name: 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: false

Options

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 with parallel: to a native strategy.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 individually needs:-addressable named jobs (e.g. "deploy only after the linux/x64 leg").

On this page