LEARN · DEBUGGING GUIDE

GitHub Actions Matrix Strategy Failing – Build Jobs Not Triggering

When your workflow matrix doesn't run all expected jobs or produces wrong combinations, the problem is often in the include/exclude logic, YAML interpolation, or empty matrix dimensions.

IntermediateCI/CD7 min read

What this usually means

Matrix strategy failures generally stem from two categories: YAML parsing issues or logic errors in include/exclude. GitHub Actions evaluates matrix expressions strictly. A missing `${{ }}` around a variable, a typo in the matrix key name, or an exclude rule that accidentally matches all entries can zero out the entire matrix. Another common cause is trying to use runtime-generated values (e.g., from a previous job step) inside the matrix definition—matrix is evaluated before any job runs, so `${{ needs.<job>.outputs.<var> }}` is not available there. Empty arrays or null values in the matrix definition also cause silent skipping—GitHub does not run a job with zero matrix combinations.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 1Check the workflow run logs: look for 'Matrix: <name>' line that lists all expected combinations – if empty, the matrix definition evaluated to nothing.
  • 2Inspect the raw YAML of the workflow file from the repo – ensure no stray characters, tabs vs spaces, or missing quotes.
  • 3Use `act` locally to simulate the workflow: `act -j <job_id> -l` shows the resolved matrix (but note act may differ).
  • 4Add a debug step at the start of the matrix job: `- name: Show matrix values run: echo "${{ toJSON(matrix) }}"`
  • 5Verify that the matrix keys match exactly in `include`/`exclude` blocks – case-sensitive and extra whitespace matters.
  • 6Check that any `${{ }}` expressions inside matrix fields are valid and not referencing runtime context (like steps.outputs).
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchThe workflow YAML file in `.github/workflows/` – especially the `jobs.<job_id>.strategy.matrix` section
  • searchGitHub Actions run logs: navigate to the workflow run, click on a job, expand 'Set up job' → 'Matrix'
  • searchRepository's Actions settings: 'Settings' → 'Actions' → 'General' – ensure workflow permissions allow required scopes
  • searchGitHub status page (status.github.com) – rare but possible platform issues affecting matrix evaluation
  • searchLocal test with `act` or a separate test repo to isolate matrix logic from other workflow complexity
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningTypo in matrix key name in `include` or `exclude` block (e.g., `os: ubuntu-latest` vs `os: ubuntu-latest` with extra space)
  • warningEmpty or null list in matrix definition from a missing `${{ }}` or a returned empty array from a previous job
  • warningExclude rule that is too broad and excludes all intended combinations
  • warningUsing `fromJSON` on a string that is not valid JSON, causing the matrix to become empty
  • warningMatrix defined inside a conditional `if` block that evaluates false, skipping the entire job
  • warningUsing a variable that is not yet available (like `github.event.inputs.<name>`) in the matrix definition
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildExplicitly define matrix values with hardcoded lists first, then add `include`/`exclude` logic gradually
  • buildWrap any dynamic values in `${{ }}` and ensure they evaluate to arrays/objects, not strings
  • buildUse `fromJSON` with a fallback: `${{ fromJSON(github.event.inputs.my_input || '[]') }}`
  • buildAdd validation steps before the matrix job to print the resolved matrix – but remember matrix is evaluated before job runs, so use a separate job
  • buildReplace complex exclude logic with positive include: define only the needed combinations in `include` under an empty main list
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedCheck the workflow run's 'Matrix' section under 'Set up job' – it should list all expected combinations
  • verifiedTrigger a test run with a minimal matrix (e.g., just two OS) and confirm both appear
  • verifiedAdd a step that prints `${{ toJSON(matrix) }}` and verify the values in the logs
  • verifiedClone the repo, modify the workflow locally, and run `act` with `--matrix` flag (if supported)
  • verifiedUse the GitHub API to re-run the workflow with the same inputs and compare results
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDon't use `${{ }}` inside a matrix list item when it's already inside a template expression – this can cause double-evaluation errors
  • warningDon't assume `include` adds to the default product; it can also replace if the combination matches exactly
  • warningDon't rely on the order of combinations – GitHub may reorder them; use `include` with explicit combinations instead
  • warningDon't use `matrix` in job-level `if` conditions expecting it to filter combinations – it filters the entire job, not individual runs
  • warningDon't put sensitive secrets in matrix values – they are visible in logs and API responses
