4.1 Authoring & Calling Reusable Workflows (workflow_call)
Key Takeaways
- Reusable workflows are declared using the workflow_call event trigger, enabling centralized CI/CD pipelines to be shared across multiple caller repositories or workflows at the job level.
- The workflow_call trigger explicitly defines strongly typed inputs (string, number, boolean), secret specifications (with optional, required, or inherit modes), and outputs mapped from child jobs.
- Caller workflows invoke reusable workflows using uses: {owner}/{repo}/.github/workflows/{filename}.yml@{ref} for remote references or uses: ./.github/workflows/{filename}.yml for local same-repository references.
- Organization sharing requires private/internal repositories containing reusable workflows to enable access for 'Repositories in this organization' or specific selected repositories.
- Reusable workflows can be nested up to ten levels deep, a single workflow file may reference at most 50 unique reusable workflows across the whole nested tree, and GITHUB_TOKEN permissions can only be narrowed as the chain descends.
Authoring & Calling Reusable Workflows (workflow_call)
In enterprise DevOps architectures, duplicating workflow definitions across dozens or hundreds of repositories introduces severe maintenance overhead, configuration drift, and compliance risks. To solve this, GitHub Actions provides reusable workflows.
A reusable workflow allows an engineering organization to define a standardized CI/CD pipeline in a central repository and invoke it from multiple caller workflows. Unlike actions, which operate at the individual step level, reusable workflows operate at the job level, orchestrating entire multi-job pipelines with their own runner allocations, secrets, and environment approvals.
1. The Reusable Workflow Mental Model: Caller vs. Called
Understanding the separation of responsibilities between the caller workflow and the called (reusable) workflow is essential for designing clean automation architectures:
- Caller Workflow: The triggering workflow residing in any repository. It defines when to execute (triggers like
push,pull_request,schedule) and passes configuration parameters, secrets, and environment settings to a job that invokes the reusable workflow via theuses:keyword. - Called (Reusable) Workflow: The workflow definition file residing in
.github/workflows/of the central or local repository. It defines what computational work occurs using theon: workflow_calltrigger, specifying required inputs, expected secrets, output contracts, and job execution graphs.
2. Authoring Reusable Workflows (on: workflow_call)
A workflow is converted into a reusable workflow by declaring workflow_call under the top-level on: trigger key. The workflow_call trigger can define three configuration blocks: inputs, secrets, and outputs.
1. Defining Inputs
Inputs defined under workflow_call are strongly typed. GitHub Actions validates incoming parameters against these specifications before executing any jobs:
on:
workflow_call:
inputs:
environment:
description: 'Target deployment environment'
required: true
type: string
replica-count:
description: 'Number of container replicas'
required: false
type: number
default: 3
run-integration-tests:
description: 'Flag to enable heavy integration tests'
required: false
type: boolean
default: false
| Input Property | Type | Description |
|---|---|---|
description | string | Human-readable explanation displayed in documentation and errors. |
required | boolean | If true, the caller workflow must provide this parameter. |
type | string | Data type constraint: string, number, or boolean. |
default | Any | Fallback value used when an optional input is omitted by the caller. |
Inside the called workflow, inputs are referenced using the inputs context: ${{ inputs.environment }} or ${{ inputs.replica-count }}.
2. Defining Secrets
Reusable workflows do not have automatic access to the caller repository's secrets unless they are explicitly passed or inherited.
on:
workflow_call:
secrets:
DATABASE_URL:
description: 'PostgreSQL connection string'
required: true
API_TOKEN:
description: 'External API deployment token'
required: false
Secrets defined here are accessed inside the called workflow via the standard secrets context: ${{ secrets.DATABASE_URL }}.
3. Mapping Outputs
To expose data calculated inside the reusable workflow back to the caller workflow, map step outputs through job outputs to top-level workflow outputs:
on:
workflow_call:
outputs:
deployment-url:
description: 'Public URL of the deployed application'
value: ${{ jobs.deploy.outputs.url }}
build-sha:
description: 'Git commit SHA that was compiled'
value: ${{ jobs.build.outputs.digest }}
3. Calling Reusable Workflows (uses:)
A caller workflow invokes a reusable workflow at the job level using the uses: key instead of steps:.
Remote vs. Local Workflow Syntax
-
Remote Reusable Workflow (Cross-Repository): Format:
{owner}/{repo}/.github/workflows/{filename}.yml@{ref}uses: enterprise-org/shared-workflows/.github/workflows/deploy.yml@v2.1.0The
@refcan be a Git tag (@v2.1.0), branch (@main), or immutable full 40-character commit SHA (@8f3a1b4c...). For production security and SLSA compliance, pinning to a full commit SHA is strongly recommended. -
Local Reusable Workflow (Same Repository): Format:
./.github/workflows/{filename}.ymluses: ./.github/workflows/build-and-package.ymlLocal workflows do not specify an
@reftag; GitHub Actions automatically evaluates the workflow file from the same commit ref that triggered the caller workflow.
Passing Secrets to Reusable Workflows
Caller workflows have two distinct mechanisms for passing secrets:
-
Explicit Secret Mapping: Passes secrets individually by name.
jobs: call-deploy: uses: org/workflows/.github/workflows/deploy.yml@v1 secrets: DATABASE_URL: ${{ secrets.PROD_DB_URL }} API_TOKEN: ${{ secrets.DEPLOY_API_TOKEN }} -
Secret Inheritance (
secrets: inherit): Injects all available secrets from the caller's context into the reusable workflow automatically.jobs: call-deploy: uses: org/workflows/.github/workflows/deploy.yml@v1 secrets: inherit
[!NOTE]
secrets: inheritautomatically passes repository secrets, organization secrets, and environment secrets accessible to the caller. Inside the called workflow, only secrets defined or referenced in its own jobs are utilized.
4. Complete Worked YAML Examples
Called Reusable Workflow (.github/workflows/reusable-service-deploy.yml)
name: Reusable Service Deployment
on:
workflow_call:
inputs:
target-environment:
description: 'Deployment environment tier'
required: true
type: string
service-name:
description: 'Name of the microservice binary'
required: true
type: string
secrets:
DEPLOY_KEY:
required: true
outputs:
endpoint-url:
description: 'Generated HTTPS endpoint'
value: ${{ jobs.deploy-cloud.outputs.service_url }}
jobs:
compile-and-package:
name: Build Artifact
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Build Binary
run: |
mkdir -p dist
echo "Compiled binary for ${{ inputs.service-name }}" > dist/app.bin
deploy-cloud:
name: Deploy to Cloud
needs: compile-and-package
runs-on: ubuntu-latest
outputs:
service_url: ${{ steps.step-deploy.outputs.url }}
steps:
- name: Authenticate and Deploy
id: step-deploy
run: |
echo "Deploying ${{ inputs.service-name }} to ${{ inputs.target-environment }}"
echo "Using authorization key with length: ${#DEPLOY_SECRET}"
URL="https://${{ inputs.service-name }}.${{ inputs.target-environment }}.internal.corp"
echo "url=${URL}" >> "$GITHUB_OUTPUT"
env:
DEPLOY_SECRET: ${{ secrets.DEPLOY_KEY }}
Caller Workflow (.github/workflows/production-release.yml)
name: Production Release Pipeline
on:
push:
tags:
- 'v*'
jobs:
execute-reusable-deployment:
name: Call Reusable Deploy Pipeline
uses: enterprise-core/ci-templates/.github/workflows/reusable-service-deploy.yml@v1.4.0
with:
target-environment: 'production'
service-name: 'auth-gateway'
secrets: inherit
verify-health:
name: Verify Post-Deployment Health
needs: execute-reusable-deployment
runs-on: ubuntu-latest
steps:
- name: Probe Health Endpoint
run: |
DEPLOYED_URL="${{ needs.execute-reusable-deployment.outputs.endpoint-url }}"
echo "Probing health check at: ${DEPLOYED_URL}/healthz"
5. Organization Sharing & Access Control
When sharing reusable workflows across repositories within an enterprise, repository access permissions dictate who can call them:
- Public Repositories: Reusable workflows in public repositories are globally accessible by any caller workflow across all GitHub repositories.
- Private & Internal Repositories: By default, private repositories cannot be accessed by other repositories. Organization administrators must configure access under Repository Settings -> Actions -> General -> Access:
- Not accessible: Reusable workflows can only be called from within the same repository.
- Accessible from repositories in the 'organization' organization: Any repository in the same organization can call the reusable workflow.
- Accessible from specified repositories: Only explicitly listed repositories within the organization are authorized to invoke the workflow.
+-----------------------------------------------------------------------------+
| ORGANIZATION ACCESS PERMISSION SETTINGS |
| |
| [Central Security Repo: 'org/ci-templates' (Private)] |
| | |
| +---> Setting: "Accessible from repositories in 'org' organization" |
| | |
| +---> Repo 'org/frontend-app' (Can invoke workflow via uses:) |
| +---> Repo 'org/payment-svc' (Can invoke workflow via uses:) |
| x---> Repo 'external-org/app' (Access Denied / Forbidden) |
+-----------------------------------------------------------------------------+
6. Nesting Limits & Matrix Orchestration
Workflow Nesting Depth Limits
GitHub Actions supports nested reusable workflows, where a caller workflow calls a reusable workflow, which in turn calls another reusable workflow.
[!IMPORTANT] Nesting depth limit: GitHub Actions lets you connect up to ten levels of workflows. The older four-level ceiling that many third-party guides still quote has been raised - check the current limit before answering from memory.
Fan-out limit: a single workflow file may call a maximum of 50 unique reusable workflows, and that count includes every workflow reachable through the nested tree. For example,
top-level-caller.yml->called-1.yml->called-2.ymlcounts as 2 reusable workflows against the 50 budget.Inaccessible nested workflow fails the whole run: if any workflow in the chain is not accessible to the initial caller, the run fails - accessibility is not re-evaluated at each hop.
Permissions only narrow going down: in a chain A -> B -> C, if A holds
packages: read, neither B nor C can obtainpackages: write. TheGITHUB_TOKENpermissions passed from a caller can be downgraded by the called workflow but never elevated.
Calling Reusable Workflows Across a Matrix
You can combine reusable workflows with matrix strategies to deploy across multiple regions or operating systems simultaneously:
jobs:
multi-region-deploy:
name: Deploy to Region
strategy:
matrix:
region: [us-east-1, eu-central-1, ap-southeast-1]
tier: [primary, standby]
uses: ./.github/workflows/reusable-deploy.yml
with:
region-name: ${{ matrix.region }}
deployment-tier: ${{ matrix.tier }}
secrets: inherit
Architectural Rules & Limitations:
- No
steps:inside caller job: A job that specifiesuses:cannot contain asteps:array. - No
env:at caller job level: Environment variables cannot be defined directly on the caller job that uses a reusable workflow; values must be passed aswith:inputs. - Inherited Runner Context: The runner environment (
runs-on) is defined inside the called workflow's individual jobs, not in the caller job.
A DevOps engineer needs to call a reusable workflow located in a private repository in the same organization. The reusable workflow requires five distinct API tokens and database connection strings. What is the most maintainable syntax to pass all available caller secrets without declaring each one individually under the job's secrets: block?
A platform team is designing a chain of reusable workflows in which a caller invokes a shared build workflow, which in turn invokes a shared security-scan workflow. What are the current published limits on nesting and fan-out for reusable workflows?
An engineer authors a caller workflow in .github/workflows/release.yml and wants to invoke a reusable workflow defined in the same repository at .github/workflows/build-test.yml. Which syntax correctly specifies the local reusable workflow reference?