cloud-devops

GitHub Actions: Reusable Workflow vs Composite Action 2026

June 26, 2026

GitHub Actions: Reusable Workflow vs Composite Action 2026

A reusable workflow packages one or more jobs and is called at job level with uses:; a composite action packages steps and runs as one step in a job. Use a reusable workflow for multiple jobs, runners, or secrets; use a composite action to bundle Marketplace-publishable steps.

TL;DR

Both features kill copy-paste in GitHub Actions, but at different levels. A reusable workflow is a .github/workflows file triggered by on: workflow_call; it can contain multiple jobs, pick its own runners, take secrets, and you call it from a job with uses:.1 A composite action is an action.yml with runs.using: "composite"; it bundles steps into one reusable step, can be published to the Marketplace, but cannot access the secrets context or contain jobs.1 On GitHub.com you can nest up to 10 levels of workflows and call up to 50 unique reusable workflows from one file.2 Everything below is verified against the current GitHub docs (© 2026) with copy-paste YAML.

What you'll learn

  • The core difference between a reusable workflow and a composite action
  • A decision checklist for when to use each
  • How to define and call a reusable workflow (inputs, secrets, uses:)
  • How to write and use a composite action (action.yml, shell:, outputs)
  • Whether a composite action can use secrets, and the safe workaround
  • How to pass secrets to reusable workflows, including secrets: inherit
  • The current nesting and matrix limits
  • How to return outputs to the caller from each
  • Whether the two can call each other

What's the difference between a reusable workflow and a composite action?

A reusable workflow reuses an entire workflow (one or more jobs and their steps); a composite action reuses a bundle of steps that run as a single step within a job.1 The practical consequences flow from that one distinction, and GitHub's own documentation lays them out in a comparison table.1

Reusable workflowComposite action
What it isA YAML file, like a standard workflowAn action bundling workflow steps
Lives inA single file in .github/workflows/A repo or directory with action.yml
InvokedDirectly within a job (jobs.<id>.uses)As a step within a job (steps[].uses)
JobsCan contain multiple jobsCannot contain jobs
RunnerEach job sets its own runs-onRuns on the caller step's runner
LoggingEvery job and step logged separatelyLogged as one step
SecretsCan use secretsCannot use the secrets context
MarketplaceCannot be publishedCan be published
NestingUp to 10 levels of workflowsUp to 10 composite actions in one workflow

One more subtlety on placement: because a composite action runs as a step, you can put steps before and after it in the same job. A reusable workflow is called directly within a job, so you cannot add steps after the call in that job—and therefore cannot use GITHUB_ENV to pass values to later steps of the calling job.1

When should you use a reusable workflow vs a composite action?

Choose a reusable workflow when the thing you are reusing is shaped like a pipeline; choose a composite action when it is shaped like a step. If your reused logic must run multiple jobs, fan out across runners, gate on environments, or consume secrets, only a reusable workflow can express that.1

Reach for a reusable workflow when you need any of:

  • More than one job, or jobs with needs: dependencies
  • Jobs that run on different runners (for example, build on Linux, sign on macOS)
  • Secrets, deployment environments, or approval gates
  • Real-time per-step logs across the whole pipeline

Reach for a composite action when you need:

  • To bundle a handful of sequential steps into one reusable step
  • Steps before and after the reused logic in the same job
  • To publish to the GitHub Marketplace
  • To stay on the caller's runner with minimal overhead

GitHub frames the runner point directly: "if the steps must be run on a type of machine that might be different from the machine chosen for the calling workflow job, then you should use a reusable workflow, not a composite action."1

How do you create and call a reusable workflow?

Add on: workflow_call to a workflow file, declare any inputs and secrets, then call it from a job with uses:.3 The reusable file must live directly in .github/workflows/—subdirectories are not supported.3

