GitHub Actions unrecognized named-value: secrets
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.
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 != ''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.
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.shCommon 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.
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
steps:
- if: env.DEPLOY_TOKEN != ''
run: ./scripts/deploy.shDecide 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.
- 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.shUse a variable where the value is not a secret
- Ask whether the thing being tested is genuinely secret, or merely configuration such as a runner label or an environment name.
- If it is configuration, store it as a repository or organization variable and read it through
vars. - The
varscontext is offered inruns-on, in job and step conditions and inconcurrency, 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.
- uses: ./.github/actions/publish
with:
token: ${{ secrets.PUBLISH_TOKEN }}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.
// 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 |
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.
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.shWhy 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.
How to prevent it
- Treat secrets as values to pass, and variables as values to decide with.
- Keep presence checks in
envso 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.
Frequently asked questions
Why can I use a secret in run but not in if?
run is documented as accepting the secrets context.Is putting a secret in env a way to leak it into a condition?
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?
Can I use a secret in runs-on to pick a runner?
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.Related guides
References
- GitHub Actions: contexts reference and context availability
- GitHub Actions: using secrets, the if conditional restriction
- actions/runner: ExpressionParser.cs, where the message is raised
- Alanwe/copilot#25: the message quoted from a real run
- actions/runner: action_yaml.json, the action manifest schema
- GitHub Actions documentation