1076 words
5 minutes

Reusable Workflow Secrets Not Passed: Find the Handoff

2026-08-10
2026-08-26
DevOps
GitHub Actions
/
CI/CD
/
DevOps
/
Troubleshooting

If a secret is available in the caller workflow but empty in a reusable workflow, GitHub Actions did not pass it across the workflow boundary. Declare the secret contract in the called workflow, map the value directly or use secrets: inherit where it is allowed, and pass it again at every nested boundary.

Find the boundary where the secret disappears#

Treat “the secret is missing” as a boundary diagnosis, not as one generic permissions problem. Start with the earliest signal:

Observed signalBoundary to inspectCorrective action
The workflow is rejected before a runner startsThe caller passed a name that the called workflow did not declare under on.workflow_call.secretsDeclare the same secret ID in the called workflow, or use the supported secrets: inherit path deliberately.
The called job runs, but a presence check failsThe caller’s job-level secrets map is missing or maps the wrong nameCompare jobs.<job_id>.secrets in the caller with secrets.<secret_id> in the called workflow.
Workflow B receives the value, but nested workflow C does notThe B-to-C boundary did not forward the secretAdd another jobs.<job_id>.secrets mapping in B; nested workflows do not receive arbitrary secrets automatically.
The value changes after a job selects an environmentA same-named environment secret owns the called job’s valueChoose one owner or use distinct names; a called job’s environment can take precedence over the caller’s passed secret.
The value exists on normal runs but not on a fork pull requestActions secrets are not passed to fork-triggered workflows, except for GITHUB_TOKENUse a no-secret validation path for untrusted fork code and require a trusted, reviewed path for deployment.
A workflow condition fails even though the secret existsA secret was referenced directly in an if: conditionalConvert only the presence state to a boolean environment value, or use a non-secret gate; never put the secret value in the condition.

For the last case, the called workflow can test presence without exposing the credential:

jobs:
deploy:
runs-on: ubuntu-latest
env:
HAS_DEPLOY_TOKEN: ${{ secrets.deploy_token != '' }}
steps:
- name: Stop when the handoff is unavailable
if: ${{ env.HAS_DEPLOY_TOKEN != 'true' }}
run: echo '::error::deploy_token is unavailable in this workflow run'

This checks availability only. It does not make untrusted fork code safe to run with deployment credentials.

Declare the secret in the reusable workflow#

A reusable workflow is called through workflow_call. Named secrets make the contract visible and let GitHub validate the caller’s input:

.github/workflows/reusable-deploy.yml
name: Reusable deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
deploy_token:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy
env:
DEPLOY_TOKEN: ${{ secrets.deploy_token }}
DEPLOY_ENVIRONMENT: ${{ inputs.environment }}
run: ./scripts/deploy.sh "$DEPLOY_ENVIRONMENT"

The caller passes a named secret on the job that uses the reusable workflow:

.github/workflows/deploy.yml
jobs:
deploy:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
secrets:
deploy_token: ${{ secrets.DEPLOY_TOKEN }}

The uses form is a job-level call, so the reusable workflow’s inputs and secrets belong beside with, secrets, and uses, not inside a step. The caller and called workflow also need compatible permissions; a called workflow cannot elevate the permissions it receives.

Choose direct mapping or secrets: inherit#

Use direct mapping when the reusable workflow needs a small, documented set of secrets:

secrets:
deploy_token: ${{ secrets.DEPLOY_TOKEN }}
package_token: ${{ secrets.PACKAGE_TOKEN }}

Use secrets: inherit when the caller and reusable workflow are inside the same organization or enterprise boundary and passing the available repository, organization, or enterprise secrets is intentional:

jobs:
deploy:
uses: org/platform/.github/workflows/reusable-deploy.yml@main
secrets: inherit

inherit is not a general cross-repository secret tunnel. For a reusable workflow outside the supported organization or enterprise boundary, map the required secrets explicitly or keep the secret-consuming job in the caller repository. Document the expected secret names even when using inherit; otherwise a rename can look like a permissions problem.

Forward secrets through nested reusable workflows#

Secrets do not automatically travel through a chain. If workflow A calls workflow B and B calls workflow C, A must pass a secret to B and B must pass it to C:

# A: caller
jobs:
release:
uses: ./.github/workflows/b.yml
secrets:
deploy_token: ${{ secrets.DEPLOY_TOKEN }}
# B: reusable workflow calling C
jobs:
publish:
uses: ./.github/workflows/c.yml
secrets:
deploy_token: ${{ secrets.deploy_token }}

If B receives the secret but C does not, inspect the B-to-C edge first. The GitHub reusable workflow documentation also limits how far a nested chain can extend and requires secrets to be passed directly through each link.

Check environment-secret precedence#

Environment secrets are a common source of a misleading “missing secret” result. Secrets defined under an environment cannot be passed through on.workflow_call. If a job in the called workflow declares an environment, a secret with the same name from that environment takes precedence over the secret the caller attempted to pass.

Decide which boundary owns the credential:

  • Use a workflow_call secret for a value supplied by the caller.
  • Use an environment secret when deployment approval and environment protection should own the value.
  • Do not give both boundaries the same name unless the precedence is deliberate and documented.

Debug the mapping without printing the value#

Check presence, not the secret itself. A temporary diagnostic step can expose whether the called workflow received a value while keeping the value masked:

- name: Check deploy token wiring
env:
DEPLOY_TOKEN: ${{ secrets.deploy_token }}
run: |
if [ -z "$DEPLOY_TOKEN" ]; then
echo '::error::deploy_token was not passed to the reusable workflow'
exit 1
fi

Remove the diagnostic after fixing the contract. Never print a secret, its length, or a transformed version of it as a debugging shortcut.

A quick checklist#

  1. The called file uses on.workflow_call and names the required inputs and secrets.
  2. The caller maps each secret under the job-level secrets key, or intentionally uses secrets: inherit.
  3. Every nested caller forwards the secret again.
  4. No called job environment silently replaces the caller’s value.
  5. The called workflow’s permissions are sufficient for the operation.
  6. Diagnostics test presence without exposing the credential.

If the same caller also uses typed manual inputs, see how GitHub Actions handles boolean workflow-dispatch inputs. For a later artifact failure, check the hidden-file behavior of upload-artifact separately; it is a different boundary from secret passing.

FAQ#

Q: Does secrets: inherit work for any public reusable workflow?#

A: No. inherit is intended for supported organization or enterprise boundaries. Use explicit mapping when the caller and called workflow do not share the required boundary.

Q: Why does a nested reusable workflow receive an empty secret?#

A: The intermediate workflow probably received the secret but did not pass it in its own secrets mapping. Each A-to-B-to-C edge needs an explicit handoff.

Q: Can on.workflow_call receive an environment secret?#

A: No. Environment secrets are not passed through the workflow_call interface. If the called job selects an environment, its environment secret can take precedence over a caller-provided secret with the same name.

References:

GitHub Docs: Reuse workflows

GitHub Docs: Workflow syntax

GitHub Docs: Use secrets in workflows

GitHub Docs: Events that trigger workflows

Reusable Workflow Secrets Not Passed: Find the Handoff
https://laplusda.com/en/posts/github-actions-reusable-workflow-secrets-not-passed/
Author
Zero
Published at
2026-08-10
License
CC BY-NC-SA 4.0
Was this article useful?

Report a typo or broken link, or suggest a related topic.