GitHub Actions Reusable Workflows

Reusable workflows let one workflow call another workflow as if it were a single step. Instead of copying the same job definitions across multiple repositories, you write the logic once and call it from many places. This reduces duplication, enforces consistency, and makes large workflow configurations much easier to maintain.

The Problem Reusable Workflows Solve

Imagine a company with 50 repositories. Each repository needs a deployment workflow that runs the same six steps: checkout, build, test, security scan, push to registry, and deploy. Without reusable workflows, the team copies these steps into 50 workflow files. When the security scan step changes, someone edits 50 files.

Without reusable workflows:
  repo-1/.github/workflows/deploy.yml  → 60 lines
  repo-2/.github/workflows/deploy.yml  → 60 lines (copy)
  repo-3/.github/workflows/deploy.yml  → 60 lines (copy)
  ...change needed → update 50 files

With reusable workflows:
  shared-repo/.github/workflows/deploy.yml  → 60 lines (source of truth)
  repo-1/.github/workflows/ci.yml           → 5 lines (calls shared)
  repo-2/.github/workflows/ci.yml           → 5 lines (calls shared)
  ...change needed → update 1 file

Creating a Reusable Workflow

A reusable workflow uses workflow_call as its trigger instead of push or pull_request. This marks it as a callable workflow rather than one that starts on its own.

# .github/workflows/deploy-template.yml
# This workflow is called by other workflows

name: Shared Deployment Workflow

on:
  workflow_call:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        type: string
      app-version:
        description: 'Version to deploy'
        required: false
        type: string
        default: 'latest'
    secrets:
      deploy-token:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy to ${{ inputs.environment }}
        run: ./deploy.sh ${{ inputs.environment }} ${{ inputs.app-version }}
        env:
          TOKEN: ${{ secrets.deploy-token }}

Inputs

The inputs block defines values the caller must or can provide. Each input has a type (string, boolean, or number), a description, whether it is required, and an optional default value.

Secrets

The secrets block declares which secrets the caller must pass. The reusable workflow cannot access the caller's secrets directly — they must be explicitly passed through this mechanism.

Calling a Reusable Workflow

In the caller workflow, use uses at the job level (not the step level) to reference the reusable workflow:

# .github/workflows/production-deploy.yml
# In any repository that needs to deploy

name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  production-deployment:
    uses: my-org/shared-workflows/.github/workflows/deploy-template.yml@main
    with:
      environment: 'production'
      app-version: '2.4.1'
    secrets:
      deploy-token: ${{ secrets.PROD_DEPLOY_TOKEN }}
Reference format:
  owner/repo/.github/workflows/filename.yml@ref

  owner   → GitHub user or organization
  repo    → Repository containing the workflow
  @ref    → Branch, tag, or commit SHA

Calling a Reusable Workflow in the Same Repository

You can also call a workflow from within the same repository using a relative path:

jobs:
  run-tests:
    uses: ./.github/workflows/test-template.yml
    with:
      test-suite: 'integration'

Passing Secrets Automatically

If the reusable workflow needs many secrets, you can forward all caller secrets at once using inherit:

jobs:
  deploy:
    uses: my-org/shared/.github/workflows/deploy.yml@main
    secrets: inherit

With inherit, every secret available in the caller's repository is automatically available in the reusable workflow. Use this carefully — only when the reusable workflow is trusted and maintained by your own team.

Outputs from Reusable Workflows

A reusable workflow can return outputs back to the caller:

# In the reusable workflow:
on:
  workflow_call:
    outputs:
      deployed-version:
        description: 'Version that was deployed'
        value: ${{ jobs.deploy.outputs.version }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.get-version.outputs.version }}
    steps:
      - id: get-version
        run: echo "version=2.4.1" >> $GITHUB_OUTPUT
# In the caller workflow:
jobs:
  deploy-step:
    uses: ./.github/workflows/deploy.yml

  notify:
    needs: deploy-step
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deployed ${{ needs.deploy-step.outputs.deployed-version }}"

Nesting Reusable Workflows

A reusable workflow can itself call another reusable workflow, up to a maximum nesting depth of 4 levels. This allows you to compose complex pipelines from smaller building blocks.

release.yml
  └── calls build.yml
        └── calls test.yml
              └── calls security-scan.yml

Reusable Workflows vs Composite Actions

Feature                  | Reusable Workflow   | Composite Action
-------------------------|---------------------|--------------------
Level                    | Job level           | Step level
Has its own runner       | Yes                 | No (uses caller's)
Can have multiple jobs   | Yes                 | No
Can use secrets          | Yes (explicitly)    | Inherited
Defined in               | .github/workflows/  | action.yml file
Called with              | uses (in job block) | uses (in step block)

Use reusable workflows when you want to share full pipelines with multiple jobs. Use composite actions when you want to share a group of steps within a single job.

Leave a Comment

Your email address will not be published. Required fields are marked *