Skip to content

Fix: GitHub Actions Reusable Workflow Not Working — Inputs Not Passed or Secrets Not Available

FixDevs · (Updated: )

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 workflow

Or secrets aren’t passed to the reusable workflow:

# In called workflow — secret is empty
- run: echo "Token is ${{ secrets.API_TOKEN }}"
# Output: Token is

Or 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 is

Or the workflow fails with:

Error: Unrecognized named-value: 'inputs'
Error: Required input 'environment' is not provided

Why 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_call trigger required — the called workflow must declare on: 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 use secrets: inherit to pass all secrets automatically.
  • inputs context only available in called workflows — using ${{ inputs.foo }} in a regular workflow (not called via workflow_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 available

Note: secrets: inherit passes all secrets the caller has access to. The called workflow must still declare which secrets it uses under on.workflow_call.secrets — but they don’t need required: true to 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 secret

Fix 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: inherit

Note: You can’t pass matrix values directly to the uses: path — only to with: and secrets:. 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: inherit

Adding 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.sh

Fix 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 caller

Version 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 jobenvironment: 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles