# GitHub Actions reusable workflow env not inherited by the called file

> GitHub Actions reusable workflow env not inherited is by design, and the calling job cannot declare env at all. See what does cross the call boundary.

Source: https://latchkey.dev/learn/github-actions/gha-env-scope-reusable-not-inherited  
Updated: 2026-09-20

GitHub Actions reusable workflow env not inherited is documented behavior rather than a bug, and the restriction is stricter than most people expect: the calling job cannot even declare an `env` block, because the schema shape for a job with `uses` has no such key. Values cross that boundary as inputs, as secrets or as organization variables, and as nothing else.

## What this error means

The called workflow reads a variable the caller set and gets an empty string. The same `env` block works perfectly for the caller's own jobs, which is what makes this read as a scoping subtlety rather than a hard boundary. If you then try the obvious repairs, they fail in two different ways: adding `env` to the calling job is rejected when the file is parsed, and passing the value through `with` using an `env` expression is rejected as an unrecognized named-value. Neither message mentions reusable workflows.

```Quoted from the title of actions/runner#2372
Unrecognized named-value: 'env'. Located at position 1 within expression
```

## Common causes

### Expecting workflow-level env to propagate

The commonest cause and the most reasonable expectation, because `env` does reach every job and every step inside the file that declares it. The boundary is the call, not the file, and the documentation lists the behavior among the limitations of reusable workflows rather than among the rules for `env`, which is why it is easy to miss.

### Treating a called workflow like a composite action

A composite action runs inside the calling job and shares its environment, so habits formed there transfer badly. A called workflow runs as its own jobs, on its own runners, with a fresh environment, so there is no shared process for a variable to survive in.

### Passing the value with an env expression

Reaching for `with: key: ${{ env.VALUE }}` on the calling job. The context list for that key does not include `env`, so this fails as an unrecognized named-value rather than as an empty string, and the message names the context instead of the boundary.

### Computing the value in a step of the caller

Writing to `$GITHUB_ENV` in one job and expecting a later calling job to see it. Environment changes do not cross jobs at all, reusable workflow or not, and the call is a job rather than a step, so there is nowhere for a step-scoped value to be read from.

## How to fix it

### Declare an input and pass the value explicitly

Give the reusable workflow a typed input and pass it from the caller. This is the documented mechanism, it is readable at the call site, and it makes the contract between the two files visible to anyone editing either one.

```.github/workflows/build.yml and ci.yml (illustrative)
# build.yml
on:
  workflow_call:
    inputs:
      app_env:
        type: string
        required: true

# ci.yml
jobs:
  call:
    uses: ./.github/workflows/build.yml
    with:
      app_env: staging
```

### Use a variable for anything shared across workflows

Repository and organization variables are readable in both files without being passed, which removes the plumbing entirely. The documentation makes the same recommendation for reusing values across workflows, and it is the right answer for things like an environment name, a region or a registry host.

```.github/workflows/build.yml (illustrative)
steps:
      - run: ./deploy.sh --env "${{ vars.APP_ENV }}"
```

### Publish a computed value as a job output first

1. Compute the value in an ordinary job in the caller and write it to `$GITHUB_OUTPUT`.
2. Map it to a job output with an `outputs` block on that job.
3. Add `needs` on the calling job so the `needs` context resolves.
4. Pass it through `with`, reading `needs.<job>.outputs.<name>`, which is a context that key accepts.

```.github/workflows/ci.yml (illustrative)
call:
    needs: prepare
    uses: ./.github/workflows/build.yml
    with:
      app_env: ${{ needs.prepare.outputs.app_env }}
```

### Set static values inside the called workflow

When the value never varies by caller, stop passing it. An `env` block in the called workflow is perfectly ordinary and applies to its own jobs and steps, and it keeps the input list short enough that the ones that remain are the ones that genuinely differ.

```.github/workflows/build.yml (illustrative)
# build.yml
on:
  workflow_call:
env:
  NODE_ENV: production

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: npm run build
```

## How to prevent it

- Treat a reusable workflow as a separate file with a declared interface, not as an include.
- Keep cross-workflow configuration in `vars` so neither side has to pass it.
- Give every input a `description`, so the contract is readable at the call site.
- Remember that a calling job accepts only nine keys, and check that list before adding one.

## The caller and the callee, and where the value stops

These files are written for this page and have never been run. The caller sets a workflow-level `env` and calls a reusable workflow; the reusable workflow reads the variable and finds nothing. Both files are valid, both jobs are green, and the value is simply not there.

The documentation states it in the limitations for reusable workflows: "Any environment variables set in an `env` context defined at the workflow level in the caller workflow are not propagated to the called workflow." The reverse is listed on the next line: variables set in the called workflow are not accessible in the caller's `env` context either, and outputs are the way back.

```.github/workflows/ci.yml and build.yml (illustrative)
# ci.yml, the caller
name: ci
on: push
env:
  APP_ENV: staging

jobs:
  call:
    uses: ./.github/workflows/build.yml

# build.yml, the called workflow
on:
  workflow_call:
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "env is [${{ env.APP_ENV }}]"   # prints: env is []
```

## The calling job has a different set of keys

