Reusable Workflows vs Composite Actions: Choosing the Right Abstraction in 2026

Paul Wechuli
February 9, 2026
github-actions
ci-cd
devops
workflows

A practical decision framework for when to reach for reusable workflows, composite actions, or just a plain shell script — with the tradeoffs that actually bite you in production.

Reusable Workflows vs Composite Actions: Choosing the Right Abstraction in 2026

There are now four reasonable ways to share CI logic across repos in GitHub Actions:

  1. A composite action — bundled as action.yml
  2. A reusable workflow — called via uses: org/repo/.github/workflows/foo.yml@ref
  3. A JavaScript / Docker action — full programmatic control
  4. A plain shell script in a shared repo, pulled via actions/checkout

Teams reach for the wrong one constantly, usually because they picked based on what the docs talked about first. Here's the framework I use now, after migrating an org's worth of CI from one to the other twice.

The decision tree

Does it need to control the job itself
(matrix, runner type, permissions, concurrency)?
 ├── Yes → Reusable workflow
 └── No  → Does it need anything beyond steps + inputs?
          ├── No  → Composite action
          └── Yes → JavaScript action (or Docker if you need a fixed toolchain)

The shell-script-in-a-repo option is a smell. It works, but it bypasses the action ecosystem's caching, versioning, and security model. Use it only for genuinely throwaway internal tooling.

What composite actions are actually good at

Composite actions shine when you have a sequence of steps that needs to run inside an existing job, and the surrounding job already controls things like runner labels, permissions, and secrets.

Good fits:

  • "Set up our internal toolchain" (login to ECR, configure git, prime caches)
  • "Publish a build artifact to our internal registry"
  • "Send a Slack notification with our standard formatting"

The thing composite actions still can't do well in 2026:

  • They cannot define permissions: — they inherit from the caller. That's usually fine, but means you can't ship an action that ships its own least-privilege story.
  • They cannot define matrices. A composite action runs once per call.
  • Step-level if: conditions inside composite actions evaluate in the caller's context, which is mostly intuitive but has sharp edges around failure() and always().

What reusable workflows are actually good at

Reusable workflows are the right answer when the shape of the job is what you want to standardize. The classic case is a release pipeline: every service repo should run the same matrix of build/test/scan/sign/push steps, with the same concurrency policy, the same permissions: block, and the same environment gating.

# .github/workflows/release.yml in service-repo
name: Release
on:
  push:
    tags: ["v*"]

jobs:
  release:
    uses: my-org/ci-workflows/.github/workflows/standard-release.yml@v3
    with:
      service-name: payments-api
      runtime: node20
    secrets: inherit
    permissions:
      contents: read
      id-token: write
      packages: write

The caller is six lines. The shared workflow is 200 lines that nobody on the service team has to think about. That's the win.

Where reusable workflows hurt:

  • Output passing is awkward. You can return outputs from a reusable workflow, but they're string-only and limited in size. Returning a JSON blob and parsing it on the other side works but feels gross.
  • Debugging is harder. A failure inside a reusable workflow shows up in the called workflow's run, not the caller's. New engineers find this disorienting.
  • You can't nest forever. Reusable workflows can call other reusable workflows, but only to a depth of four. If you find yourself bumping into this, you've over-abstracted.

The hybrid pattern that works

The combination I keep landing on for medium-sized orgs:

  • One reusable workflow per pipeline shape (build, release, security-scan, infra-deploy).
  • Inside each reusable workflow, the actual work is done by composite actions that wrap the messy parts (cloud login, cache key construction, artifact attestation).
  • JavaScript actions only for things that genuinely need code — anything that walks a directory tree, parses YAML, talks to an API with retries, or transforms data between steps.

This keeps the surface area small. Service teams interact with reusable workflows. Platform teams own composite + JS actions underneath. Each layer has one job.

A note on versioning

Pin reusable workflows and composite actions by tag, not by branch, not by SHA. Tag with a v1, v1.2, v1.2.3 scheme and move the major/minor tags forward as you ship.

The argument for SHA-pinning is supply chain integrity. The argument against is that it makes your shared CI effectively unmaintainable — nobody upgrades, drift accumulates, and the next time you ship a breaking change you discover that 40 repos are still on a six-month-old SHA. Use actions/dependabot or the new Actions immutable references feature to get the security story without making upgrades a project.

What's actually new in 2026

A few things have shifted since this debate started:

  • Immutable action references are now GA and worth turning on org-wide. They make SHA-pinning less necessary for most cases.
  • Reusable workflow outputs can now carry artifact references, which removes one of the bigger pain points around passing data between callers.
  • Composite actions now support pre: and post: steps, which closes a real gap (you can finally write a composite action that sets something up and cleans it up).

If you wrote your shared-CI strategy in 2023, it's worth a revisit. A lot of the workarounds people built — wrapper scripts, marker files, hacky output passing — aren't needed anymore.