Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXXXIX · PipelinesPipelines

Reusable workflows — DRY at the workflow level, what they enable, what they cost

Intermediate⏱ ~22 mingit

What you'll learn

  • Declare a reusable workflow with the workflow_call trigger
  • Call a reusable workflow from a caller workflow with inputs and secrets
  • Distinguish a reusable workflow from a composite action
  • Identify the four DRY patterns reusable workflows replace and their costs
  • Recognise the over-engineering threshold where a reusable workflow is wrong

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A reusable workflow is a workflow file that another workflow can call. The caller passes inputs and secrets; the callee runs as if it were a job in the caller’s DAG, but in a separate runner, with its own log, its own status, and its own artifact namespace. The mechanism is workflow_call on the callee and uses: on the caller. The effect is DRY at the workflow level: one definition, N callers.

Declaring a reusable workflow

# .github/workflows/terraform-apply.yml (the callee)
name: Terraform Apply

on:
  workflow_call:
    inputs:
      environment:
        type: string
        required: true
      working-directory:
        type: string
        required: false
        default: terraform/
    secrets:
      aws-role-to-assume:
        required: true

jobs:
  apply:
    runs-on: ubuntu-latest
    environment: ${ inputs.environment }
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${ secrets.aws-role-to-assume }
      - run: terraform apply -auto-approve ${ inputs.working-directory }

The on: workflow_call: trigger declares the workflow as callable. inputs: defines the typed parameters the caller must pass. secrets: declares the secrets the caller must forward. The callee’s jobs run with the inputs and secrets the caller provided.

Calling a reusable workflow

# .github/workflows/deploy-production.yml (the caller)
name: Deploy Production

on:
  push:
    branches: [main]

jobs:
  apply-production:
    uses: org/repo/.github/workflows/terraform-apply.yml@main
    with:
      environment: production
      working-directory: terraform/production/
    secrets:
      aws-role-to-assume: ${ secrets.AWS_DEPLOY_ROLE }

The uses: clause references the callee by path and ref. The with: clause passes inputs; the secrets: clause forwards secrets. The callee’s jobs are inlined into the caller’s DAG; the caller’s downstream jobs can depend on the callee’s outputs.

flowchart LR
    C["Caller workflow"] -->|"uses: org/repo/.github/workflows/callee.yml@main"| W["Reusable workflow"]
    W --> J1["apply job"]
    W --> J2["plan job"]

Inputs, secrets, and outputs

The three data channels between caller and callee:

flowchart TB
    C["Caller"] -->|"inputs (typed parameters)"| W["Reusable workflow"]
    C -->|"secrets (forwarded)"| W
    W -->|"outputs (return values)"| C
  • Inputs are typed parameters declared in on.workflow_call.inputs. The caller passes them via with:. Inputs are visible in the caller’s run page and the callee’s run page.
  • Secrets are sensitive values declared in on.workflow_call.secrets. The caller forwards them via secrets:; the callee references them as ${ secrets.<name> }. The caller cannot pass a secret the callee has not declared.
  • Outputs are values the callee returns to the caller. The callee declares them in jobs.<id>.outputs; the caller reads them via needs.<callee-job>.outputs.<name>.
# Callee declaring outputs
jobs:
  apply:
    outputs:
      apply-id: ${ steps.apply.outputs.id }
    steps:
      - id: apply
        run: terraform apply ...
# Caller consuming outputs
jobs:
  notify:
    needs: [apply-production]
    runs-on: ubuntu-latest
    steps:
      - run: echo "apply id: ${ needs.apply-production.outputs.apply-id }"

Reusable workflow versus composite action

Two DRY mechanisms; different scopes:

flowchart TB
    Q{"What is being\nextracted?"}
    Q -->|"A sequence of steps"| A["Composite action\n(action.yml)"]
    Q -->|"A whole pipeline"| B["Reusable workflow\n(workflow_call)"]
  • Composite action is a single step that bundles multiple commands. Declared in action.yml. Reused via uses: in a step. The composite runs in the caller’s job; it cannot have its own runner, environment, or matrix.
  • Reusable workflow is a whole pipeline extracted into a callee. Declared in a workflow file with workflow_call. Reused via uses: in a job. The callee runs in its own job; it can have its own runner, environment, matrix, and concurrency group.

A composite action is the right tool for “I have a sequence of shell commands I want to share”. A reusable workflow is the right tool for “I have a job (or jobs) I want to share with its own runner and environment”.

The four patterns reusable workflows replace

