Fix: GitHub Actions Reusable Workflow Not Working — Inputs Not Passed or Secrets Not Available
Part of: Docker, DevOps & Infrastructure
Quick Answer
How to fix GitHub Actions reusable workflow issues — workflow_call trigger, passing inputs and secrets, output variables, caller vs called permissions, and common errors.
The Problem
A reusable workflow doesn’t receive inputs from the caller:
# caller.yml
jobs:
deploy:
uses: ./.github/workflows/deploy.yml
with:
environment: production # Input not available in called workflowOr secrets aren’t passed to the reusable workflow:
# In called workflow — secret is empty
- run: echo "Token is ${{ secrets.API_TOKEN }}"
# Output: Token isOr the called workflow can’t access outputs from the reusable workflow:
# Caller — output is empty
- run: echo "Version is ${{ needs.build.outputs.version }}"
# Output: Version isOr the workflow fails with:
Error: Unrecognized named-value: 'inputs'
Error: Required input 'environment' is not providedWhy This Happens
Reusable workflows use the workflow_call trigger and have strict rules for passing data. The trigger declares an interface — inputs, secrets, and outputs — that callers must match exactly. Anything not declared on that interface is silently dropped at the boundary between caller and called workflow.
The most common confusion is the secret boundary. GitHub deliberately blocks all secrets from crossing into a called workflow unless the caller passes them explicitly. This is a security control, not a bug. Composite actions inherit the caller’s secrets through env:, but reusable workflows do not. If you migrated from a composite action and stopped seeing your tokens, this is the reason.
The second source of confusion is the inputs context. Inside a reusable workflow, ${{ inputs.foo }} resolves to the value passed by the caller. Inside a regular workflow triggered by push or workflow_dispatch, ${{ inputs.foo }} only resolves if the workflow declared matching workflow_dispatch.inputs. Reusing the same YAML for both call paths means you must declare inputs under both triggers, or split the file.
workflow_calltrigger required — the called workflow must declareon: workflow_call:to accept calls from other workflows. Without it, the workflow can’t be reused.- Inputs must be declared — the called workflow must explicitly declare every input under
on.workflow_call.inputs. Passing undeclared inputs is silently ignored. - Secrets must be passed explicitly — secrets from the caller are not automatically available in called workflows. They must be explicitly passed via
secrets:in the caller and declared in the called workflow. Or usesecrets: inheritto pass all secrets automatically. inputscontext only available in called workflows — using${{ inputs.foo }}in a regular workflow (not called viaworkflow_call) causes “Unrecognized named-value: ‘inputs’”.
Fix 1: Define workflow_call Correctly
The called workflow must declare on: workflow_call: with all inputs and secrets:
# .github/workflows/deploy.yml — REUSABLE workflow
name: Deploy
on:
workflow_call:
# Declare all inputs this workflow accepts
inputs:
environment:
required: true
type: string
description: 'Target environment (staging, production)'
docker_tag:
required: false
type: string
default: 'latest'
description: 'Docker image tag to deploy'
dry_run:
required: false
type: boolean
default: false
# Declare secrets this workflow needs
secrets:
DEPLOY_TOKEN:
required: true
description: 'Token for deployment API'
SLACK_WEBHOOK:
required: false
# Declare outputs this workflow produces
outputs:
deployment_id:
description: 'ID of the created deployment'
value: ${{ jobs.deploy.outputs.deployment_id }}
jobs:
deploy:
runs-on: ubuntu-latest
outputs:
deployment_id: ${{ steps.create-deployment.outputs.deployment_id }}
steps:
- name: Deploy to ${{ inputs.environment }}
id: create-deployment
if: ${{ !inputs.dry_run }}
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
DEPLOYMENT_ID=$(./scripts/deploy.sh \
--env ${{ inputs.environment }} \
--tag ${{ inputs.docker_tag }})
echo "deployment_id=$DEPLOYMENT_ID" >> $GITHUB_OUTPUT
- name: Dry run notification
if: ${{ inputs.dry_run }}
run: echo "DRY RUN: would deploy ${{ inputs.docker_tag }} to ${{ inputs.environment }}"
- name: Notify Slack
if: ${{ secrets.SLACK_WEBHOOK != '' }}
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-d '{"text": "Deployed to ${{ inputs.environment }}"}'Fix 2: Call a Reusable Workflow with Inputs and Secrets
# .github/workflows/release.yml — CALLER workflow
name: Release
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
outputs:
docker_tag: ${{ steps.tag.outputs.tag }}
steps:
- uses: actions/checkout@v4
- name: Generate tag
id: tag
run: echo "tag=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
deploy-staging:
needs: build
uses: ./.github/workflows/deploy.yml # Relative path for same repo
with:
environment: staging
docker_tag: ${{ needs.build.outputs.docker_tag }}
secrets:
DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}
# SLACK_WEBHOOK: not passed — optional secret, caller omits it
deploy-production:
needs: [build, deploy-staging]
uses: ./.github/workflows/deploy.yml
with:
environment: production
docker_tag: ${{ needs.build.outputs.docker_tag }}
secrets:
DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
# Use outputs from the reusable workflow
post-deploy:
needs: deploy-production
runs-on: ubuntu-latest
steps:
- name: Log deployment ID
run: echo "Deployed with ID ${{ needs.deploy-production.outputs.deployment_id }}"Using secrets: inherit (simpler):
# Pass ALL secrets from caller to called workflow automatically
deploy-production:
uses: ./.github/workflows/deploy.yml
with:
environment: production
secrets: inherit # All secrets from the caller are availableNote:
secrets: inheritpasses all secrets the caller has access to. The called workflow must still declare which secrets it uses underon.workflow_call.secrets— but they don’t needrequired: trueto be available.
Fix 3: Reference Reusable Workflows in Other Repos
Call workflows from other repositories:
# Call a workflow from another repository
jobs:
lint:
uses: myorg/shared-workflows/.github/workflows/lint.yml@main
# Format: {owner}/{repo}/.github/workflows/{file}@{ref}
with:
node_version: '20'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
# Use a specific commit SHA for reproducible builds
deploy:
uses: myorg/shared-workflows/.github/workflows/deploy.yml@abc1234
with:
environment: production
# Use a release tag
test:
uses: myorg/shared-workflows/.github/workflows/[email protected]Calling across organizations requires explicit permissions:
# The called workflow's repository must allow it
# Settings → Actions → General → Allow {org} to use reusable workflows
# Or use a PAT with repo scope as a secretFix 4: Pass Matrix Values to Reusable Workflows
Combine matrix strategy with reusable workflows:
# Caller — matrix of environments
jobs:
deploy-matrix:
strategy:
matrix:
environment: [dev, staging, production]
include:
- environment: production
requires_approval: true
- environment: staging
requires_approval: false
- environment: dev
requires_approval: false
uses: ./.github/workflows/deploy.yml
with:
environment: ${{ matrix.environment }}
secrets: inheritNote: You can’t pass matrix values directly to the
uses:path — only towith:andsecrets:. The workflow path itself must be a static string.
Fix 5: Reusable Workflow Patterns
Build-once, deploy-many pattern:
# .github/workflows/ci-cd.yml
jobs:
test:
uses: ./.github/workflows/test.yml
secrets: inherit
build:
needs: test
uses: ./.github/workflows/build.yml
with:
push_image: true
secrets: inherit
# outputs: image_tag
deploy-staging:
needs: build
uses: ./.github/workflows/deploy.yml
with:
environment: staging
image_tag: ${{ needs.build.outputs.image_tag }}
secrets: inherit
integration-tests:
needs: deploy-staging
uses: ./.github/workflows/integration-test.yml
with:
target_url: https://staging.example.com
secrets: inherit
deploy-production:
needs: [build, integration-tests]
uses: ./.github/workflows/deploy.yml
with:
environment: production
image_tag: ${{ needs.build.outputs.image_tag }}
secrets: inheritAdding environment protection rules:
# deploy.yml — require approval for production
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }} # Links to GitHub Environment settings
# production environment has protection rules (required reviewers)
steps:
- name: Deploy
run: ./deploy.shFix 6: Debug Reusable Workflow Issues
# Add debug output to trace inputs and secrets
jobs:
debug:
runs-on: ubuntu-latest
steps:
- name: Print inputs
run: |
echo "environment: ${{ inputs.environment }}"
echo "docker_tag: ${{ inputs.docker_tag }}"
echo "dry_run: ${{ inputs.dry_run }}"
- name: Check secret availability
run: |
if [ -z "$SECRET_VALUE" ]; then
echo "DEPLOY_TOKEN is empty!"
else
echo "DEPLOY_TOKEN is set (length: ${#SECRET_VALUE})"
fi
env:
SECRET_VALUE: ${{ secrets.DEPLOY_TOKEN }}Common error messages and fixes:
Error: "Unrecognized named-value: 'inputs'"
→ The workflow isn't called via workflow_call — inputs context unavailable
→ Add 'on: workflow_call:' or check the trigger type
Error: "Required input 'environment' is not provided"
→ Caller uses `with:` but doesn't include all required inputs
→ Add the missing input in the caller's `with:` block
Error: "Unexpected value 'uses'"
→ 'uses' at job level requires no 'steps' — jobs with 'uses' can't have steps
→ Split into two separate jobs
Warning: "Secret 'API_TOKEN' was not passed to the called workflow"
→ The called workflow declares the secret but caller doesn't pass it
→ Add 'secrets: { API_TOKEN: ${{ secrets.API_TOKEN }} }' to the callerVersion History: What Changed and When
Reusable workflows shipped to general availability in November 2021. The first release supported workflow_call, typed inputs (string, boolean, number), and outputs at the workflow level. Secret inheritance was not yet available — callers had to enumerate every secret by hand, even for shared utilities like actions/cache tokens. If you maintain workflows written before late 2022, you will often see long secrets: blocks that predate inherit.
secrets: inherit arrived in August 2022, and it is the single biggest quality-of-life fix in the reusable workflows lifecycle. Before it, refactoring a job into a reusable workflow meant auditing every secret reference in the original job and copying each one into the caller. After August 2022, you can move a job behind workflow_call and add secrets: inherit on the caller side without touching the secret list. The called workflow still needs to declare each secret under on.workflow_call.secrets, but required: false is enough — the inherit path supplies the value.
Matrix passthrough became reliable in 2023. Earlier in the lifecycle, mixing strategy.matrix with uses: produced inconsistent fan-out, especially when matrix include: added keys not present in the base matrix. The 2023 runner updates made matrix values available as ${{ matrix.* }} inside with: on the caller, which is what the Fix 4 pattern relies on. If you are on a self-hosted runner image older than mid-2023, upgrade before debugging matrix-driven reusable workflows.
OIDC tokens flow through reusable workflows. When the called workflow requests an OIDC token via permissions: id-token: write, the token’s job_workflow_ref claim points to the reusable workflow, not the caller. Cloud IAM trust policies must match the reusable workflow’s path (owner/repo/.github/workflows/file.yml@ref), not the caller’s. This trips up teams that move IAM-touching steps into a shared workflow and then see “Not authorized to perform sts:AssumeRoleWithWebIdentity” until they update the trust policy. OIDC support was rolled out in late 2021 and the job_workflow_ref claim has been stable since 2022.
Called workflow outputs had a rough early period. Before mid-2022, declaring outputs: under workflow_call and reading them via needs.<job>.outputs.<name> worked only when the value was a simple string. Object or JSON outputs had to be stringified with toJSON() on the producer side and fromJSON() on the consumer side. That pattern still works and is still the recommended path for structured data — the JSON round-trip survives all runner updates.
Nesting limits changed as well. Reusable workflows can call other reusable workflows up to four levels deep as of 2024. Earlier, the limit was three. Hitting the nesting cap fails the run with “Cannot nest reusable workflows more than X levels deep.” If you see this error on an old runbook, the simplest fix is to flatten one layer rather than wait for a limit bump.
If your workflow predates any of these versions, expect to find: explicit secret lists where inherit would work, manual matrix expansion via separate jobs, and stringified outputs that could now be passed as proper typed outputs. None of this is broken — but it is extra YAML that newer GitHub Actions versions let you delete.
Still Not Working?
Reusable workflow job can’t have steps — a job that calls a reusable workflow with uses: cannot also have steps:. If you need both, use two separate jobs with needs:.
env context not available in called workflows — environment variables set in the caller are not passed to called workflows. Pass them as explicit inputs instead.
Outputs from reusable workflows need needs: reference — to use outputs from a reusable workflow job, the consumer job must declare it in needs: and reference it as ${{ needs.<job-id>.outputs.<output-name> }}. The output must be declared both at the step level (echo "foo=bar" >> $GITHUB_OUTPUT), job level (outputs: foo: ${{ steps.step-id.outputs.foo }}), and workflow_call level (outputs: foo: value: ${{ jobs.job-id.outputs.foo }}).
GITHUB_TOKEN permissions are scoped per workflow file — the permissions: block in the caller does not propagate to the called workflow. The called workflow’s GITHUB_TOKEN is computed from its own permissions: declaration plus the repository default. If a step needs contents: write and the called workflow only declares contents: read, raising it in the caller has no effect. Move the permissions: block into the reusable workflow itself.
Environment protection rules apply at the called workflow’s job — environment: production inside the reusable workflow triggers protection rules (required reviewers, wait timer, deployment branches). The caller cannot bypass them. If a deploy waits forever for approval, check the environment settings in the called workflow’s repository, not the caller’s.
Workflow file path is case-sensitive on Linux runners — .github/workflows/Deploy.yml and .github/workflows/deploy.yml are different files on Linux. The uses: reference must match the actual filename exactly. Windows runners are forgiving here; Linux ones are not. CI works locally on Windows then fails on ubuntu-latest for exactly this reason.
For related GitHub Actions issues, see Fix: GitHub Actions Cache Not Working, Fix: GitHub Actions Timeout, Fix: GitHub Actions Secret Not Available, and Fix: GitHub Actions Matrix Strategy.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: GitHub Actions Job Timeout — Workflow Cancelled or Stuck After 6 Hours
How to fix GitHub Actions timeout issues — job-level and step-level timeouts, stuck processes, self-hosted runner timeouts, debugging hanging jobs, and timeout best practices.
Fix: GitHub Actions Artifacts Not Working — Upload Fails, Download Empty, or Artifact Not Found
How to fix GitHub Actions artifact issues — upload-artifact path patterns, download-artifact across jobs, retention days, artifact name conflicts, and the v3 to v4 migration.
Fix: GitHub Actions Secret Not Available — Environment Variable Empty in Workflow
How to fix GitHub Actions secrets that appear empty or undefined in workflows — secret scope, fork PR restrictions, environment protection rules, secret names, and OIDC alternatives.
Fix: GitHub Actions Matrix Strategy Not Working — Jobs Not Running or Failing
How to fix GitHub Actions matrix strategy issues — matrix expansion, include/exclude patterns, failing fast, matrix variable access, and dependent jobs with matrix outputs.