How to create a reusable GitHub Actions workflow
Create a callable GitHub Actions workflow with inputs, secrets, and outputs so multiple repositories share one tested CI definition instead of copying YAML.
What and why
Copying the same CI YAML into many repositories creates a maintenance trap: when the build logic changes, you must edit every copy, and the copies drift apart over time. A reusable workflow solves this by defining the logic once in one repository and letting other workflows call it with parameters, much like calling a function. This keeps CI consistent across a fleet of repositories and means a single change propagates everywhere that calls it.
Reusable workflows differ from composite actions: a reusable workflow can contain multiple jobs, run on different runners, and use environments, while a composite action bundles steps within a single job. For sharing a whole pipeline, the reusable workflow is the right tool.
Prerequisites
- An existing GitHub Actions workflow you want to share.
- Two or more repositories, or workflows, that need the same logic.
- Familiarity with how triggers, jobs, and contexts work.
Steps
1. Add the workflow_call trigger
In the shared repository, create .github/workflows/reusable-ci.yml and make it callable by exposing the workflow_call event:
name: Reusable CI
on:
workflow_call:
A workflow becomes reusable purely by declaring this trigger. It can still also declare other triggers if it runs standalone too.
2. Declare inputs and secrets
on:
workflow_call:
inputs:
node-version:
type: string
default: '20'
secrets:
npm-token:
required: false
Inputs are typed parameters with optional defaults, forming the workflow's public interface. Secrets are not inherited automatically; callers must pass them explicitly, which keeps secret flow auditable. Mark a secret required: true when the workflow cannot function without it.
3. Use inputs in jobs
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci && npm test
Reference parameters with the inputs context anywhere in the workflow. The job behaves exactly as a normal job; the only difference is where its parameters come from.
4. Define workflow outputs
Expose results back to the caller so it can act on them:
outputs:
result: ${{ jobs.ci.outputs.result }}
This maps a job-level output up to the workflow level. The caller reads it from the needs context of the calling job, enabling patterns like deploy-after-build across the boundary.
5. Call the workflow from a caller
In another repository's workflow, call the shared workflow at the job level:
jobs:
call-ci:
uses: my-org/shared/.github/workflows/reusable-ci.yml@main
with:
node-version: '22'
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
The uses key points to the shared workflow by path and ref. The with block sets inputs and secrets passes secrets through. Note that the caller uses uses at the job level, not the step level.
Verification
Push both repositories and trigger the caller. In the Actions tab, the run displays the reusable workflow nested under the calling job, with its own steps visible. Change node-version in the caller from 22 to 20 and confirm the runtime version in the logs changes without editing the shared file. This proves the parameterization works.
Next Steps
Pin the reusable workflow to a release tag instead of a branch so callers get stable, intentional updates. Add required inputs to enforce correct usage and fail fast on misconfiguration. Combine a reusable workflow with a matrix in the caller to fan out across versions. Document the inputs and outputs so other teams can adopt the shared workflow confidently.
Prerequisites
- A working GitHub Actions workflow
- Two or more repos sharing CI logic
- Understanding of workflow triggers
Steps
- 1Add the workflow_call trigger
- 2Declare inputs and secrets
- 3Use inputs in jobs
- 4Define workflow outputs
- 5Call the workflow from a caller
- 6Verify reuse