Reusable GitHub Actions Workflow with workflow_call
Define a reusable Node.js setup workflow once and call it from any other workflow with workflow_call. Inputs, outputs, and secrets supported.
A reusable GitHub Actions workflow that takes a Node.js version as input and handles checkout, setup, install, and caching. Other workflows in the same repo (or even other repos) call it via workflow_call instead of duplicating the same boilerplate. This is the DRY pattern for organizations with many similar repos.
Tested on GitHub Actions ubuntu-latest, Node 24.
When to Use This
- You maintain 5+ similar Node.js repos and want to share CI logic
- Your CI workflows have grown copy-pasted boilerplate
- You want to enforce a standard install flow across teams
- You're building an internal "platform" of shared workflows
Don't use this when you only have one or two workflows (the indirection isn't worth it) or when the shared logic is just steps (use a composite action instead, simpler).
Code
The reusable workflow:
# .github/workflows/setup-node.yml
name: Reusable Node Setup
on:
workflow_call:
inputs:
node-version:
description: 'Node.js version to install'
required: false
type: string
default: '24'
install-command:
description: 'Command to install dependencies'
required: false
type: string
default: 'npm ci'
outputs:
cache-hit:
description: 'Whether the npm cache was hit'
value: ${{ jobs.setup.outputs.cache-hit }}
jobs:
setup:
runs-on: ubuntu-latest
outputs:
cache-hit: ${{ steps.node.outputs.cache-hit }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node ${{ inputs.node-version }}
id: node
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
run: ${{ inputs.install-command }}The caller workflow:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
setup:
uses: ./.github/workflows/setup-node.yml
with:
node-version: '24'
test:
needs: setup
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
- run: npm ci
- run: npm testThe uses: ./.github/workflows/setup-node.yml syntax calls the local reusable workflow. For workflows in other repos, use uses: my-org/shared-workflows/.github/workflows/setup-node.yml@v1.
Usage
A multi-job CI pipeline that calls the reusable workflow once and depends on it for downstream jobs:
# .github/workflows/full-ci.yml
name: Full CI
on:
push:
branches: [main]
pull_request:
jobs:
lint:
uses: ./.github/workflows/setup-node.yml
with:
node-version: '24'
install-command: 'npm ci && npm run lint'
test:
uses: ./.github/workflows/setup-node.yml
with:
node-version: '24'
install-command: 'npm ci && npm test'
build:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
- run: npm ci
- run: npm run buildThree independent jobs (lint, test, build) all rely on the same reusable workflow for their setup phase.
Pitfalls
- Reusable workflows must live in
.github/workflows/. They can't sit in any other directory. The runner won't find them. - Secrets do NOT auto-inherit. Pass them explicitly via the
secrets:block onworkflow_call, or usesecrets: inherit(which forwards all secrets — convenient but less secure). - The reusable workflow runs as a separate job in the calling workflow's run. This means an extra job startup cost (~10-30 seconds) compared to inline steps.
needs:works between caller jobs and reusable jobs. A downstream job canneeds: setupwheresetupis the reusable workflow call.- Outputs from reusable workflows are accessed via
needs.<job>.outputs.<name>. They don't merge into the parent workflow's outputs automatically. - Versioning matters for cross-repo calls. Use
@v1or a SHA, never@main. Otherwise upstream changes can break every consumer.
Related Snippets & Reading
- Next.js CI Workflow — the inline version this replaces
- Matrix Build Across Node Versions — works with reusable workflows too
- GitHub Actions reusing workflows — official docs
Frequently Asked Questions
What's the difference between a reusable workflow and a composite action?
A composite action runs as a single step inside a job — it can't define its own jobs or runners. A reusable workflow is a full workflow with its own jobs, runners, and matrix support, called like a job from another workflow. Use composite actions for shared steps, reusable workflows for shared jobs.
Can a reusable workflow use repo secrets from the caller?
Yes, but you have to pass them explicitly. Reusable workflows do NOT inherit secrets automatically. You either declare them in the secrets block of workflow_call, or pass `secrets: inherit` from the caller (which forwards all secrets, less safe).