Skip to content
Latchkey LogoLatchkey home

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.

Workflow keys split by whether the matrix context is offered there, and the six that refuse it
The two-column table on the right is reproduced in the body of this page. Its left column and the first four rows of its right column are read from the context availability table in the GitHub docs.

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.

Actions annotation, quoted from actions/runner#1985
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."

.github/workflows/ci.yml (illustrative)
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

  1. Find your key in the left column of the table above; if it is not there, the reference cannot stay.
  2. For a job condition, move the test down into the steps, where the matrix context is available.
  3. 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.

.github/workflows/ci.yml (illustrative)
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: ['20', '22']
        exclude:
          - os: windows-latest
            node: '20'
    steps:
      - run: node --version

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

.github/workflows/ci.yml (illustrative)
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
    steps:
      - name: Linux only
        if: matrix.os == 'ubuntu-latest'
        run: ./linux-checks.sh

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

.github/workflows/release.yml (illustrative)
  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.

actions/runner, Expressions2/ParseException.cs
// 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 herematrix is refused here
jobs.<job_id>.runs-onjobs.<job_id>.if
jobs.<job_id>.namejobs.<job_id>.strategy
jobs.<job_id>.env and environmentconcurrency at workflow level
jobs.<job_id>.container and servicesrun-name
jobs.<job_id>.steps.run, with, env, if, namea step uses key
jobs.<job_id>.outputs and timeout-minutesthe 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.

.github/workflows/ci.yml (illustrative)
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 --version

Why 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?
Because the two keys are evaluated at different moments. The condition decides whether the job is planned, which happens before the matrix is expanded; the runner label is resolved once a matrix job exists. The documentation states the ordering directly, and the contexts table reflects it by offering matrix to one key and not the other.
Can one matrix dimension refer to another in the same block?
No. The strategy key is offered only the contexts that exist before the job does, which are the repository and event context, the dependency context, variables and inputs. Anything self-referential inside the block is rejected as an unrecognized name rather than resolved in some order.
What does the position number in the message count?
Characters within the expression, starting at one. It is the token index plus one, taken from the expression parser, so it lands on the first character of the offending name. The expression printed after it is the whole expression as the parser saw it, which is useful when the original was spread over several lines.
How do I skip some matrix combinations without a job condition?
Use exclude to remove combinations before the jobs exist, or build the list in a producer job and hand the consumer only the entries you want. Both keep the decision in a place where the values are already known, so nothing has to read a context that is not offered yet.

Related guides

References

Skipping one leg saves one rate. Changing the runner saves it on every leg. Latchkey, $0.0025/min. Start free → 30-day trial · No credit card