GitHub Actions Reusable Workflow "secret not defined" - Declare in workflow_call
A caller passes a secret to a reusable workflow that does not declare it under workflow_call.secrets, so the call is rejected - or the called workflow reads a secret that was never forwarded.
What this error means
The caller fails validation saying the secret is not defined in the called workflow, or the reusable workflow sees an empty secret because the caller did not pass it and did not use inherit.
Invalid workflow file: secret 'DEPLOY_TOKEN' is not defined in the
referenced workflow .github/workflows/deploy.ymlDiagnose it: print the context before you change anything
Most workflow-expression bugs are not syntax errors, they are an expression reading something that is empty. GitHub resolves a missing property to an empty string instead of failing the run, so a wrong reference looks like a logic bug rather than a mistake. Dump the contexts first and you will usually see the answer immediately.
- name: Dump contexts
run: |
echo '--- github ---' ; echo '${{ toJSON(github) }}'
echo '--- needs ---' ; echo '${{ toJSON(needs) }}'
echo '--- steps ---' ; echo '${{ toJSON(steps) }}'
echo '--- matrix ---' ; echo '${{ toJSON(matrix) }}'
echo '--- inputs ---' ; echo '${{ toJSON(inputs) }}'Check the context is allowed where you used it
Contexts are not available everywhere. The same expression can be valid in a step if and invalid in a job if, which is why an expression that works in one workflow fails when moved.
| Where you wrote it | Contexts available there |
|---|---|
run-name | github, inputs, vars |
concurrency | github, inputs, vars |
Top-level env | github, secrets, inputs, vars |
jobs.<id>.if | github, needs, vars, inputs |
jobs.<id>.steps.if | github, needs, strategy, matrix, job, runner, env, vars, steps, inputs |
jobs.<id>.outputs | Full access, including secrets |
Reusable workflow outputs | github, jobs, vars, inputs |
Common causes
Secret not declared in workflow_call
A reusable workflow must list each secret it accepts under on.workflow_call.secrets. Passing an undeclared secret with secrets: { NAME: ... } is rejected.
Relying on inherit when explicit is needed
secrets: inherit forwards all caller secrets, but if you pass secrets explicitly the called workflow still must declare each one. Mixing the two incorrectly leaves a secret empty.
How to fix it
Declare secrets in the called workflow
# deploy.yml (called)
on:
workflow_call:
secrets:
DEPLOY_TOKEN:
required: truePass secrets explicitly or inherit
Either forward each declared secret by name, or use inherit to pass all caller secrets to a trusted called workflow.
# caller
jobs:
call:
uses: ./.github/workflows/deploy.yml
secrets:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
# or, for a trusted callee: secrets: inheritCatch it before it reaches CI
Every failure in this cluster is statically detectable. actionlint parses workflow expressions, checks context availability against the same rules above, and validates needs references, so these bugs never need to cost you a run.
# one-off
docker run --rm -v "$(pwd):/repo" --workdir /repo rhysd/actionlint:latest -color
# as a job, before anything expensive runs
- uses: actions/checkout@v4
- run: |
bash <(curl -s https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
./actionlint -colorHow to prevent it
- Declare every secret a reusable workflow needs under workflow_call.secrets.
- Pass secrets explicitly for least privilege; reserve inherit for trusted callees.
- Document the secret contract alongside the inputs in the called workflow.