GitHub Actions Reusable Workflows
Reusable workflows let one workflow call another workflow as if it were a single step. Instead of copying the same job definitions across multiple repositories, you write the logic once and call it from many places. This reduces duplication, enforces consistency, and makes large workflow configurations much easier to maintain.
The Problem Reusable Workflows Solve
Imagine a company with 50 repositories. Each repository needs a deployment workflow that runs the same six steps: checkout, build, test, security scan, push to registry, and deploy. Without reusable workflows, the team copies these steps into 50 workflow files. When the security scan step changes, someone edits 50 files.
Without reusable workflows:
repo-1/.github/workflows/deploy.yml → 60 lines
repo-2/.github/workflows/deploy.yml → 60 lines (copy)
repo-3/.github/workflows/deploy.yml → 60 lines (copy)
...change needed → update 50 files
With reusable workflows:
shared-repo/.github/workflows/deploy.yml → 60 lines (source of truth)
repo-1/.github/workflows/ci.yml → 5 lines (calls shared)
repo-2/.github/workflows/ci.yml → 5 lines (calls shared)
...change needed → update 1 file
Creating a Reusable Workflow
A reusable workflow uses workflow_call as its trigger instead of push or pull_request. This marks it as a callable workflow rather than one that starts on its own.
# .github/workflows/deploy-template.yml
# This workflow is called by other workflows
name: Shared Deployment Workflow
on:
workflow_call:
inputs:
environment:
description: 'Target environment'
required: true
type: string
app-version:
description: 'Version to deploy'
required: false
type: string
default: 'latest'
secrets:
deploy-token:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to ${{ inputs.environment }}
run: ./deploy.sh ${{ inputs.environment }} ${{ inputs.app-version }}
env:
TOKEN: ${{ secrets.deploy-token }}
Inputs
The inputs block defines values the caller must or can provide. Each input has a type (string, boolean, or number), a description, whether it is required, and an optional default value.
Secrets
The secrets block declares which secrets the caller must pass. The reusable workflow cannot access the caller's secrets directly — they must be explicitly passed through this mechanism.
Calling a Reusable Workflow
In the caller workflow, use uses at the job level (not the step level) to reference the reusable workflow:
# .github/workflows/production-deploy.yml
# In any repository that needs to deploy
name: Deploy to Production
on:
push:
branches: [main]
jobs:
production-deployment:
uses: my-org/shared-workflows/.github/workflows/deploy-template.yml@main
with:
environment: 'production'
app-version: '2.4.1'
secrets:
deploy-token: ${{ secrets.PROD_DEPLOY_TOKEN }}
Reference format:
owner/repo/.github/workflows/filename.yml@ref
owner → GitHub user or organization
repo → Repository containing the workflow
@ref → Branch, tag, or commit SHA
Calling a Reusable Workflow in the Same Repository
You can also call a workflow from within the same repository using a relative path:
jobs:
run-tests:
uses: ./.github/workflows/test-template.yml
with:
test-suite: 'integration'
Passing Secrets Automatically
If the reusable workflow needs many secrets, you can forward all caller secrets at once using inherit:
jobs:
deploy:
uses: my-org/shared/.github/workflows/deploy.yml@main
secrets: inherit
With inherit, every secret available in the caller's repository is automatically available in the reusable workflow. Use this carefully — only when the reusable workflow is trusted and maintained by your own team.
Outputs from Reusable Workflows
A reusable workflow can return outputs back to the caller:
# In the reusable workflow:
on:
workflow_call:
outputs:
deployed-version:
description: 'Version that was deployed'
value: ${{ jobs.deploy.outputs.version }}
jobs:
deploy:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- id: get-version
run: echo "version=2.4.1" >> $GITHUB_OUTPUT
# In the caller workflow:
jobs:
deploy-step:
uses: ./.github/workflows/deploy.yml
notify:
needs: deploy-step
runs-on: ubuntu-latest
steps:
- run: echo "Deployed ${{ needs.deploy-step.outputs.deployed-version }}"
Nesting Reusable Workflows
A reusable workflow can itself call another reusable workflow, up to a maximum nesting depth of 4 levels. This allows you to compose complex pipelines from smaller building blocks.
release.yml
└── calls build.yml
└── calls test.yml
└── calls security-scan.yml
Reusable Workflows vs Composite Actions
Feature | Reusable Workflow | Composite Action
-------------------------|---------------------|--------------------
Level | Job level | Step level
Has its own runner | Yes | No (uses caller's)
Can have multiple jobs | Yes | No
Can use secrets | Yes (explicitly) | Inherited
Defined in | .github/workflows/ | action.yml file
Called with | uses (in job block) | uses (in step block)
Use reusable workflows when you want to share full pipelines with multiple jobs. Use composite actions when you want to share a group of steps within a single job.
