# Reusable Workflows vs Composite Actions in GitHub Actions

> The two ways to stop copy-pasting CI YAML across repos, and the rule I use to pick between them.

URL: https://www.matthewswong.com/en/blog/github-actions-reusable-workflows-composite-actions/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2026-07-14
- Updated: 2026-07-14
- Category: DevOps

> **Key takeaway:** Use a composite action to share a sequence of steps inside one job, like a checkout-and-setup preamble that runs on the caller's runner. Use a reusable workflow to share one or more whole jobs, control the runner, gate environments, and receive secrets. Composite actions plug into a job; reusable workflows are called as a job.

Every team that runs CI on more than a handful of repositories hits the same wall: the same forty lines of checkout, language setup, cache restore, lint, and test get copy-pasted into every workflow file. When the Node version bumps or a cache key changes, you edit it in twelve places and miss two. GitHub Actions gives you two tools to kill that duplication, and they are constantly confused with each other because both are described as reuse. They are not interchangeable.

The distinction is simpler than most articles make it. A composite action bundles steps. A reusable workflow bundles jobs. Once that clicks, almost every decision falls out of it automatically, including where secrets can flow and which runner your code lands on. I run CI across several small services on a self-hosted VPS, and getting this split right is what keeps my deploy logic in one file instead of scattered across every repo.

## The core distinction: steps versus jobs

A composite action is referenced from inside a job's steps list with the same uses syntax you already use for third-party actions. It cannot define its own runner, cannot contain jobs, and its internal steps collapse into a single line in the log. It runs on whatever machine the calling job chose. A reusable workflow is the opposite: it is referenced at the job level, defines its own runner with runs-on, can contain several jobs, and each step logs in real time. That difference in altitude drives everything else.

| Dimension | Composite action | Reusable workflow |
| --- | --- | --- |
| Unit of reuse | A sequence of steps | One or more whole jobs |
| How it is called | As a step (uses inside steps) | At job level (uses on a job) |
| Runner control | Runs on the caller's runner | Defines its own runs-on |
| Secrets | No secrets block; pass values as inputs | Native secrets plus secrets inherit |
| Logging | Collapsed into one step | Each step logged separately |
| Marketplace | Publishable to the Marketplace | Not publishable; referenced by path |

## When to reach for a composite action

Reach for a composite action when several workflows need the same preamble but each one continues differently afterward. The classic case is checkout, set up the language runtime, and restore the dependency cache. Your test job adds test steps after it, your build job adds build steps, and your release job adds packaging steps. A composite action lets each job keep full control of what comes next while sharing the boring setup. You can also stack multiple composite actions and inline steps in the same job in any order, which reusable workflows cannot do.

> Every run step inside a composite action must declare its own shell (for example shell: bash). It is not inherited from the caller, and forgetting it is the single most common reason a composite action fails to load with a confusing validation error.

## When to reach for a reusable workflow

Reach for a reusable workflow when you want to standardize an entire pipeline, not just a preamble. The platform team owns the runner selection, the environment gates, the job graph, and the secrets contract; consuming teams just call it and pass parameters. This is the right tool when the shared logic must run on a specific runner regardless of the caller, when it needs to consume secrets, or when execution depends on if conditionals evaluated at the job level. My deploy pipeline lives here: one reusable workflow defines build, push to the registry, and SSH deploy, and every service repo calls it with a different environment input.

## Passing inputs and secrets, the part everyone gets wrong

Inputs are typed and behave the same for both: you declare them and pass them with the with keyword. Secrets are where the two diverge sharply. A reusable workflow has a first-class secrets block in its workflow_call trigger, and the caller either passes each secret explicitly or uses secrets inherit to hand over all of the calling workflow's secrets at once. Inherit only works when the caller and the called workflow are in the same organization or enterprise.

> Composite actions have no secrets block and cannot read the secrets context on their own. To use a secret inside one you must pass its value in as an ordinary input from a calling step. GitHub still masks any registered secret value in logs, but you lose the typed secrets contract, so treat secret-heavy logic as a signal that you want a reusable workflow, not a composite action.

## Cutting duplication across many repositories

For cross-repo reuse, put both artifacts in one central repository, commonly named something like ci-templates, and reference them from every other repo. Pin your references to a tag or a full commit SHA, never to a moving branch, so a change to the shared logic cannot silently break every downstream pipeline at once. Roll new versions forward with a tag like v1, v2 and let consumers upgrade deliberately. Be aware of the platform ceilings when you compose these: a single workflow can reference a limited number of reusable workflows, and nesting has a maximum depth, so keep chains shallow and flat rather than building deep towers of workflows calling workflows.

> A practical hybrid works best at scale: keep small, generic setup logic as composite actions so any job can compose it, and keep whole standardized pipelines as reusable workflows. A reusable workflow can itself use your composite actions internally, giving you one place for step-level reuse and one place for job-level reuse.

## The decision rule I actually use

- Am I sharing steps that plug into a larger job the caller still controls? Composite action.
- Am I sharing one or more entire jobs, including the runner and the job graph? Reusable workflow.
- Does the shared logic need real secrets or environment gates? Reusable workflow, every time.
- Must it run on a specific runner regardless of who calls it? Reusable workflow.
- Do I want it published on the Marketplace for others to consume? Composite action.

They are complementary, not competing. The teams with the cleanest CI I have seen use both deliberately: composite actions for the reusable step preambles and reusable workflows for the standardized pipelines that consume them. Get the steps-versus-jobs distinction right and the rest is just wiring inputs and, carefully, secrets.

## Sources

- GitHub Docs: Reusing workflow configurations
- GitHub Docs: Reuse workflows (inputs and secrets)
- GitHub Changelog: Simplify using secrets with reusable workflows

## Source links

- https://docs.github.com/en/actions/concepts/workflows-and-actions/reusing-workflow-configurations
- https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows
- https://github.blog/changelog/2022-05-03-github-actions-simplify-using-secrets-with-reusable-workflows/

## FAQ

### What is the main difference between a reusable workflow and a composite action?

A composite action bundles a sequence of steps that plug into a job the caller still controls, and it runs on the caller's runner. A reusable workflow bundles one or more entire jobs, defines its own runner, and is called at the job level rather than as a step. In short, composite actions share steps and reusable workflows share jobs.

### Can a composite action use secrets in GitHub Actions?

Not directly. Composite actions have no secrets block and cannot read the secrets context on their own. To use a secret you must pass its value in as an ordinary input from a calling step. If your shared logic depends heavily on secrets, that is a strong signal to use a reusable workflow instead.

### How do I pass all my secrets to a reusable workflow at once?

Use secrets: inherit on the calling job instead of listing each secret. This hands over all of the calling workflow's secrets to the called workflow. It only works when the caller and the reusable workflow live in the same organization or enterprise.

### How do I share CI logic across many different repositories?

Put your composite actions and reusable workflows in one central repository, often named ci-templates, and reference them from every other repo. Pin references to a tag or full commit SHA rather than a moving branch so a change cannot silently break every downstream pipeline. Publish new versions behind tags like v1 and v2 so consumers upgrade deliberately.

### Why does my composite action fail with a shell error?

Every run step inside a composite action must declare its own shell, such as shell: bash. Unlike normal workflow steps, the shell is not inherited from the caller. Forgetting it is the most common reason a composite action fails to load with a confusing validation error.

---

- HTML version: https://www.matthewswong.com/en/blog/github-actions-reusable-workflows-composite-actions/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/github-actions-reusable-workflows-composite-actions/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
