GitHub Actions Workflow env Not Inherited by a Reusable / Called Workflow
A variable set in a caller workflow env block is empty inside the reusable workflow it calls. env does not propagate across the workflow_call boundary - only declared inputs and secrets do.
What this error means
A reusable workflow reads an env var the caller defined and gets an empty value, even though the same env block works for the caller’s own jobs.
# caller
env:
APP_ENV: staging
jobs:
call:
uses: ./.github/workflows/build.yml
# build.yml reads ${{ env.APP_ENV }} -> empty, not "staging"Diagnose 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
env does not cross workflow_call
A reusable workflow gets a fresh context. The caller’s workflow- and job-level env are not visible inside it; only inputs and secrets are passed explicitly.
Relying on implicit inheritance
Unlike a composite action that runs in the caller’s job, a called workflow runs as its own jobs, so there is no shared env to inherit.
How to fix it
Pass values as declared inputs
Declare an input on the reusable workflow and pass the env value through with:.
# caller
jobs:
call:
uses: ./.github/workflows/build.yml
with:
app_env: ${{ env.APP_ENV }}
# build.yml
on:
workflow_call:
inputs:
app_env: { type: string, required: true }Set env inside the reusable workflow when static
- For constant values, define env directly in the reusable workflow.
- For caller-specific values, always pass them as inputs.
- Pass secrets explicitly or with secrets: inherit - env is never inherited.
Catch 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
- Pass dynamic values to reusable workflows as inputs, not via env.
- Document each reusable workflow’s required inputs and secrets.
- Do not assume env crosses the workflow_call boundary.