# GitHub Actions Unrecognized named-value: matrix

> GitHub Actions Unrecognized named-value: matrix means the key you used is not on the matrix context list. Here is that list and the fix.

Source: https://latchkey.dev/learn/github-actions/github-actions-unrecognized-named-value-matrix  
Updated: 2026-09-20

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.

```Actions annotation, quoted from actions/runner#1985
Unrecognized named-value: 'matrix'. Located at position 26 within expression: contains(inputs.SCHEMAS, matrix.customer.schema)
```

## 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
```

> Remember that adding a step condition does not remove the success gate: [GitHub Actions if: always()](/learn/github-actions/gha-if-always-vs-success-misuse).

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

## 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"
```

## 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 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 |

> The left column and the first four rows of the right column are read from the context availability table in the GitHub contexts reference. The two uses rows carry no context list in the schema at all, which is why any name in them is rejected.

## 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
```

> When that generated matrix fails to expand rather than being rejected, the message comes from the evaluator instead: [GitHub Actions fromJSON matrix](/learn/github-actions/gha-fromjson-matrix-not-array).

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

## FAQ

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

## References

- [GitHub Actions: contexts reference, context availability table](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#context-availability)
- [GitHub Actions: workflow syntax, jobs.<job_id>.if is evaluated before the matrix](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idif)
- [actions/runner#1985: Unrecognized named-value: matrix in a job if conditional](https://github.com/actions/runner/issues/1985)
- [actions/runner: ParseException.cs, where the message is assembled](https://github.com/actions/runner/blob/main/src/Sdk/DTExpressions2/Expressions2/ParseException.cs)
- [actions/runner#444: the same message at position 1, for a different name on the shell key](https://github.com/actions/runner/issues/444)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
