Advanced Matrix Builds
Go beyond a simple grid with include, exclude, and dynamic matrices generated at runtime.
You already know a matrix runs a job across combinations of values. The senior techniques are shaping that grid precisely and even building it on the fly.
include and exclude
- exclude — remove specific combinations from the grid (e.g. skip an old Node version on Windows).
- include — add extra properties to matching combinations, or append one-off entries that aren't part of the cross-product.
fail-fast and max-parallel
fail-fast: false lets every combination finish so you see all failures, not just the first. max-parallel caps concurrency when you want to be gentle on a rate-limited service.
Dynamic matrices
A job can emit JSON as an output, and a downstream job can consume it as its matrix. This lets you generate the build grid from changed files, a config file, or an API call — the matrix is computed, not hard-coded.
Example
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
max-parallel: 4
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
exclude:
- os: windows-latest
node: 18 # skip this combo
include:
- os: ubuntu-latest
node: 22
experimental: true # extra flag for one cell
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm testWhen to use it
- A library maintainer uses fail-fast: false so a failing Node 18 run does not cancel the Node 20 and 22 runs still in progress.
- A team uses matrix includes to inject environment-specific variables into individual matrix combinations without creating separate job definitions.
- A cross-platform CLI adds a per-OS binary extension variable to the matrix so the upload step uses the correct filename on each runner.
More examples
Disable fail-fast for full matrix coverage
fail-fast: false lets all six matrix jobs run to completion even if one fails, giving you full visibility across all combinations.
strategy:
fail-fast: false
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, windows-latest]
jobs:
test:
runs-on: ${{ matrix.os }}
steps:
- run: node --versionAdd extra variables with matrix include
The include list attaches arbitrary properties (ext, label) to each OS entry so the upload step can form the correct filename per platform.
strategy:
matrix:
include:
- os: ubuntu-latest
ext: ''
label: linux
- os: windows-latest
ext: '.exe'
label: windows
steps:
- run: mv dist/app dist/app-${{ matrix.label }}${{ matrix.ext }}
- uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.label }}
path: dist/app-${{ matrix.label }}${{ matrix.ext }}Pass max-parallel to control concurrency
max-parallel caps the simultaneous runners to three, useful when an external resource (browser pool, licence server) has limited capacity.
strategy:
matrix:
shard: [1, 2, 3, 4, 5, 6]
max-parallel: 3
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- run: npx playwright test --shard=${{ matrix.shard }}/6
Discussion