( 07 )War story

Node.js CI matrix skips half the jobs after adding an exclude rule

Backend Engineer at a mid-size SaaS companyGitHub Actions, Node.js 14/16/18, ubuntu-latest/windows-latest/macos-latest

Timeline

  1. 09:15Pushed commit adding Node 18 to the CI matrix (previously only 14 and 16). Workflow runs but only Node 14 and 16 appear; Node 18 jobs missing.
  2. 09:20Checked Actions tab: only 6 jobs (2 OS × 3 Node = 6 expected, but got 4). No failure, just missing.
  3. 09:25Inspected workflow YAML: matrix has `node: [14, 16, 18]` and `os: [ubuntu, windows, macos]`. Exclude block: `- os: macos, node: 16` (correct). But also `- os: ubuntu, node: 18` (intentional to save time).
  4. 09:30Noticed exclude block had extra spaces after commas – YAML parsed them as separate keys? No, YAML allows spaces.
  5. 09:35Used GitHub API to get workflow run details: `gh run view <id> --log | grep Matrix`. Output shows matrix combinations: only 4 entries.
  6. 09:40Realized exclude block had `ubuntu-latest` vs `ubuntu` (matrix uses `ubuntu-latest` but exclude used `ubuntu`). The exclude didn't match, so why missing jobs?
  7. 09:45Looked at raw logs: 'Matrix: node, os' line shows only two node versions. Checked YAML again – node list: `[14, 16, 18]` but 18 was on a new line with wrong indentation: it became a separate key.
  8. 09:50Fixed YAML indentation, pushed again: all 9 jobs (3 OS × 3 Node) appeared correctly. Exclude worked as intended.
  9. 10:00Added a validation job that prints the matrix to avoid future silent failures.

We had a stable CI matrix with Node 14 and 16 across Ubuntu, Windows, and macOS. I added Node 18 to the list, and also an exclude rule to skip Ubuntu+Node 18 because that combination was already tested elsewhere. Pushed, and saw only 4 jobs instead of expected 6 (after exclusions). The missing jobs were all Node 18 runs. No error message, no red X – just fewer jobs.

I spent the first 20 minutes checking the exclude logic, thinking I accidentally excluded too much. But the exclude had a typo: `ubuntu` instead of `ubuntu-latest`, so it didn't match anything. That meant the exclude block was completely ignored – so why were Node 18 jobs missing? I was confused. Then I looked at the raw YAML file and noticed that `18` was on a new line, indented incorrectly. Because of YAML's multiline list parsing, `18` was being treated as a separate key `node: [14, 16]` and then a separate mapping `18: null`. GitHub's matrix parser silently dropped the invalid entry.

The fix was trivial: correct indentation so that `18` aligns under the same list. After that, all combinations appeared. I added a debugging job that prints the matrix values at the start of each workflow run to catch similar issues early. The lesson: YAML indentation errors in matrix definitions cause silent failures – always validate with a linter or test run.

Root cause

YAML indentation error: the value `18` in the node list was not aligned with `14` and `16`, causing the matrix parser to ignore it.

The fix

Corrected indentation in the matrix node list so that all values are properly aligned as a single YAML list.

The lesson

Always use a YAML linter before committing workflow files. Matrix definitions are sensitive to YAML structure – a single misaligned entry can silently drop combinations.

( 08 )How GitHub Actions evaluates the matrix strategy

The matrix strategy is resolved at workflow parse time, before any job runs. GitHub takes the product of all list values in the matrix definition, then applies include/exclude rules. The result is a set of combination objects that become individual job runs. If the product is empty, the job is skipped entirely – no failure, just zero runs.

Key nuance: `include` can either add new combinations or override existing ones if the combination matches exactly. `exclude` removes combinations that match all given keys. Wildcard keys are not supported; each key must match exactly. Also, the matrix keys are case-sensitive and must match the names used in `include`/`exclude`.

