Reusable Workflow
A reusable workflow is a GitHub Actions workflow that declares on: workflow_call, so other workflows call it as a job and share one pipeline definition.
A reusable workflow is a GitHub Actions workflow file that declares on: workflow_call, so other workflows can call it as a job rather than copying its steps. One definition of a pipeline then lives in one file, and every caller across repositories or across jobs in the same repository runs that same definition.
The pattern exists because build, test, and release logic tends to be duplicated into every repository that needs it. A reusable workflow turns that duplication into a single file plus a one line reference at each call site.
Definition
A reusable workflow has two halves. The called workflow is an ordinary workflow file that lists workflow_call among its triggers and optionally declares typed inputs, secrets, and outputs. The caller job is a job in another workflow that points at the called workflow with uses instead of listing steps.
The file has to sit directly in the .github/workflows directory of the repository that owns it. Subdirectories of that path are not supported for reusable workflows (GitHub reusable workflows documentation, checked on 2026-08-13). The caller references the file by path and ref: owner/repo/.github/workflows/build.yml@v1, where the ref is a commit SHA, a tag, or a branch. A workflow in the same repository can use the short form ./.github/workflows/build.yml, which always resolves to the ref of the caller's own commit.
What a caller job may contain
A job that calls a reusable workflow accepts a fixed set of keys: name, uses, with, secrets, strategy, needs, if, concurrency, and permissions. GitHub rejects steps and runs-on in that job, because the called workflow owns its own jobs and each of those jobs declares its own runs-on. Machine selection therefore stays inside the reusable file, which is why changing runner labels for a whole organization can be a single edit in one repository.
strategy is worth calling out. A caller job can fan a matrix across the same reusable workflow, so one file drives a build across several language versions or target platforms while the pipeline logic stays in one place.
Inputs, secrets, and outputs
Values cross the boundary through three declared channels:
- Inputs are declared under
on.workflow_call.inputsand passed by the caller underwith. Each input has a type, and the value must match:boolean,number, orstring. - Secrets are declared under
on.workflow_call.secretsand passed by name, or passed as a group withsecrets: inheritwhen the caller and the called workflow belong to the same organization or enterprise. Environment secrets cannot be passed in, becauseon.workflow_callhas noenvironmentkeyword. - Outputs are declared under
on.workflow_call.outputs, each mapped to a job output inside the called workflow. The caller reads them asneeds.<job_id>.outputs.<name>.
Environment variables do not cross the boundary. Anything set in an env context at the workflow level of the caller stays in the caller, so values that the called workflow needs have to arrive as inputs.
Permissions travel one way. GitHub allows the GITHUB_TOKEN permissions of a called workflow to be maintained or reduced through the chain, and rejects any attempt to elevate them, so delegating work to a reusable file is a way to lose access rather than gain it.
Limits worth knowing before you nest
Two documented ceilings shape how far this pattern scales (GitHub reusable workflows reference, checked on 2026-08-13):
| Limit | Documented value |
|---|---|
| Unique reusable workflows called from a single workflow file | 50 |
| Levels of connected workflows | 10, meaning the top level caller plus nine levels below it |
Secrets pass only to the workflow a file calls directly. In a chain from A to B to C, workflow C receives a secret from A only when A passed it to B and B passed it on to C.
Reusable workflow compared with composite action
Both remove duplication, and they operate at different layers.
| Dimension | Reusable workflow | Composite action |
|---|---|---|
| Unit shared | A whole workflow, with jobs | A sequence of steps |
| Where it runs | Its own jobs, each on its own runner | Inside a job the caller already started |
Declares runs-on | Yes, per job in the called file | No, it inherits the caller's machine |
| Called from | jobs.<id>.uses | steps[*].uses |
| Log view | Separate jobs in the run graph | Steps folded into the calling job |
| File location | .github/workflows of the owning repository | Any path containing action.yml |
Example
The called workflow below declares one input, one secret, and one output. It lives at .github/workflows/build.yml in a repository named acme/ci.
name: build
on:
workflow_call:
inputs:
node-version:
required: true
type: string
publish:
required: false
type: boolean
default: false
secrets:
npm-token:
required: true
outputs:
artifact-name:
description: Name of the uploaded build artifact
value: ${{ jobs.build.outputs.artifact-name }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact-name: ${{ steps.pack.outputs.name }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
- run: npm test
- id: pack
env:
NODE_AUTH_TOKEN: ${{ secrets.npm-token }}
run: |
npm pack
echo "name=$(ls *.tgz)" >> "$GITHUB_OUTPUT"
- if: inputs.publish
run: npm publishA workflow in any repository that is allowed to reach acme/ci calls it as a job. The calling job carries no steps of its own:
name: pull-request
on: [pull_request]
jobs:
build:
uses: acme/ci/.github/workflows/build.yml@v1
with:
node-version: "22"
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
report:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "built ${{ needs.build.outputs.artifact-name }}"In the run graph, build appears as its own job with the steps from the called file, and report reads the output the called workflow published. Bumping @v1 to @v2 across the calling repositories is the whole upgrade, and the pipeline steps themselves are edited once in acme/ci.
The same shape with a matrix keeps the fan out at the call site while the steps stay in one file:
jobs:
build:
strategy:
matrix:
node-version: ["20", "22"]
uses: acme/ci/.github/workflows/build.yml@v1
with:
node-version: ${{ matrix.node-version }}
secrets: inheritRelated Terms
- Composite actions and how they differ from reusable workflows: the step level unit of reuse that runs on the caller's machine.
- How to reuse a workflow across repositories: the access settings, ref pinning, and secret passing a cross repository call needs.
- Running reusable workflows across a large set of repositories: versioning, rollout, and runner label changes when many repositories share one workflow file.
- GitHub reusable workflows reference: the upstream syntax and limits, checked on 2026-08-13.
- WarpBuild quick start: pointing a workflow's
runs-onat a managed runner fleet. - WarpBuild pricing: per minute rates by runner type.
FAQ
What is the difference between a reusable workflow and a composite action?
A reusable workflow is a whole workflow file with its own jobs, and each of those jobs declares its own runs-on and gets its own machine. A composite action is a bundle of steps that runs inside a job the caller already started, on the caller's machine. Use a reusable workflow when you want to share job structure, and a composite action when you want to share a sequence of steps.
Can a reusable workflow live in a different repository?
Yes. The caller references it as owner/repo/.github/workflows/file.yml@ref, where ref is a commit SHA, a tag, or a branch. The file has to sit directly in the .github/workflows directory of the repository that owns it, because GitHub does not support subdirectories of that path for reusable workflows.
Why are the caller's environment variables missing inside the reusable workflow?
GitHub does not propagate variables set in an env context at the workflow level of the caller into the called workflow. Pass the values you need as typed inputs under with, and send values back out through outputs declared on on.workflow_call.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.