flowchart LR
    subgraph P1["1. Copied workflow files"]
        A1["repo1/.github/workflows/build.yml"]
        A2["repo2/.github/workflows/build.yml"]
        A3["repo3/.github/workflows/build.yml"]
    end
    subgraph P2["2. Local scripts in repos"]
        B1["scripts/deploy.sh in repo1"]
        B2["scripts/deploy.sh in repo2"]
    end
    subgraph P3["3. Shared CI templates"]
        C1["internal-template@v1"]
        C2["internal-template@v2"]
    end
    subgraph P4["4. Composite actions doing too much"]
        D1["action.yml with runner + secrets"]
    end
  1. Copied workflow files. N repositories with N copies of the same workflow. A bug fix in repo1’s copy is not in repo2’s copy. Reusable workflows replace this with one callee referenced by N callers.
  2. Local scripts in repos. N repositories with N copies of scripts/deploy.sh. The reusable workflow replaces the script with a job; the script becomes the step inside the callee.
  3. Shared CI templates. A “template” repository that N callers copy from. The template is a snapshot, not a reference; updates are pull requests, not live. Reusable workflows are referenced live, by SHA or branch.
  4. Composite actions doing too much. An action.yml that wants its own runner, environment, or matrix. The composite action is the wrong scope; the reusable workflow is the right one.

The costs

Reusable workflows impose three costs that the team must pay deliberately:

  • Coupling. A change to the callee is a change to every caller. The team must review the callee’s PR with the caller’s PR in mind; the coupling is across repositories, not just within one.
  • Debugging distance. A failure in the callee is a failure whose logs live in a different run page. The engineer must follow the uses: reference to the callee and dig through a second run page to find the failure.
  • Review surface. A reusable workflow is a library; its API (inputs, secrets, outputs) is a contract. A breaking change to the contract is a breaking change to every caller. The reviewer must check the contract, not just the implementation.

When the reusable workflow is the wrong tool

The reusable workflow is over-engineering when:

flowchart TB
    Q{"Is the duplication real?"}
    Q -->|"two callers, divergent soon"| NO["Copy the workflow"]
    Q -->|"two callers, identical forever"| YES["Reusable workflow"]
    Q -->|"three callers, divergent in places"| MAYBE["Reusable workflow with care"]
  • Two callers, divergent soon. The duplication is incidental; the workflows will diverge. A reusable workflow forces them back into sync; the divergence is now a breaking change to the contract.
  • Three callers, but the callee has more callers than the team can review. The coupling is wider than the review surface; breaking changes will slip through.
  • The callee is one job, one step. A reusable workflow for a single step is a composite action that happens to have a runner. Use a composite action.

Production discipline

  1. The reusable workflow is a library; review its API (inputs, secrets, outputs) as a contract. A breaking change to the API is a breaking change to every caller.
  2. Pin the uses: reference to a SHA or a release tag. A branch reference (@main) tracks the latest commit; a SHA reference is immutable.
  3. Add a test caller in the callee repository. The test caller exercises every input combination; it is the contract test for the callee.
  4. Document the inputs and outputs in the callee’s README. A caller who cannot read the callee’s contract from its documentation is a caller who will guess and break.
  5. Audit the callers on every breaking change. A reusable workflow with N callers has N callers to update. The audit is the price of the DRY.

Cross-course references

  • Ansible for Production Sysadmins - Part XXXVII (RepoArch) applies the same pattern to Ansible roles: a role is a library; its variables are its API; its defaults are its outputs.
  • Terraform for Production Sysadmins - Part XIII (Modules) treats Terraform modules the same way: a module is a library; its variables are its API; its outputs are its return values.
  • Linux for Production Sysadmins - Part XXXIV (ConfigMgmt) applies the same pattern to apt package repositories: a repository is a library; its Packages file is its API.

Quiz

Knowledge check · 4 questions

  1. Q1. A team has extracted `terraform apply` into a reusable workflow with `on: workflow_call`. The reusable workflow declares one input (`environment`) and one secret (`aws-role-to-assume`). The caller passes the secret in `secrets:`. What happens at runtime if the caller forgets to forward the secret?

  2. Q2. A reusable workflow with no caller in production is dead code; the team should either find a caller or delete the workflow.

  3. Q3. State the trigger keyword that marks a workflow as reusable and the keyword the caller uses to invoke it.

  4. Q4. Diagnose why a breaking change to a reusable workflow broke three production deploys simultaneously and recommend a release discipline.

    Team T maintains a reusable workflow `terraform-apply.yml` referenced by three caller repositories: `infra-prod`, `infra-staging`, and `infra-dev`. The callee's author renames the input `working-directory` to `tf-dir` and merges the change to `main`. The next deploy from each caller fails because the callee no longer accepts the old input. All three production deploys are blocked.

Passing score: 75%. Answers are checked in this browser.