GitHub Actions Unrecognized named-value: matrix
GitHub Actions Unrecognized named-value: matrix is the expression parser refusing a name it was not given for the key you used it in. The matrix context is real and your job does have a matrix; the key you put the reference in is simply not one of the keys where that context is offered.

What this error means
The workflow is rejected before anything runs, with a message that quotes the name, gives a position inside the expression, and prints the whole expression back to you. The position is one-based and counts characters, so it points at the exact token rather than at the line. The confusing part is that the same reference works ten lines away. A job can use the matrix context in its runner label, in its name, in its environment and in every one of its steps, and be refused for using it in its own condition, because availability is decided per workflow key and not per job. The message does not say which contexts would have been allowed, so the reference table is what turns it into a one-minute fix rather than an afternoon.
Unrecognized named-value: 'matrix'. Located at position 26 within expression: contains(inputs.SCHEMAS, matrix.customer.schema)A minimal workflow that produces it
This file is written for this page and has never been run. It is the shape from actions/runner#1985, reduced: a job with a matrix that also wants to filter which matrix entries run, by testing a matrix value in its own condition.
The intent is reasonable and the syntax is fine. It is rejected because the job condition is evaluated before there is a matrix to read, which the documentation states directly: "The jobs.<job_id>.if condition is evaluated before jobs.<job_id>.strategy.matrix is applied."
jobs:
test:
runs-on: ${{ matrix.os }}
if: matrix.os == 'ubuntu-latest'
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- run: echo "ok"Common causes
The reference is in a job-level condition
The most common by a wide margin, and the case in actions/runner#1985. The job condition decides whether the job is planned at all, and that decision is made before the matrix is expanded, so there is no matrix for it to read. The condition accepts the repository and event context, the dependency context, variables and inputs, and nothing else.
The matrix is trying to reference itself
A dimension whose value is computed from another dimension in the same block. The strategy key is offered the same four contexts as the job condition, so anything self-referential is refused rather than evaluated in some order.
The reference is in a key that takes no expressions at all
The uses key of a step or of a reusable-workflow call carries no context list, so every name inside an expression there is unknown. It looks like a matrix problem and it is really a key that was never expression-capable.
The job has no matrix
The simple case, and the one to rule out first. A reference copied into a job that does not declare a strategy will be rejected wherever it sits, because the context is only offered to jobs in a matrix.
How to fix it
Move the reference to a key that offers the context
- Find your key in the left column of the table above; if it is not there, the reference cannot stay.
- For a job condition, move the test down into the steps, where the matrix context is available.
- For a runner label, a job name or an environment, no change is needed: those already accept it.
Filter the matrix instead of filtering the job
When the goal is to run a subset, say so in the matrix rather than in a condition. An exclude block removes combinations before the jobs are created, which is both allowed and cheaper than creating jobs that skip.
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: ['20', '22']
exclude:
- os: windows-latest
node: '20'
steps:
- run: node --versionGate the step rather than the job
If the decision really does depend on a matrix value, it belongs on a step. Step conditions are offered the matrix context, so the test moves down two lines and starts working.
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- name: Linux only
if: matrix.os == 'ubuntu-latest'
run: ./linux-checks.shPass the value as an input rather than into a uses key
For a reusable-workflow call, the location cannot take an expression, but the inputs can. Keep the reference for the with block and pin the workflow reference to a fixed path and ref.
call:
strategy:
matrix:
target: [staging, production]
uses: ./.github/workflows/deploy.yml
with:
target: ${{ matrix.target }}How a name becomes unrecognized
Every key in the schema carries a list of contexts that may be used inside it. When the reader meets an expression it hands that list to the expression parser as the set of legal names, and the parser rejects anything outside it. There is no separate check for the matrix context; there is one mechanism and the list is the input.
The message is assembled in ParseException.cs in actions/runner. The description comes from the kind of parse failure, the quoted token is what you wrote, and the position is the token index plus one, which is why a reference at the very start of an expression reports position 1.
The same machinery produces the same message for every other context name. actions/runner#444 is the identical failure with a different word, on a key that offers no contexts at all.
// ParseException.cs
case ParseExceptionKind.UnrecognizedNamedValue:
description = "Unrecognized named-value";
break;
Message = $"{description}: '{RawToken}'. Located at position {TokenIndex + 1} within expression: {Expression}";Where the matrix context is offered, and where it is not
The right column is short and is worth memorizing, because those six keys are where every report of this message comes from. The documentation introduces the table the left column is read from with a sentence that is the whole rule: "The listed contexts are only available for the given workflow key, and may not be used anywhere else."
The pattern behind the split is ordering. Everything that is evaluated while the run graph is being planned, before the matrix exists, is in the right column. Everything evaluated once a matrix job has been created is in the left.
| matrix is available here | matrix is refused here |
|---|---|
| jobs.<job_id>.runs-on | jobs.<job_id>.if |
| jobs.<job_id>.name | jobs.<job_id>.strategy |
| jobs.<job_id>.env and environment | concurrency at workflow level |
| jobs.<job_id>.container and services | run-name |
| jobs.<job_id>.steps.run, with, env, if, name | a step uses key |
| jobs.<job_id>.outputs and timeout-minutes | the uses key of a reusable-workflow call |
A matrix cannot be built out of itself
The second row of the right column is the one people trip over when they are generating a matrix rather than filtering one. The strategy block accepts only the contexts available before the job exists, which does not include the matrix that block is defining. An entry that refers to another entry in the same matrix is therefore rejected rather than resolved in order.
The workaround is to do the arithmetic before the job. A producer job emits the combinations already expanded, and the consumer reads them through the dependency context, which is on the allowed list for the strategy block.
jobs:
plan:
runs-on: ubuntu-latest
outputs:
combos: ${{ steps.build.outputs.combos }}
steps:
- id: build
run: echo 'combos=[{"os":"ubuntu-latest","node":"22"}]' >> "$GITHUB_OUTPUT"
test:
needs: plan
runs-on: ${{ matrix.combo.os }}
strategy:
matrix:
combo: ${{ fromJSON(needs.plan.outputs.combos) }}
steps:
- run: node --versionWhy there is no recorded run on this page
The expression parser runs while GitHub reads the workflow file, so a rejected reference never reaches a machine. There is no run, no log, and nothing a runner could have done differently, so this page carries none of those and claims no repair. Both quoted messages come from named issues in actions/runner and the workflows here are illustrative.
How to prevent it
- Keep the context availability table open whenever you write an expression above the steps list.
- Decide subsets with exclude and include rather than with a job condition.
- Generate dynamic matrices in a producer job, so the strategy block only reads a dependency output.
- Lint workflows with a tool that knows the table, so the rejection arrives on commit rather than on push.
Frequently asked questions
Why can I use matrix in runs-on but not in the job if?
Can one matrix dimension refer to another in the same block?
What does the position number in the message count?
How do I skip some matrix combinations without a job condition?
Related guides
References
- GitHub Actions: contexts reference, context availability table
- GitHub Actions: workflow syntax, jobs.<job_id>.if is evaluated before the matrix
- actions/runner#1985: Unrecognized named-value: matrix in a job if conditional
- actions/runner: ParseException.cs, where the message is assembled
- actions/runner#444: the same message at position 1, for a different name on the shell key
- GitHub Actions documentation