This episode covers matrix strategy, a way to test your application on many combinations of operating systems and programming language versions from just one job definition, complete with include, exclude, max-parallel, and fail-fast.

In episode 6 we learned to control job flow with needs and if. Now imagine an application that must run on Windows, macOS, and Linux, while supporting Node.js 18, 20, and 22. How many job definitions would you have to write? Copy-pasting the same job definition nine times is naive — besides being wasteful, every duplicate is a candidate source of bugs because one copy may be forgotten to be updated.
GitHub Actions has an elegant answer to this problem: matrix strategy. With a single job definition, you can test your application on many combinations of operating systems and programming language versions at once — and each combination runs as a separate parallel job. In this episode we'll unpack how it works along with all its customization options.
Matrix strategy is like testing product quality under various conditions before release: is this shoe still comfortable on slippery roads? In hot weather? On rocky terrain? One product, many scenarios. In GitHub Actions, those scenarios are combinations of values you define, and each combination runs as an independent runner job.
With a matrix, you get:
How to use it: under strategy at the job level, define matrix with one or more arrays of values. GitHub then creates the Cartesian product of all those arrays:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm testWith three operating systems and three Node.js versions, GitHub creates 9 combinations — and each combination is a separate runner job. The active combination's values are read via expressions, for example matrix.os to choose the runner and matrix.node-version to choose the Node version. You just write the logic once; GitHub multiplies it.
Tip
Notice that the matrix expressions above use double curly braces — that's normal inside a workflow YAML file, and indeed they may only appear inside a fenced code block like this. In the UI, GitHub displays each combination as a separate run with a label like "test (ubuntu-latest, 18)" so they're easy to tell apart.
Matrix expressions follow the matrix.<key> pattern. The key used in an expression must be exactly the same as the key defined in matrix — a typo like matrix.node when what exists is matrix.node-version will yield an empty value and the job will fail immediately. This is one of the most common sources of errors in matrix pipelines.
include adds combinations that are not produced by the Cartesian product. It can also add new keys that don't exist in the main arrays, which can then be read as matrix.<key> inside a step:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node-version: [18, 20]
include:
- os: macos-latest
node-version: 22
coverage: trueHere the base combinations are only 2 times 2, then include adds one macOS job with Node.js 22. The extra coverage key can be used in steps, for example to run a coverage report on only one combination.
exclude is the opposite — it removes combinations you don't want. Great for dropping combinations not supported by a vendor or too slow to test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20, 22]
exclude:
- os: windows-latest
node-version: 18The "Windows + Node 18" combination (if truly unsupported by the application) is immediately dropped from the run list. Important note: values in exclude must match exactly a combination that the matrix actually generates — if they don't match, it's ignored without effect.
Two matrix execution behavior controllers:
max-parallel limits how many combinations run at the same time — useful when your runner minute quota is limited or your application needs resources that shouldn't all be consumed at once.fail-fast controls behavior when one combination fails. The default is true: other combinations are cancelled immediately as soon as one fails. Set it to false if you want all combinations to keep running to completion, for example to see a complete failure report.strategy:
fail-fast: false
max-parallel: 2
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20, 22]With max-parallel: 2, of the 9 combinations only 2 run at once, the rest queue. This is a favorite pattern for teams working at peak hours that don't want their minute quota drained in an instant.
Let's combine everything into a workflow ready for team use:
name: Matrix Test
on:
push:
pull_request:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
max-parallel: 2
matrix:
os: [ubuntu-latest, windows-latest]
node-version: [18, 20, 22]
include:
- os: macos-latest
node-version: 22
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm testThis workflow produces 7 runs: 2 OS times 3 Node versions (6 combinations) plus 1 macOS combination from include. fail-fast: false ensures one failure doesn't cancel testing on other operating systems — valuable information when tracking down a bug that only appears on a specific platform.
Warning
Beware of combination explosions. Testing 3 OS times 3 Node versions times 2 dependency versions times 2 database schemas = 36 jobs, and all of them run in parallel initially. If your minute quota is limited, that many combinations can burn your allowance in a single push. Combine max-parallel, exclude, and adjust the number of variations — full cross-testing is fine on the main CI, while regular PRs only need one lean matrix.
| Mistake | Symptom | Solution |
|---|---|---|
| Typo in a matrix key name | Expression value is empty, job fails | Match the expression key exactly to the definition |
exclude doesn't match a combination | Combination still runs | Make sure values are identical to the Cartesian result |
fail-fast left true | Other combinations cancelled when one fails | Set false when you want a complete report |
| Too many combinations | Minute quota runs out fast | Use max-parallel, exclude, a lean matrix |
Matrix without include for special cases | Special job written separately, wasteful | Leverage include + extra keys |
Matrix strategy is the main weapon for multiplatform testing:
matrix produces a separate job per combination.include adds special variations and extra keys; exclude discards unwanted combinations.max-parallel controls load, fail-fast controls the fate of other combinations when one fails.matrix.<key> expressions.In the next episode 8, we'll discuss Artifacts & Caching Management — moving build results between jobs with artifacts and speeding up pipelines with dependency caching. These two capabilities make your multi-job workflows feel light and fast!