A GitHub Actions composite action cannot access secrets because the secrets context is not one of the names the expression parser recognizes inside an action.yml. The failure is a parse error rather than an empty value, so nothing runs at all.

The runner builds the message in ParseException as the description, the raw token in quotes, then the position and expression when a token is available.
What this error means
The run fails immediately with an annotation naming a file, a line and a column, and no job appears in the run at all. The file named is the action's action.yml, which may live in another repository entirely, so the first place you look is the workflow that called it and the workflow is fine. The message quotes the offending name in single quotes and, when the parser has a token to point at, appends the position and the whole expression.
```Actions log, quoted from BrunoSobrino/TheMystic-Bot-MD#384
[error] thomasbnt/hello-to-news-contributions/v1.0/action.yml (Line: 14, Col: 23): Unrecognized named-value: 'secrets'
## Common causes
### The action.yml references secrets directly
The literal case. A composite step uses `${{ secrets.NPM_TOKEN }}` in a `run`, an `env` or a `with`, having been written by copying a workflow step into an action. The name is valid in the file it came from and not in the file it landed in.
### A step-level or job-level if tests a secret
The same message from a plain workflow. Guarding work on whether a secret is set is a reasonable thing to want, and `if` is the obvious place to put it, but `secrets` is not available in either `if`. This is the version that appears in repositories with no composite actions at all.
### A secret is referenced in a key that does not accept it
Less common and harder to spot, because the same file uses `secrets` successfully a few lines away. Keys like `runs-on`, `strategy` and a job's `environment` do not accept the context, so one expression fails while its neighbors are fine.
### The action was refactored from a reusable workflow
Reusable workflows do accept a `secrets` block, so moving one to a composite action moves code that was correct into a file where it is not. The diff looks like a simple relocation, which is why the error arrives as a surprise.
## How to fix it
### Declare the secret as an input on the action
Add an input to the action and read it through the `inputs` context. Composite actions do not receive `INPUT_` environment variables automatically, so use the context rather than the variable.
```action.yml (illustrative)
name: publish
inputs:
npm-token:
description: 'Token used to publish'
required: true
runs:
using: composite
steps:
- shell: bash
env:
NODE_AUTH_TOKEN: ${{ inputs.npm-token }}
run: npm publish
Pass it explicitly at the call site
The caller is the only place that can read the secret, so it hands it over as an ordinary input. The value remains masked in logs.
```.github/workflows/release.yml (illustrative)
- uses: ./.github/actions/publish with: npm-token: ${{ secrets.NPM_TOKEN }} ```
For an if condition, go through env
Assign the secret to an environment variable at the job or step level, where the context is accepted, and test the variable in the condition. This is the supported shape for branching on whether a secret is configured.
```.github/workflows/release.yml (illustrative)
jobs:
publish:
runs-on: ubuntu-latest
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- if: env.NPM_TOKEN != ''
run: npm publish
### Consider a reusable workflow when there are many secrets
A reusable workflow accepts a `secrets` block and supports `secrets: inherit`, so it is the better shape when a unit of work genuinely needs several secrets. A composite action is the better shape when it needs one or two and should be usable from other repositories.
## How to prevent it
- Treat an action's inputs as its whole interface, and write every credential it needs into that list.
- When moving steps out of a workflow into an action, rewrite every `secrets.` reference in the same commit.
- Guard on `env` rather than on `secrets`, everywhere, so the habit never produces this failure.
- Read the context availability table before putting an expression in an unfamiliar key.
## The message is built by the expression parser
This is not the action complaining and it is not a permission check. `ParseException` in the runner assembles the text from a fixed description for the kind of failure, the raw token that failed, and where it sat. The `UnrecognizedNamedValue` kind maps to the description `Unrecognized named-value`, and the message becomes that description, a colon, the token in single quotes, and then, when a token object is available, `. Located at position ` with a one-based index `within expression: ` and the whole expression.
So the parser was reading an expression, reached the name `secrets`, and had no such name in the set it was given for that file. It is the same failure you would get from a misspelled context. Nothing was evaluated, nothing was empty, and no job was scheduled.
## Where secrets resolves and where it does not
Availability is per key, not per file, and GitHub publishes the list. These are the four places people most often try, and only two of them work.
| Key the expression sits in | Is `secrets` an accepted name | What to use instead |
| --- | --- | --- |
| An `action.yml` of a composite action | No, at any key | An input declared on the action and passed by the caller |
| `jobs.<job_id>.if` in a workflow | No | Copy the secret into `env` on the job and test `env.NAME` |
| `jobs.<job_id>.steps.if` in a workflow | No | Copy the secret into `env` on the step and test `env.NAME` |
| `jobs.<job_id>.steps.with` and `steps.env` | Yes | Nothing, this is where a secret is passed in |
> The middle two rows catch people who never wrote a composite action. The workaround that suggests itself for a missing secret, guarding the step with `if: secrets.NAME != ''`, produces this same message from a plain workflow file. Assigning the secret to `env` first and testing that is the documented way around it, and the value stays masked in the log either way.
## Why the restriction exists and what replaces it
GitHub states the rule and the reason together: "The `secrets` context is not available for composite actions due to security reasons. If you want to pass a secret to a composite action, you need to do it explicitly as an input." A composite action is code that runs inside your job, frequently from another repository at a tag that can move, and giving it ambient access to every secret in the run would make reviewing what it can reach impossible.
Passing secrets as inputs is therefore the feature rather than the workaround. It also has a property worth wanting: the call site lists exactly which secrets the action receives, so a reader of the workflow can see the blast radius without opening the action.
## Why there is no recorded run on this page
There is nothing to record, in the strongest sense on this page: the expression fails to parse, so the run is rejected before any job is created, no runner is assigned, and no step ever exists. The whole of the evidence is the annotation and the grammar that produced it, and we read that grammar in the runner's own `ParseException` rather than inferring it from examples. The annotation above is quoted from a public pull request where the failing file belonged to a third-party action.
## FAQ
### Why is this a parse error rather than an empty value?
Because the name is not defined in that file at all. The runner validates expressions against the set of names allowed in the key being read, and an unknown name is rejected during parsing. There is no evaluation, so there is no empty string to observe, and no job is created to observe it in.
### Can I use secrets anywhere in an action.yml?
No. The `secrets` context is not available in a composite action at any key, which GitHub states with its reason: it is withheld for security, and a secret should be passed explicitly as an input. Inputs are the supported channel and they also document at the call site which secrets the action can see.
### Why does my workflow get this error with no composite action involved?
Because `secrets` is also unavailable in `if` conditions, at both job and step level. Writing `if: secrets.NAME != ''` produces the same message from an ordinary workflow file. Assign the secret to `env` first and test `env.NAME` in the condition instead.
### What does the position in the message refer to?
It is a one-based index into the expression that failed, followed by that expression in full. The runner adds both only when it has a token to point at, which is why some annotations carry just the name in quotes. The line and column before it locate the expression in the file.
## References
- [GitHub Actions: contexts reference, the secrets context and composite actions](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#secrets-context)
- [GitHub Actions: contexts reference, which contexts each key accepts](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#context-availability)
- [actions/runner: ParseException.cs, where the message text is assembled](https://github.com/actions/runner/blob/main/src/Sdk/DTExpressions2/Expressions2/ParseException.cs)
- [GitHub Actions: metadata syntax for composite actions](https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax)
Top comments (0)