Skip to main content
GitHub Actions intermediate Lesson 4 of 5

GitHub Actions: Reusable Workflows & Composite Actions

Share CI/CD logic across repositories using reusable workflows (workflow_call) and composite actions—avoid duplication without losing flexibility.

When every repository duplicates the same build-and-push steps, a single bug fix means updating dozens of files. Reusable workflows and composite actions let you define CI logic once and call it from many workflows.

Learning outcomes

By the end you can:

  • create a reusable workflow with workflow_call
  • pass inputs and secrets to reusable workflows
  • create a composite action with action.yml
  • choose between reusable workflows and composite actions

1) Reusable workflows — share entire workflow files

A reusable workflow is a normal workflow file with workflow_call as a trigger. Other workflows call it with uses: owner/repo/.github/workflows/file.yml@ref.

Define the reusable workflow

# .github/workflows/build-and-push.yml  (in your central repo or the same repo)

name: Build and Push Docker Image

on:
  workflow_call:
    inputs:
      image_name:
        required: true
        type: string
      registry:
        required: false
        type: string
        default: ghcr.io
      tag:
        required: false
        type: string
        default: ${{ github.sha }}
    secrets:
      REGISTRY_PASSWORD:
        required: true

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Log in to registry
        uses: docker/login-action@v3
        with:
          registry: ${{ inputs.registry }}
          username: ${{ github.actor }}
          password: ${{ secrets.REGISTRY_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: ${{ inputs.registry }}/${{ inputs.image_name }}:${{ inputs.tag }}

Call the reusable workflow

# In any repository's workflow file

name: CI

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  docker:
    needs: test
    uses: myorg/shared-workflows/.github/workflows/build-and-push.yml@main
    with:
      image_name: myorg/myapp
      tag: ${{ github.sha }}
    secrets:
      REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}

2) Passing outputs from reusable workflows

# Reusable workflow: define outputs
on:
  workflow_call:
    outputs:
      image_tag:
        description: "The Docker image tag that was built"
        value: ${{ jobs.build.outputs.image_tag }}

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image_tag: ${{ steps.set_tag.outputs.tag }}
    steps:
      - id: set_tag
        run: echo "tag=${{ github.sha }}" >> $GITHUB_OUTPUT

      - name: Build Docker image
        run: docker build -t myapp:${{ steps.set_tag.outputs.tag }} .
# Caller: use the output
jobs:
  build:
    uses: ./.github/workflows/build-and-push.yml@main
    # ...

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy image
        run: kubectl set image deployment/myapp myapp=${{ needs.build.outputs.image_tag }}

3) Composite actions — reusable step sequences

A composite action lives in a directory with an action.yml file. It composes multiple steps and can be called from any workflow step with uses:.

Create the composite action

# Directory: .github/actions/setup-node-project/action.yml

name: Setup Node Project
description: Checkout, setup Node, install dependencies with caching

inputs:
  node-version:
    description: Node.js version to use
    required: false
    default: "20"
  working-directory:
    description: Directory containing package.json
    required: false
    default: "."

runs:
  using: composite
  steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Set up Node
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: npm
        cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json

    - name: Install dependencies
      shell: bash
      working-directory: ${{ inputs.working-directory }}
      run: npm ci

Use the composite action

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Setup project
        uses: ./.github/actions/setup-node-project
        with:
          node-version: "20"

      - name: Run tests
        run: npm test

4) When to use each

Reusable WorkflowComposite Action
ScopeFull workflow (jobs + runners)Steps within a single job
Runs onIts own runnersCaller job’s runner
JobsCan contain multiple jobsSteps only
SecretsExplicitly passedInherited from caller
Best forSharing multi-job pipelinesSharing step sequences

5) Full example: organisation-wide CI pattern

# Caller: every service repo uses this pattern
name: Service CI/CD

on:
  push:
    branches: [main]
  pull_request:

jobs:
  ci:
    uses: myorg/platform/.github/workflows/node-ci.yml@main
    with:
      node-version: "20"
      run-e2e: ${{ github.ref == 'refs/heads/main' }}
    secrets: inherit    # pass all caller secrets to the reusable workflow

  deploy:
    if: github.ref == 'refs/heads/main'
    needs: ci
    uses: myorg/platform/.github/workflows/deploy.yml@main
    with:
      environment: production
      image: myorg/${{ github.event.repository.name }}
    secrets: inherit

Next steps

  • Deployment workflows: OIDC, environments, and production gates
  • Composite actions as a local action library
  • Semantic versioning and release automation

Frequently Asked Questions

What is the difference between a reusable workflow and a composite action?
A reusable workflow is a full .github/workflows file called with workflow_call—it runs jobs on its own runners. A composite action lives in action.yml and composes steps inline within an existing job. Use reusable workflows for multi-job flows; use composite actions for step-level reuse.
Can a reusable workflow call another reusable workflow?
Yes, up to a nesting depth of 3 levels as of current GitHub limits. A caller calls a reusable workflow that itself calls another reusable workflow.