# GitHub Actions unrecognized named-value: secrets

> GitHub Actions unrecognized named-value: secrets means the context is not offered for that key. See which keys accept it and the ways around it.

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

GitHub Actions unrecognized named-value: secrets is the expression parser telling you that the `secrets` context was not on the list of names it was given for that particular key. The list is per key rather than per workflow, so the same expression is fine in a `run` and rejected two lines above it in an `if`.

## What this error means

The run does not start. There is no job, no runner and no log to open: the Actions tab shows a red entry with no steps in it, and the message names a file, a line, a column and the expression it stopped on. Every run on that branch fails the same way and in the same few seconds, including reruns, because the file never gets as far as being scheduled. Editing anything else in the workflow changes nothing until this one expression moves.

```Quoted from Alanwe/copilot#25; that report carries a second, identical error on line 41
The workflow is not valid. .github/workflows/az-login.yml (Line: 34, Col: 13): Unrecognized named-value: 'secrets'. Located at position 1 within expression: secrets.AZURE_CREDENTIALS != ''
```

## Common causes

### A secret tested in an if condition

The dominant cause. Gating a step or a job on whether an optional secret was configured is a natural thing to want, and it is the one place the context is withheld. The documentation states it directly, and the runner enforces it by handing the parser a name list with no secrets entry.

### A secret used in runs-on, concurrency or a job name

Keys evaluated before a runner exists take a narrower set of contexts. `runs-on` allows github, inputs, vars, needs, strategy and matrix, so a self-hosted label held in a secret is rejected. This is usually a case for a repository or organization variable, which is readable in all three places.

### A secret referenced inside an action.yml

Composite and other custom actions are parsed against a separate schema, and nothing in it offers the secrets context. A third-party action that references a secret in its own manifest fails to load, and the error names the action file rather than your workflow, which sends people looking in the wrong repository.

### The context name is right but misspelled

The same message appears for any name the parser does not know, so `secret.TOKEN`, `Secrets.TOKEN` in a case-sensitive position or an entirely invented context produces identical wording with a different token quoted. Read the quoted token before assuming the restriction applies to you.

## How to fix it

### Hoist the secret into env, then test the variable

Assign the secret at job level and gate on the environment variable. This is the approach the documentation recommends for exactly this case, and it keeps the decision in the same file rather than pushing it into a script.

```.github/workflows/cd.yml (illustrative)
env:
      DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
    steps:
      - if: env.DEPLOY_TOKEN != ''
        run: ./scripts/deploy.sh
```

### Decide in a step, then gate on its output

When the decision is more than a presence test, do it inside a `run` step, where the secrets context is available, and write a plain flag to the step output file. Downstream conditions then read a step output, which every condition may do. Write the flag, never the secret.

```.github/workflows/cd.yml (illustrative)
- id: check
        run: |
          if [ -n "${{ secrets.DEPLOY_TOKEN }}" ]; then
            echo "ready=true" >> "$GITHUB_OUTPUT"
          else
            echo "ready=false" >> "$GITHUB_OUTPUT"
          fi
      - if: steps.check.outputs.ready == 'true'
        run: ./scripts/deploy.sh
```

### Use a variable where the value is not a secret

1. Ask whether the thing being tested is genuinely secret, or merely configuration such as a runner label or an environment name.
2. If it is configuration, store it as a repository or organization variable and read it through `vars`.
3. The `vars` context is offered in `runs-on`, in job and step conditions and in `concurrency`, so one value covers all of them.

### Pass the value into an action as an input

An action manifest cannot read secrets, by the design of the schema it is parsed against. Declare an input on the action and pass the secret from the workflow with `with`, which is a key where the secrets context is available. If the offending action is someone else's, this is a change that has to happen in their repository or in a fork.

```.github/workflows/cd.yml (illustrative)
- uses: ./.github/actions/publish
        with:
          token: ${{ secrets.PUBLISH_TOKEN }}
```

## How to prevent it

- Treat secrets as values to pass, and variables as values to decide with.
- Keep presence checks in `env` so the condition never names the secrets context.
- Run actionlint in CI: it knows the per-key context lists and catches this before a push.
- Read the quoted token in the message before assuming which context is at fault.

## A workflow that produces it

This file is written for this page and has never been run. It is the shape that reaches the error most often: a step that should only run when an optional secret has been configured, guarded by a test on that secret. The intent is reasonable and the syntax is well formed. It is the position that is wrong.

Move the same expression into the `run` body and it compiles, because `run` is one of the keys whose list of names includes `secrets`. Nothing about the expression changed.

```.github/workflows/cd.yml (illustrative)
name: cd
on: push

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - name: Sign the build
        if: secrets.SIGNING_KEY != ''
        run: ./scripts/sign.sh
```

## Where the message comes from

The expression compiler is handed an explicit set of allowed names for the key it is compiling. In `PipelineTemplateConverter.ConvertToIfCondition` the runner picks `s_stepNamedValues` for a step condition and `s_jobIfNamedValues` for a job condition, then calls `expressionParser.CreateTree(condition, null, namedValues, functions)`. The step list contains strategy, matrix, steps, github, inputs, job, runner, env, needs and vars. The job list contains github, needs and vars. Neither contains secrets.

