GitHub Actions: Reusable Workflow vs Composite Action 2026
June 26, 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 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.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
-
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
-
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
-
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 -
actions/checkout releases — v6.0.3 is the current latest (released 2026-06-02). https://github.com/actions/checkout/releases ↩
-
GitHub Docs, "Metadata syntax for GitHub Actions" — composite
runs.using, requiredshellon run steps, inputs and outputs. https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 -
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
-
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 ↩