A job in the schema is one of two shapes. The first has `runs-on` and `steps` and everything that goes with them. The second, which is the one selected as soon as the parser reads a `uses` key, has exactly nine properties: `name`, `uses`, `with`, `secrets`, `needs`, `if`, `permissions`, `concurrency` and `strategy`. The documentation publishes the same list under "Supported keywords for jobs that call a reusable workflow".

The consequence is mechanical. The template reader narrows the candidate shapes as it reads keys, so `uses` commits the job to the second shape, and the next key that shape does not know is reported with the unexpected-value message. An `env` block on a calling job is not ignored and does not silently do nothing: the file is rejected before the run starts.

```actions/runner, src/Sdk/WorkflowParser/workflow-v1.0.json, abridged
"workflow-job": {
  "mapping": {
    "properties": {
      "name": { "type": "string-strategy-context" },
      "uses": { "type": "non-empty-string", "required": true },
      "with": "workflow-job-with",
      "secrets": "workflow-job-secrets",
      "needs": "needs",
      "if": "job-if",
      "permissions": "permissions",
      "concurrency": "job-concurrency",
      "strategy": "strategy"
    }
  }
}
```

## The second repair that does not work either

Having lost the `env` block, the natural next move is to pass the value through `with`, reading it from the caller's environment. That is rejected too, and this is the one worth knowing because the message says nothing about reusable workflows. The value type for `jobs.<job_id>.with.<input_id>` allows only the github, inputs, vars, needs, strategy and matrix contexts, and the documented availability table agrees. `env` is not among them.

This was reported as actions/runner#2372, whose title is the message itself: "Unrecognized named-value: 'env'. Located at position 1 within expression" when used in reusable workflow jobs. So the value has to come from somewhere a calling job is allowed to read: a literal, an input of the caller, a repository or organization variable, or an output of an earlier job.

```.github/workflows/ci.yml (illustrative)
jobs:
  call:
    uses: ./.github/workflows/build.yml
    with:
      app_env: ${{ vars.APP_ENV }}      # allowed
      # app_env: ${{ env.APP_ENV }}     # rejected: env is not offered here
```

## What crosses the boundary

Three things cross and two do not. Reading the table once is usually enough to stop reaching for the two that do not, and it also explains why `vars` is the quiet winner for configuration: it is not passed at all, it is simply readable on both sides.

The last row is worth its own sentence. The documentation notes that reusable workflows "are called directly within a job, and not from within a job step", so there is no step in the caller that could write to `GITHUB_ENV` for the call to pick up. A value computed at runtime therefore has to be published as a job output first, and read through `needs` when the call is made.

| Set in the caller | Crosses into the called workflow | How to get the value across |
| --- | --- | --- |
| `env` at workflow level | no | declare an input, pass a literal or a `vars` value |
| `env` on the calling job | the key is not allowed at all | same as above |
| `with`, against declared inputs | yes | this is the mechanism |
| `secrets`, named or `inherit` | yes | `secrets: inherit`, or a named map |
| `vars` at repository or organization level | readable on both sides | nothing to do |
| a value written to `GITHUB_ENV` by a step | no | publish it as a job output, pass it via `needs` |

> Permissions cross too, with a rule of their own: the documentation notes that the token permissions passed from the caller "can be only downgraded (not elevated) by the called workflow".

## Why there is no recorded run on this page

Half of this page is about a file that never starts, so for that half there is no job, no runner and no log: the `env` block on a calling job is rejected during parsing, as is the `with` expression that reads it. The other half is about an absence, where a variable that did not cross the boundary reads as an empty string, and an empty string in a log is indistinguishable from a variable that was never set, misspelled, or set to nothing on purpose. A recorded run would therefore show either nothing at all or a blank where a value should be, and neither would be evidence for which of those it was. The schema and the documented limitation, both quoted above, say it without ambiguity.

## FAQ

### Why is env empty inside a called workflow?

Because it was never sent. The documented limitations for reusable workflows state that environment variables set in an `env` context at the workflow level in the caller are not propagated to the called workflow. The called workflow runs as its own jobs with a fresh environment, so there is nothing to inherit.

### Can I add an env block to a job that calls a reusable workflow?

No. Once the parser reads `uses`, the job is matched against a shape whose only properties are name, uses, with, secrets, needs, if, permissions, concurrency and strategy. Any other key, including `env`, is reported as an unexpected value and the file is rejected.

### Why does with: ${{ env.X }} fail on a calling job?

Because the context list for that key does not include `env`. Only github, inputs, vars, needs, strategy and matrix are offered there, so the expression parser reports an unrecognized named-value. Pass a literal, a `vars` value, or an output read through `needs` instead.

### Do secrets cross the workflow_call boundary?

Yes, but only when you send them. Use a named `secrets` map, or `secrets: inherit` to pass everything the caller can see. Secrets reach only directly called workflows, so in a chain of three, the middle workflow has to pass them on explicitly.

## References

- [GitHub Actions: reusing workflow configurations, limitations and supported keywords](https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations)
- [GitHub Actions: reuse workflows, inputs and secrets](https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows)
- [GitHub Actions: contexts reference and context availability](https://docs.github.com/en/actions/reference/workflows-and-actions/contexts)
- [actions/runner#2372: unrecognized named-value env in a calling job](https://github.com/actions/runner/issues/2372)
- [actions/runner: workflow-v1.0.json, the two shapes a job can take](https://github.com/actions/runner/blob/main/src/Sdk/WorkflowParser/workflow-v1.0.json)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