When `CreateTree` meets a name that is not in the set it was given, `ExpressionParser` throws with the named-value kind, and `ParseException` assembles the sentence you see: the description, the raw token in quotes, the one-based position, and the whole expression. The file, line and column in front of it come from the template context, and the leading "The workflow is not valid." is the wrapper the workflow parser puts round the collected errors.

```actions/runner, src/Sdk/DTExpressions2/Expressions2/ExpressionParser.cs
// Named-value
case TokenKind.NamedValue:
    var name = context.Token.RawValue;
    if (context.ExtensionNamedValues.TryGetValue(name, out var namedValueInfo))
    {
        node = namedValueInfo.CreateNode();
        node.Name = name;
    }
    else if (context.AllowUnknownKeywords)
    {
        node = new NoOperationNamedValue();
        node.Name = name;
    }
    else
    {
        throw new ParseException(ParseExceptionKind.UnrecognizedNamedValue, context.Token, context.Expression);
    }
    break;
```

## Where secrets is offered and where it is not

The documented availability table is the same information from the other side, and it is worth reading as a shape rather than memorising. Secrets are offered where a value is being passed into something that will run, and withheld where a value is being used to decide whether something runs at all. The documentation states the rule for conditions outright: "Secrets cannot be directly referenced in `if:` conditionals."

The rows below are taken from that table, filtered to the keys people actually reach for. The last row is the one that surprises people most, because a composite action feels like part of your workflow: the action manifest schema has no definition anywhere whose context list includes secrets, so an expression in an action.yml cannot see them at all.

| Workflow key | `secrets` offered | What to use instead |
| --- | --- | --- |
| `env` at workflow level | yes | nothing, it already works |
| `jobs.<job_id>.env` | yes | nothing, it already works |
| `jobs.<job_id>.steps.run` | yes | nothing, it already works |
| `jobs.<job_id>.if` | no | a `vars` value, or a job output |
| `jobs.<job_id>.steps.if` | no | an `env` var set from the secret |
| `jobs.<job_id>.runs-on` | no | `vars`, which is readable there |
| an expression in `action.yml` | no | declare an input and pass it with `with` |

> Reading a secret into `env` and then testing the env var is not a way around the masking. The value is still masked in the log; what changes is only which context the expression is allowed to name.

## The corrected file

The documentation suggests the fix in the same sentence that states the restriction: set the secret as an environment variable, then test the variable. `env` at job level is one of the keys that does accept the secrets context, and `env` is on the list of names a step condition may use, so the two halves meet.

Do the assignment at job level rather than on the step, because a step's own `env` block is not in scope for that step's own `if`. That ordering catches people who move the assignment one line too far down and see the same failure with a different name in it.

```.github/workflows/cd.yml, corrected (illustrative)
jobs:
  release:
    runs-on: ubuntu-latest
    env:
      SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
    steps:
      - uses: actions/checkout@v5
      - name: Sign the build
        if: env.SIGNING_KEY != ''
        run: ./scripts/sign.sh
```

## Why there is no recorded run on this page

There is nothing to run. The file is rejected while it is being parsed, so no job is created, no runner is assigned and no log exists to capture. The only artifact the product produces is the red banner on the Actions tab, which is a rendering of the message quoted at the top of this page, and that message we took from a public repository where someone pasted it rather than manufacturing one of our own. Every part of the sentence is accounted for above by the code that assembles it, which is a stronger claim than a screenshot of our own repository would be.

## FAQ

### Why can I use a secret in run but not in if?

Because the expression compiler is given a different list of allowed names for each key. The runner passes a step-condition list containing strategy, matrix, steps, github, inputs, job, runner, env, needs and vars, with no secrets entry, while `run` is documented as accepting the secrets context.

### Is putting a secret in env a way to leak it into a condition?

No, and it does not weaken the masking either. The value is still redacted in logs. What changes is only that the expression names the `env` context, which step conditions are allowed to use, instead of the `secrets` context, which they are not.

### Why does the error name an action.yml I did not write?

Because the action manifest is parsed against its own schema, and no definition in that schema offers the secrets context. A third-party action referencing a secret in its own file fails to load, and the message names that file. The fix has to be made in the action, or in a fork of it.

### Can I use a secret in runs-on to pick a runner?

No. The context list for `runs-on` is github, inputs, vars, needs, strategy and matrix. Store the label as a repository or organization variable and read it through `vars`, which is available in that key and in conditions as well.

## References

- [GitHub Actions: contexts reference and context availability](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts)
- [GitHub Actions: using secrets, the if conditional restriction](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets)
- [actions/runner: ExpressionParser.cs, where the message is raised](https://github.com/actions/runner/blob/main/src/Sdk/DTExpressions2/Expressions2/ExpressionParser.cs)
- [Alanwe/copilot#25: the message quoted from a real run](https://github.com/Alanwe/copilot/issues/25)
- [actions/runner: action_yaml.json, the action manifest schema](https://github.com/actions/runner/blob/main/src/Runner.Worker/action_yaml.json)

---

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
