Skip to content
Dev Tools Beginner Tutorial

Build Reusable GitHub Actions with Composite Actions and workflow_call

Turn duplicated CI YAML into one composite action and one reusable workflow you can call from any repo.

Rachel Goldstein
Rachel Goldstein
Dev Tools Editor · Sep 8, 2026 · 4 min read
Build Reusable GitHub Actions with Composite Actions and workflow_call

What you'll build

A tiny Node project whose CI is split into two shareable pieces: a composite action that installs Node and dependencies, and a reusable workflow (triggered by workflow_call) that runs the tests. Your main CI file shrinks to six lines, and both pieces are ready to be called from other repos.

Prerequisites

  • A GitHub account and git installed.
  • Node.js 24 (current LTS) locally, with npm.
  • The GitHub CLI (gh), authenticated via gh auth login.
  • Verified against GitHub Actions docs as of September 2026, actions/checkout v7.0.1, and actions/setup-node v7.0.0. Any OS works; commands assume a POSIX shell (on Windows, use Git Bash or WSL).

1. Scaffold a Node project

mkdir actions-reuse-demo && cd actions-reuse-demo
git init
npm init -y
npm pkg set scripts.test="node --test"
npm install --package-lock-only
mkdir -p test .github/actions/node-setup .github/workflows

The --package-lock-only flag generates package-lock.json without a node_modules folder. CI needs that lockfile because npm ci refuses to run without one.

Create test/math.test.js:

const test = require('node:test');
const assert = require('node:assert');

test('adds two numbers', () => {
  assert.strictEqual(1 + 1, 2);
});

2. Create the composite action

A composite action bundles several steps into one uses: line. Create .github/actions/node-setup/action.yml:

name: Node setup
description: Install a pinned Node.js version and project dependencies
inputs:
  node-version:
    description: Node.js version to install
    required: false
    default: '24'
runs:
  using: composite
  steps:
    - uses: actions/setup-node@v7
      with:
        node-version: ${{ inputs.node-version }}
    - run: npm ci
      shell: bash

Two rules trip people up here. The file must be named action.yml (or action.yaml), and every run: step needs an explicit shell:, because a composite action doesn't know what runner it'll execute on.

3. Create the reusable workflow

A reusable workflow goes further than a composite action: it defines whole jobs, including the runner. The workflow_call trigger is what makes it callable. Create .github/workflows/ci-reusable.yml:

name: Reusable Node CI

on:
  workflow_call:
    inputs:
      node-version:
        description: Node.js version to test against
        required: false
        type: string
        default: '24'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: ./.github/actions/node-setup
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm test

Note the layering: the reusable workflow owns checkout and the runner, then delegates setup to the composite action. Callers only see one input.

4. Call it from your CI workflow

Create .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]

jobs:
  test:
    uses: ./.github/workflows/ci-reusable.yml
    with:
      node-version: '24'

The uses: sits at the job level, not inside steps:. Local reusable workflows use the ./ prefix with no @ref; cross-repo calls use the full form owner/repo/.github/workflows/ci-reusable.yml@v1.

5. Push and run

git add -A
git commit -m "CI via composite action and reusable workflow"
gh repo create actions-reuse-demo --private --source=. --push

The push to main triggers the CI workflow immediately.

Verify it works

gh run watch

You should see the nested job name and a green result:

✓ main CI · 17234567890
Triggered via push about 1 minute ago

JOBS
✓ test / test in 24s (ID 48123456789)

Open the job log (gh run view --log) and confirm the test output:

✔ adds two numbers (0.8ms)
ℹ tests 1
ℹ pass 1
ℹ fail 0

Troubleshooting

  • error parsing called workflow ... workflow was not found: the path or ref is wrong. Local calls must start with ./ and take no @ref; cross-repo calls need the full owner/repo/.github/workflows/file.yml@ref path. If the workflow lives in another private repo, enable access under that repo's Settings > Actions > General > Access ("Accessible from repositories owned by ..."), which only works within the same user account or organization.
  • Required property is missing: shell: a run: step in your composite action lacks shell: bash. Composite actions require it on every run: step; there's no default.
  • npm ci fails with The 'npm ci' command can only install with an existing package-lock.json: you didn't commit the lockfile. Run npm install --package-lock-only, commit, and push.
  • Secrets are empty inside the called workflow: reusable workflows don't see the caller's secrets by default. Add secrets: inherit to the calling job, or declare each secret under on.workflow_call.secrets and pass it explicitly. Passing secrets via with: won't work; inputs and secrets are separate channels.

Next steps

To share across repos, tag releases of the repo holding your reusable workflow (git tag v1) and call it by tag, so consumers upgrade deliberately. Reference the composite action cross-repo as owner/repo/.github/actions/node-setup@v1; inside a shared reusable workflow, prefer that full form over ./ paths, which resolve against whatever repo the caller checked out. From there, read GitHub's docs on reusing workflows for outputs, matrix calls, and the ten-level nesting limit.

Sources & further reading

  1. Reuse workflows — docs.github.com
  2. Create a composite action — docs.github.com
  3. Share across private repositories — docs.github.com
  4. actions/checkout releases (v7.0.1) — github.com
  5. actions/setup-node releases (v7.0.0) — github.com
Rachel Goldstein
Written by
Rachel Goldstein · Dev Tools Editor

Rachel has been embedded in the developer tooling ecosystem for nearly eight years, covering everything from IDE wars and package-manager drama to the quiet rise of AI-assisted coding. She has a soft spot for open-source maintainers and an unhealthy number of terminal emulators installed on a single laptop.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading