What are reusable workflows and composite actions — when to use each?
Quick Answer
Reusable workflows are complete workflow files called from other workflows using the 'uses' key at the job level, providing full job-level encapsulation with their own runners and secrets. Composite actions bundle multiple steps into a single action used at the step level, sharing the caller's runner and environment. Use reusable workflows for standardized multi-job pipelines and composite actions for shared step sequences.
Detailed Answer
Imagine you run a franchise of coffee shops. A reusable workflow is like your complete franchise operating manual — it defines entire shifts (jobs) with their own staff (runners) and procedures. A composite action is more like a laminated recipe card posted at the espresso machine — it bundles a sequence of steps that any barista (runner) at any shop can follow within their existing shift. The franchise manual operates independently with its own resources, while the recipe card is embedded into whatever shift is already running.
Reusable workflows are standalone YAML files in .github/workflows/ that declare 'on: workflow_call' as their trigger. They accept typed inputs and secrets, can contain multiple jobs with their own runner specifications, and return outputs. The calling workflow references them with 'uses: org/repo/.github/workflows/file.yml@ref' at the job level. Composite actions, defined in an action.yml file, combine multiple 'run' steps and 'uses' references into a single action that callers invoke at the step level. They accept inputs via the 'inputs' key and produce outputs, but they execute on the caller's runner, sharing its filesystem and environment variables.
Under the hood, when a reusable workflow is called, GitHub's orchestrator fetches the referenced YAML file at the specified git ref, validates the inputs against the declared schema, and merges the called workflow's jobs into the caller's dependency graph. Each job in the reusable workflow gets its own runner — completely isolated from the caller's runner. Secrets must be explicitly passed or inherited using 'secrets: inherit'. Composite actions, by contrast, are resolved at step execution time: the runner downloads the action repository, reads action.yml, and injects the composite steps inline into the current job. The composite steps share the same workspace, PATH modifications, and environment variables as surrounding steps, which enables tight integration but also means side effects can leak.
In production, the decision framework is clear. Use reusable workflows when you need to standardize entire pipelines across repositories — for example, a platform team providing a blessed CI/CD pipeline that product teams must use. Reusable workflows enforce consistency at the job level, support environment protection rules, and can be versioned via git tags. Use composite actions when you need to share a common sequence of steps — like setting up a specific toolchain, running a standard lint-and-format check, or performing authenticated Docker builds. Composite actions are more granular and composable; you can use multiple composite actions within a single job alongside other steps.
A critical gotcha is the nesting limitation: reusable workflows can only be nested up to four levels deep, and a reusable workflow cannot call another reusable workflow from within a composite action step. Another trap is secret handling — reusable workflows require explicit secret passing (or 'secrets: inherit'), and if you forget, the called workflow silently receives empty values rather than throwing an error. With composite actions, a common mistake is assuming they can use 'secrets' context directly — they cannot. Secrets must be passed as inputs from the calling workflow. Additionally, composite actions do not support service containers or the 'if' conditional on individual steps in older runner versions, which can cause unexpected behavior.