Composite Actions: Between Step and Workflow Size
Bundle repeated step groups into reusable composite GitHub Actions that run inside the calling job with no extra checkout, to keep your pipeline files DRY.
Netbay Engineering
Netbay Engineering
On this page
You have five steps — check out, set up Node, install deps, cache, run lint — that you paste into every job in every repo. That is a smell. A reusable workflow is overkill because you want these steps in the same job, sharing the same runner and working tree. The answer is a composite action: a named bundle of steps you can invoke with uses from anywhere, like a reusable action but built from your own run blocks and other actions. This post shows when to use one and how to build it.
The gap a composite action fills
Think of the size scale. A single step is one run or one action. A job is a unit of environment. A workflow is a collection of jobs. In between, you sometimes want to group several steps that must execute in the same job and be reusable. That is the slot composite actions occupy — smaller than a reusable workflow, more substantial than a step, and idiomatic for "setup the toolchain" type logic.
The decisive difference from a reusable workflow: a composite action runs inside the calling job, so it shares the checkout, the working directory, and any environment the job already set. You pay no new runner startup cost and no separate checkout.
Structure of a composite action
A composite action lives in a directory with a action.yml file plus any scripts it needs. The action.yml declares name, description, inputs, and the steps to run under runs.using: composite. Here is a small example that sets up Node with caching:
name: setup-node-cache
description: checkout, setup node and install deps with cache
inputs:
node-version:
required: true
type: string
runs:
using: composite
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- run: npm ci
shell: bash
working-directory: ${{ github.action_path }}One important detail: because a composite action runs inside the caller's job, run steps must specify a shell explicitly using the shell key; it does not inherit automatically. Here shell: bash is set. Inside the action folder, scripts referenced by a relative path resolve relative to the action root, which you can reach via github.action_path.
Publishing and calling the composite action
The action lives in a repository (or a subdirectory of one). Callers reference it with uses pointing at a ref, exactly like reusable workflows — but this time it appears as a step inside a job. A caller looks like this:
jobs:
lint:
runs-on: ubuntu-22.04
steps:
- uses: acme-org/setup-node-cache@v1
with:
node-version: 20
- run: npm run lint
test:
runs-on: ubuntu-22.04
steps:
- uses: acme-org/setup-node-cache@v1
with:
node-version: 20
- run: npm testBoth jobs reuse the same composite action, but each gets its own checkout and cache because each is its own runner. That is the trade-off you accept when grouping at the job boundary rather than the step boundary.
Passing inputs and secrets
Inputs are declared in action.yml and referenced inside the action as inputs.NAME. For secrets, there is no dedicated secrets container in a composite action — pass what you need as an input or read it from the environment. The cleanest pattern is to declare an input and forward the secret at the call site:
# action.yml
inputs:
npm-token:
required: false
type: string
runs:
using: composite
steps:
- run: echo "//npm.pkg.dev/:_authToken=${{ inputs.npm-token }}" > ~/.npmrc
shell: bashCallers then write with npm-token referring to a secret. Keep secrets out of logs by never echo-ing them intentionally, and be aware that downloading a composite action runs code from the referenced ref, so pin it and review what it executes.
Keeping composite actions maintainable
Prefer a composite action when the grouping is a well-understood, stable unit (toolchain setup, release-local test, deploy assist) that you reuse in multiple jobs. Resist making one action path-specific or branch-specific. If the grouping spans multiple operating systems or needs its own environment, that is the reusable-workflow tier again. Composite actions shine precisely because they are small, focused, and composable.
Takeaway
When a step feels too small and a reusable workflow feels too big, reach for a composite action. It reuses a bundle of steps inside the same job with no runner startup and no extra checkout, and it keeps your pipeline files DRY. Testing a composite action against a real target is easy on a spare Netbay Ubuntu VPS — provision one in under 60 seconds at netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance