How do GitHub Actions reusable workflows differ from composite actions, and when should you use each?
Quick Answer
Reusable workflows are entire workflow files called with the 'uses' keyword at the job level via workflow_call, running as a separate job with their own runner. Composite actions bundle multiple steps into a single action called at the step level, running within the caller's job and runner. Use reusable workflows for standardizing entire CI/CD pipelines across repos, and composite actions for packaging a group of related steps into a shareable unit.
Detailed Answer
Think of reusable workflows and composite actions like the difference between hiring a catering company versus giving your chef a recipe card. A reusable workflow is the catering company: they show up with their own kitchen (runner), their own staff (job context), and deliver the finished product. You tell them the menu (inputs) and they handle everything independently. A composite action is the recipe card: your existing chef (current job runner) follows the steps in your kitchen, using your ingredients (environment, workspace), and the result is part of the same meal (job) you are already cooking.
Reusable workflows are defined as standard workflow YAML files with an on: workflow_call trigger. They accept typed inputs (string, boolean, number) and secrets, and they output values that the calling workflow can consume. When a calling workflow references a reusable workflow with jobs: build: uses: org/shared-workflows/.github/workflows/ci.yml@main, GitHub spins up an entirely separate job with its own runner, workspace, and environment context. The reusable workflow can contain multiple jobs internally, each with their own steps, and those jobs appear in the calling workflow's visualization as nested entries. Reusable workflows support up to four levels of nesting (a reusable workflow calling another reusable workflow) and can be stored in a dedicated repository that the entire organization references.
Composite actions are defined in an action.yml file with runs: using: composite. They contain a sequence of steps, each of which can run shell commands or call other actions. When a workflow step references a composite action with uses: org/actions/setup-deploy@v2, those steps execute inline within the calling job on the same runner, sharing the workspace, environment variables, and file system. Composite actions accept inputs and produce outputs, but they do not have their own runner or job boundary. They are essentially a macro that expands into additional steps in the calling job. This makes them lighter weight than reusable workflows but also means they inherit and can modify the caller's environment.
In production, the decision between the two depends on the scope of what you are standardizing. Use reusable workflows when you want to enforce an entire pipeline pattern: your sre-team might maintain a standard deployment workflow that includes building the container, running security scans, deploying to staging, running smoke tests, and promoting to production. Every service team calls this single reusable workflow, ensuring consistent deployment practices across fifty repositories. Use composite actions when you need to package a smaller, reusable piece of functionality: a setup-node-and-cache action that installs Node.js, restores the npm cache, and runs npm ci, or a notify-slack action that formats and sends a deployment notification. Composite actions are ideal for DRY-ing up repeated step sequences within workflows.
A key gotcha with reusable workflows is that they cannot access the calling workflow's environment variables or secrets unless explicitly passed through the inputs and secrets declarations. If you forget to pass a secret, the reusable workflow gets an empty value with no warning, potentially causing silent deployment failures. Use secrets: inherit as a shortcut to pass all secrets, but be aware this grants the reusable workflow access to every secret in the calling repository. For composite actions, the gotcha is that shell defaults are not inherited: every run step in a composite action must explicitly declare its shell (shell: bash), otherwise it defaults to the system shell which may differ between Ubuntu and Windows runners. Also, composite actions cannot use if conditionals on the action level, only on individual steps within the action.