INTERMEDIATEGITHUB-ACTIONSREUSABLE WORKFLOWS

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.

By Tested on GitHub Actions ubuntu-latest, Node 24

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 test

The 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 build

Three 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 on workflow_call, or use secrets: 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 can needs: setup where setup is 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 @v1 or a SHA, never @main. Otherwise upstream changes can break every consumer.

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).

X (Twitter)LinkedIn