---
title: 'GitHub Actions: Reusable Workflow vs Composite Action 2026'
description: 'Reusable workflow vs composite action in GitHub Actions: jobs vs steps, secrets, nesting limits, and outputs—plus exactly when to use each, with YAML.'
url: https://nerdleveltech.com/github-actions-reusable-workflow-vs-composite-action
language: en
date: '2026-06-26'
category: cloud-devops
tags:
  - github actions
  - reusable workflows
  - composite actions
  - ci-cd
  - devops
  - workflow
  - cloud-devops
  - tutorial
markdown_url: https://nerdleveltech.com/github-actions-reusable-workflow-vs-composite-action.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/github-actions-reusable-workflow-vs-composite-action'
terms: https://nerdleveltech.com/llms.txt
---

# 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:`.[^vs] 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.[^vs] On GitHub.com you can nest up to **10 levels** of workflows and call up to **50 unique** reusable workflows from one file.[^limits] 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.[^vs] The practical consequences flow from that one distinction, and GitHub's own documentation lays them out in a comparison table.[^vs]

| | Reusable workflow | Composite action |
| --- | --- | --- |
| What it is | A YAML file, like a standard workflow | An action bundling workflow steps |
| Lives in | A single file in `.github/workflows/` | A repo or directory with `action.yml` |
| Invoked | Directly within a job (`jobs.<id>.uses`) | As a step within a job (`steps[].uses`) |
| Jobs | Can contain multiple jobs | Cannot contain jobs |
| Runner | Each job sets its own `runs-on` | Runs on the caller step's runner |
| Logging | Every job and step logged separately | Logged as one step |
| Secrets | Can use secrets | Cannot use the `secrets` context |
| Marketplace | Cannot be published | Can be published |
| Nesting | Up to 10 levels of workflows | Up 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.[^vs]

## 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.[^vs]

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."[^vs]

## 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:`.[^reuse] The reusable file must live directly in `.github/workflows/`—subdirectories are not supported.[^reuse]

```yaml
# .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:`.[^reuse]

```yaml
# .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.[^reuse] (`actions/checkout` is at `v6` as of June 2026.[^checkout])

## 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:`.[^meta] The key rule developers miss: **every `run` step inside a composite action must specify a `shell:`**—there is no default.[^meta]

```yaml
# .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:

```yaml
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> }}`.[^meta] Note that, unlike a top-level workflow, a composite action does **not** automatically expose its inputs as environment variables—pass them explicitly where needed.[^meta]

## 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.[^vs][^secrets] GitHub's runner team noted in the original design that secrets support in composite actions was deferred to a future design.[^secrets]

```yaml
# caller passes the secret in as an input
      - uses: ./.github/actions/deploy
        with:
          token: ${{ secrets.DEPLOY_TOKEN }}
```

```yaml
# 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.[^vs]

## 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`.[^reuse] The `inherit` keyword implicitly passes the caller's secrets and works for workflows in the same organization or enterprise.[^reuse]

```yaml
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.[^reuse] Second, `on.workflow_call` does not support the `environment` keyword, so environment-scoped secrets cannot be passed in from the caller.[^reuse]

## 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.[^reuse][^limits] Loops in the workflow tree are not permitted, and permissions can only be **maintained or reduced** through the chain, never elevated.[^reuse]

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.[^limits] Composite actions have their own limit—you can nest up to **10 composite actions** in one workflow.[^vs] Separately, a matrix can generate up to **256 jobs** per workflow run.[^matrix]

```yaml
# 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>`.[^reuse] A composite action exposes outputs at the top level, mapping each to a step output.[^meta]

```yaml
# 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"`.[^reuse][^meta]

## 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.[^vs]

## 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.[^reuse]

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

## FAQ

**What is the main difference between a reusable workflow and a composite action?**
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.[^vs]

**Can a composite action use secrets?**
No. The `secrets` context is unavailable inside composite actions—pass the secret as an input from the caller instead.[^vs][^secrets]

**How do I pass all secrets to a reusable workflow?**
Use `secrets: inherit` in the calling job to implicitly pass the caller's secrets (same organization or enterprise).[^reuse]

**How many reusable workflows can I nest?**
Up to 10 levels of workflows on GitHub.com, and a maximum of 50 unique reusable workflows from a single workflow file, including nested trees.[^limits]

**Can a composite action contain jobs or pick its own runner?**
No. Composite actions cannot contain jobs and run on the caller step's runner; if you need separate jobs or runners, use a reusable workflow.[^vs]

**Can a reusable workflow be published to the GitHub Marketplace?**
No—only actions (including composite actions) can be published to the Marketplace.[^vs]

**Why does my composite action `run` step fail to parse?**
Every `run` step in a composite action must include a `shell:` key; unlike workflow steps, there is no default shell.[^meta]

**Can a composite action call a reusable workflow?**
No. Composite actions only contain steps; reusable workflows are invoked at the job level, so the call direction is reusable-workflow-uses-composite-action, not the reverse.[^vs]

## 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.[^vs]

From here, wire your reusable pipeline into real deployments with [keyless GitHub Actions deploys to AWS using OIDC and Terraform](https://nerdleveltech.com/github-actions-oidc-aws-terraform-keyless-deploys-tutorial) or [GCP Workload Identity Federation](https://nerdleveltech.com/github-actions-gcp-workload-identity-federation-tutorial), and make your reusable CI fail loudly with [Vitest coverage thresholds that break the build](https://nerdleveltech.com/vitest-coverage-thresholds-fail-ci-tutorial).

[^vs]: 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
[^reuse]: 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
[^limits]: 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
[^matrix]: 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
[^meta]: 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
[^secrets]: 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
[^checkout]: actions/checkout releases — v6.0.3 is the current latest (released 2026-06-02). https://github.com/actions/checkout/releases
