Reusable Workflows & Composite Actions
Reusable Workflows & Composite Actions
As a platform grows, the same CI/CD patterns repeat across dozens of repositories: lint, test, build Docker image, push to registry, deploy to staging. Copy-pasting workflow YAML is the fastest path to an unmaintainable mess — a security patch or runner upgrade has to be applied in 40 places. GitHub Actions provides two orthogonal mechanisms to break that cycle: reusable workflows and composite actions. Knowing which tool fits which problem, and how to version both correctly, is what separates a professional CI platform from a pile of duplicated YAML.
Reusable Workflows — workflow_call
A reusable workflow is a normal workflow file that declares on: workflow_call as one of its triggers. Any other workflow can then invoke it as if it were a single job — the callee runs in full isolation with its own jobs, runners, and environment. This is the right tool when you want to share entire pipeline stages including multi-job logic, environment gates, and OIDC credentials.
Reusable workflows support typed inputs (string, boolean, number, choice) and secrets passing, and can emit outputs for the caller to consume. Inputs are declared under on.workflow_call.inputs; secrets under on.workflow_call.secrets.
A caller in any repository in the same organization invokes this with the uses key at the job level — not the step level:
uses key on a job accepts three reference forms: org/repo/.github/workflows/file.yml@ref for cross-repo, ./.github/workflows/file.yml for same-repo (no @ref), and the special org/.github repository as a shared organization-level store. Always pin cross-repo calls to a tag or SHA — never use a mutable branch like @main in production; a breaking change there will silently break every caller on next run.Composite Actions
A composite action is a reusable sequence of steps packaged in an action.yml file. Unlike reusable workflows, composite actions run as steps inside an existing job — they share the runner, the workspace, and all environment variables of the calling job. This makes them the right abstraction for encapsulating a repeatable step sequence (install dependencies, configure a tool, run a linter) without the overhead of a separate job.
Create the action in a repository under any path (conventionally .github/actions/<name>/action.yml for same-repo actions, or as the root action.yml for a standalone action repo).
Every run step inside a composite action must declare a shell — GitHub cannot infer it from context the way it can in a regular workflow. Forgetting this is the most common authoring error.
uses: my-org/.github/actions/setup-node-pnpm@abc1234), audit changes in code review, and never use a mutable tag in a security-sensitive pipeline.Architecture — When to Use Which
Organization-Wide Standards with a Central Workflow Repository
Large engineering organizations store all canonical reusable workflows and composite actions in a single shared repository — by convention named .github under the organization (e.g. my-org/.github). GitHub gives this repo two special powers: its .github/workflows/ directory is the default look-up path for workflow_call references using the short form, and its workflow-templates/ directory populates the "Actions" new-workflow UI for every repository in the org.
Pair the shared repo with required workflows (configured in Organization Settings → Actions → Required workflows). A required workflow runs on every pull request in every repository in the org — regardless of what the repo's own CI does. This is how platform teams enforce security scans, license checks, or SBOM generation without asking every dev team to opt in.
v1, v1.2, v1.2.3). Callers pin to @v1 (a lightweight tag you move forward on patch releases), giving you the ability to push non-breaking improvements without callers updating their YAML. Introduce breaking changes under @v2 and maintain both in parallel during a migration window — the same model used by actions/checkout and actions/setup-node.Common Failure Modes
- Missing
shellon compositerunsteps — the action fails with a cryptic error. Everyruninsideruns.using: compositeneedsshell: bash(orpwsh,python, etc.). - Passing secrets as inputs to composite actions — secrets auto-inherit; passing them as inputs exposes the values in the workflow summary log. Pass secrets only to reusable workflows via
on.workflow_call.secrets. - Circular calls — a reusable workflow calling itself (directly or via a chain) is a hard error. GitHub detects and blocks cycles up to three levels deep.
- Depth limit — reusable workflows can be nested up to four levels deep. Hitting this usually signals the abstraction is too fine-grained; consolidate.
- Mutable ref on cross-repo call —
@mainon a caller means a force-push or accidental commit to the shared repo immediately breaks all callers. Always use a tag or SHA.