( 09 )Common pitfalls with dynamic matrix values

Matrix values must be known at parse time. You cannot use `${{ needs.job.outputs.something }}` inside the matrix definition because matrix is evaluated before any job runs. If you need dynamic values, use a separate job to generate the matrix and then use `fromJSON` in a dependent job's matrix definition.

Another pitfall: `fromJSON` expects a valid JSON string. If the string is empty or malformed, the matrix will be empty. Always provide a fallback: `${{ fromJSON(input || '[]') }}`. Also, watch out for quoting – `fromJSON` returns an array, not a string. For example, `matrix: ${{ fromJSON('[]') }}` is correct.

( 10 )Debugging matrix failures step by step

1. Reproduce the issue with a minimal workflow. Remove all include/exclude and start with a simple product matrix. If that works, add back complexity incrementally. 2. Use the GitHub CLI to fetch the resolved matrix: `gh api repos/:owner/:repo/actions/workflows/:id/runs/:run_id/jobs` and inspect the `matrix` field. 3. Add a debug job that prints the matrix before the real jobs: create a job that echoes `${{ toJSON(needs.previous.outputs.matrix) }}` if you have a generating job. 4. Enable debug logging by setting `ACTIONS_STEP_DEBUG: true` in the workflow environment – this logs extra details about matrix resolution (though limited).

( 11 )Using `act` to simulate matrix workflows locally

The `act` tool can run GitHub Actions locally, but it has limitations with matrix strategies. Use `act -j <job_id> -l` to list the jobs that would run, which shows the resolved matrix. However, `act` may not perfectly replicate GitHub's matrix evaluation, especially with `fromJSON` or complex includes. Still, it's useful for catching obvious YAML errors. To test a specific combination, pass environment variables: `act -j build --matrix '{"os":"ubuntu-latest","node":"14"}'` (but this may not work in all versions).

( 12 )Best practices for maintainable matrix definitions

Prefer explicit `include` over complex exclude rules. Define an empty main list and add all needed combinations via `include`. This makes the intended combinations visible and reduces logic errors. For dynamic matrices, generate them in a separate job and output a JSON array, then use `fromJSON` in the dependent job. Always validate the generated matrix with a unit test in CI. Finally, use a YAML linter (like `yamllint`) in a pre-commit hook or a linting workflow to catch indentation errors immediately.

Frequently asked questions

Why does my matrix job show 0 runs even though I defined values?

This usually means the product of your matrix lists is empty. Check that all list values are non-empty arrays and correctly formatted. Common causes: a stray comma creating an empty string, using `${{ }}` that evaluates to null, or an exclude rule that removes all combinations. Also verify that the job is not skipped by an `if` condition.

Can I use GitHub Actions matrix with dynamic values from a previous job?

Yes, but only if you generate the matrix in a separate job and pass it as a JSON output. The dependent job must use `fromJSON` in its matrix definition. For example: `matrix: ${{ fromJSON(needs.generator.outputs.matrix) }}`. Remember that the generator job must run first and cannot depend on the matrix job.

My include block adds extra combinations, but some are duplicates. How does GitHub handle that?

If an include combination exactly matches an existing product combination (all keys match), it overrides that combination's values. If it introduces a new key, it merges with the existing combination. If you want to avoid duplicates, use include only with an empty main list.

Why does my workflow fail with 'Invalid workflow file' when I use matrix?

This is often a YAML syntax error. Check for missing quotes around strings that contain special characters (like `*` or `[`), improper indentation, or using reserved keywords. Run the file through a YAML validator. Also ensure that `strategy.matrix` is under a job and not at the workflow level.

How do I debug a matrix that works locally with `act` but fails on GitHub?

Differences can arise from environment variables, secrets, or GitHub-specific context (like `github.event`). `act` may not support all features (e.g., `fromJSON` with complex expressions). Compare the resolved matrix by adding a debug step that prints `${{ toJSON(matrix) }}` on both platforms. Also check GitHub's status page for platform issues.