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 Workflow | Composite Action | |
|---|---|---|
| Scope | Full workflow (jobs + runners) | Steps within a single job |
| Runs on | Its own runners | Caller job’s runner |
| Jobs | Can contain multiple jobs | Steps only |
| Secrets | Explicitly passed | Inherited from caller |
| Best for | Sharing multi-job pipelines | Sharing 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.