# .github/workflows/deploy.yml  (the reusable workflow)
name: Reusable deploy
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
    secrets:
      deploy_token:
        required: true
    outputs:
      url:
        description: "Deployed URL"
        value: ${{ jobs.deploy.outputs.url }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    outputs:
      url: ${{ steps.ship.outputs.url }}
    steps:
      - uses: actions/checkout@v6
      - id: ship
        run: echo "url=https://${{ inputs.environment }}.example.com" >> "$GITHUB_OUTPUT"
        env:
          TOKEN: ${{ secrets.deploy_token }}

Call it at the job level. Inputs go under with: and their data type must match the type declared in the called workflow; named secrets go under secrets:.3

# .github/workflows/ci.yml  (the caller)
name: CI
on:
  push:
    branches: [main]

jobs:
  call-deploy:
    uses: octo-org/repo/.github/workflows/deploy.yml@v1
    with:
      environment: staging
    secrets:
      deploy_token: ${{ secrets.DEPLOY_TOKEN }}

You reference the file as {owner}/{repo}/.github/workflows/{file}@{ref}, or ./.github/workflows/{file} for a workflow in the same repository. The {ref} can be a SHA, a release tag, or a branch; if a tag and branch share a name the tag wins, and a commit SHA is the safest option for stability and security.3 (actions/checkout is at v6 as of June 2026.4)

How do you create and use a composite action?

Create an action.yml (or action.yaml) with runs.using: "composite" and a list of steps, then call it as a step with uses:.5 The key rule developers miss: every run step inside a composite action must specify a shell:—there is no default.5

# .github/actions/setup-build/action.yml
name: Setup and build
description: Install deps and build the project
inputs:
  node-version:
    description: Node.js version
    required: false
    default: "22"
outputs:
  artifact-path:
    description: Path to the build output
    value: ${{ steps.build.outputs.path }}
runs:
  using: composite
  steps:
    - uses: actions/setup-node@v6
      with:
        node-version: ${{ inputs.node-version }}
    - name: Install
      run: npm ci
      shell: bash
    - id: build
      run: |
        npm run build
        echo "path=dist" >> "$GITHUB_OUTPUT"
      shell: bash

Use it as a step, with steps before and after if you like:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - name: Build via composite
        id: bp
        uses: ./.github/actions/setup-build
        with:
          node-version: "22"
      - name: After
        run: echo "built at ${{ steps.bp.outputs.artifact-path }}"

Reference inputs with ${{ inputs.<name> }} and step outputs with ${{ steps.<id>.outputs.<name> }}.5 Note that, unlike a top-level workflow, a composite action does not automatically expose its inputs as environment variables—pass them explicitly where needed.5

Can a composite action use secrets?

No. The secrets context is not available inside a composite action; the documented approach is to pass the secret as an input from the calling workflow.16 GitHub's runner team noted in the original design that secrets support in composite actions was deferred to a future design.6

# caller passes the secret in as an input
      - uses: ./.github/actions/deploy
        with:
          token: ${{ secrets.DEPLOY_TOKEN }}
# action.yml reads it via inputs, not the secrets context
inputs:
  token:
    required: true
runs:
  using: composite
  steps:
    - run: ./deploy.sh
      shell: bash
      env:
        TOKEN: ${{ inputs.token }}

If you need native secret handling—or the secret should never be threaded through an input—use a reusable workflow instead, since reusable workflows accept secrets directly.1

How do you pass secrets to a reusable workflow?

Declare each secret under on.workflow_call.secrets, then pass it from the caller with the secrets: keyword—either named, or all at once with secrets: inherit.3 The inherit keyword implicitly passes the caller's secrets and works for workflows in the same organization or enterprise.3

jobs:
  call-deploy:
    uses: ./.github/workflows/deploy.yml
    with:
      environment: production
    secrets: inherit   # pass all of the caller's secrets

Two caveats worth knowing. First, in a nested chain, secrets reach only the directly called workflow: in a chain A → B → C, workflow C receives A's secret only if A passed it to B and B then passed it to C.3 Second, on.workflow_call does not support the environment keyword, so environment-scoped secrets cannot be passed in from the caller.3

How many levels can you nest reusable workflows?

On GitHub.com you can connect a maximum of ten levels of workflows—the top-level caller plus up to nine reusable workflows—and you can call a maximum of 50 unique reusable workflows from a single workflow file, counting nested trees.32 Loops in the workflow tree are not permitted, and permissions can only be maintained or reduced through the chain, never elevated.3

These figures were raised over time—many older guides and forum answers still cite the earlier limits of four levels and 20 workflows, so trust the current numbers above.2 Composite actions have their own limit—you can nest up to 10 composite actions in one workflow.1 Separately, a matrix can generate up to 256 jobs per workflow run.7

# a mid-level reusable workflow calling a lower-level one
name: Mid-level reusable
on:
  workflow_call:
jobs:
  call-another:
    uses: octo-org/repo/.github/workflows/lowlevel.yml@v1

How do you return outputs to the caller?

A reusable workflow maps step outputs up to job outputs, then to workflow outputs under on.workflow_call.outputs; the caller reads them through needs.<job>.outputs.<name>.3 A composite action exposes outputs at the top level, mapping each to a step output.5

# reading a reusable workflow's outputs in the caller
jobs:
  job1:
    uses: octo-org/repo/.github/workflows/deploy.yml@v1
    with:
      environment: staging
    secrets: inherit
  job2:
    runs-on: ubuntu-latest
    needs: job1
    steps:
      - run: echo "Deployed to ${{ needs.job1.outputs.url }}"

For a composite action, the consuming step reads ${{ steps.<id>.outputs.<name> }} (shown earlier as steps.bp.outputs.artifact-path). In both cases, step outputs are written with echo "name=value" >> "$GITHUB_OUTPUT".35

Can a composite action call a reusable workflow?

No—a composite action contains steps, not jobs, and reusable workflows are invoked only at the job level, so a composite action cannot call one. The relationship works the other way: a reusable workflow's jobs can use composite actions just like any other action, and a composite action can nest other composite actions.1

Can you use a matrix with a reusable workflow?

Yes. A job that uses a strategy.matrix can call a reusable workflow, producing one run per matrix value—useful for fanning a deploy across environments.3

jobs:
  deploy-all:
    strategy:
      matrix:
        target: [dev, stage, prod]
    uses: octo-org/repo/.github/workflows/deploy.yml@v1
    with:
      environment: ${{ matrix.target }}
    secrets: inherit

Bottom line

Match the tool to the shape of what you're reusing: a composite action for a reusable step, a reusable workflow for a reusable pipeline. If you need jobs, multiple runners, environments, or native secrets, it's a reusable workflow; if you want a lightweight, Marketplace-publishable bundle of steps that lives inside an existing job, it's a composite action.1

From here, wire your reusable pipeline into real deployments with keyless GitHub Actions deploys to AWS using OIDC and Terraform or GCP Workload Identity Federation, and make your reusable CI fail loudly with Vitest coverage thresholds that break the build.

Footnotes

  1. GitHub Docs, "Reusing workflow configurations" — reusable workflows versus composite actions and key-differences table. https://docs.github.com/en/actions/concepts/workflows-and-actions/reusing-workflow-configurations 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

  2. GitHub Docs, "Reusing workflow configurations" (reference), "Limitations of reusable workflows" — up to ten levels of workflows and a maximum of 50 unique reusable workflows from a single workflow file. https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations 2 3 4

  3. GitHub Docs, "Reuse workflows" — workflow_call, calling syntax, secrets: inherit, matrix, nesting, and outputs. https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows 2 3 4 5 6 7 8 9 10 11 12 13 14

  4. actions/checkout releases — v6.0.3 is the current latest (released 2026-06-02). https://github.com/actions/checkout/releases

  5. GitHub Docs, "Metadata syntax for GitHub Actions" — composite runs.using, required shell on run steps, inputs and outputs. https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax 2 3 4 5 6 7

  6. GitHub Docs, "Use secrets in GitHub Actions," and actions/runner ADR 0549 (composite run steps) on the unavailable secrets context. https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets 2 3

  7. GitHub Docs, "Actions limits" — a job matrix can generate a maximum of 256 jobs per workflow run. https://docs.github.com/en/actions/reference/limits

Frequently Asked Questions

A reusable workflow reuses entire jobs and is called at the job level; a composite action reuses a bundle of steps and runs as a single step inside a job. 1