GitHub Actions composite action cannot access secrets directly
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.

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.
[error] thomasbnt/hello-to-news-contributions/v1.0/action.yml (Line: 14, Col: 23): Unrecognized named-value: 'secrets'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.
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.
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 publishPass 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.
- 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.
jobs:
publish:
runs-on: ubuntu-latest
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- if: env.NPM_TOKEN != ''
run: npm publishConsider 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.
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 |
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.
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
envrather than onsecrets, everywhere, so the habit never produces this failure. - Read the context availability table before putting an expression in an unfamiliar key.
Frequently asked questions
Why is this a parse error rather than an empty value?
Can I use secrets anywhere in an action.yml?
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?
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.