Reusable Workflows & Custom Actions
Once an organization has more than a few repositories, copy-pasted GitHub Actions YAML becomes a maintenance problem. A security fix or a runtime upgrade has to land in fifty places, and they drift apart. GitHub Actions offers two ways to share logic. Reusable workflows share whole jobs (and pipelines of jobs). Custom actions, whether composite, JavaScript, or Docker, share steps.
Used well, they let a platform team offer "golden path" pipelines: every service gets standardized, secure build, test, and deploy stages by calling one versioned workflow with a few inputs.
TL;DR
- Reusable workflow: a workflow with
on: workflow_call, called as a job withuses: org/repo/.github/workflows/x.yml@v1. It shares whole jobs, runners, environments, and secrets. - Composite action: a
action.ymlbundling steps, used as a step withuses: org/actions/setup@v1. It runs inside the caller's job. - JavaScript actions run Node code with the Actions toolkit; Docker actions run a container (Linux only).
- Define typed inputs, outputs, and secrets (or
secrets: inherit) as the interface. - Version with tags (
@v1), and pin third-party actions to commit SHAs. - Reusable workflows nest up to 10 levels deep, and a run can call up to 50 distinct workflows in total.
Quick Example
A reusable Node CI workflow in a central repository:
A service repository calling it:
Core Concepts
Reusable Workflows
A workflow becomes reusable by adding the workflow_call trigger. Callers invoke it at the job level with uses: instead of runs-on/steps. Characteristics:
- It can contain multiple jobs with their own runners,
needs, matrices, services, and environments (so deployment approvals work). - Inputs are typed (
string,number,boolean); secrets are passed explicitly or viasecrets: inherit(same organization or enterprise). - Outputs map from its jobs' outputs.
- The caller's
permissionscap what the called workflow'sGITHUB_TOKENcan do. - Callers can use
strategy.matrixon the calling job to invoke the workflow multiple times.
Composite Actions
A composite action is an action.yml with runs.using: composite and a list of steps. It runs inside the caller's job, on the caller's runner:
JavaScript and Docker Actions
- JavaScript actions (
runs.using: node20or later) run a bundled script using@actions/coreand@actions/github. They start fast, run on every OS, and suit logic too complex for YAML, such as calling APIs or parsing files. - Docker container actions run a container image, so they can use any language, but they're Linux-only and slower to start because the image must build or pull.
Choosing the Right Mechanism
Versioning and Distribution
- Tag releases with semver (
v2.3.1) and maintain a moving major tag (v2) that callers pin to for compatible updates. - Internal actions can live in private or internal repositories. Allow access under the repository's Actions settings, so other repos in the organization can use them.
- Third-party actions should be pinned to a full commit SHA (with a version comment), since tags can be moved by the author or an attacker. Dependabot updates SHA pins. See GitHub Actions security.
- Treat breaking input changes like any API change: bump the major version and document the migration.
Best Practices
Keep Interfaces Small and Opinionated
A reusable workflow with twenty inputs is a harder-to-read copy of YAML. Encode standards (security scanning, caching, least-privilege permissions) internally, and expose only what genuinely varies between services.
Test Shared Workflows Before Release
Maintain example caller repositories or a test workflow in the template repo that exercises each input path. A broken @v2 breaks every service's CI at once.
Document Inputs, Outputs, and Required Permissions
Callers need to know which secrets and permissions to grant (for example id-token: write for OIDC deploys). Put it in the README and in description fields.
Prefer Explicit Secrets for Cross-Boundary Calls
secrets: inherit is convenient inside one organization, but it passes every secret. For sensitive pipelines, pass only the secrets the workflow needs.
Common Mistakes
Mixing Up Where uses: Goes
Composite actions go under steps; reusable workflows replace a job's runs-on/steps.
Forgetting shell: in Composite Actions
Every run: step in a composite action must declare shell: bash (or pwsh, etc.). Omitting it is a validation error.
Referencing a Branch Instead of a Version
uses: acme/ci-templates/.github/workflows/deploy.yml@main means any push to the template's main instantly changes every consumer's pipeline, untested. Pin to tags and roll out updates deliberately.
FAQ
Reusable workflow or composite action?
Use a reusable workflow to standardize entire jobs or pipelines, especially with multiple jobs, environments, or approvals. Use a composite action to package a sequence of steps that callers slot into their own jobs, like "set up toolchain" or "publish coverage".
Can a reusable workflow access the caller's secrets?
Only those passed explicitly under secrets: or all of them with secrets: inherit. Environment secrets come from environments referenced inside the called workflow, not from the caller's job.
Can reusable workflows be private?
Yes. A workflow in a private or internal repository can be called by other repositories in the same organization or enterprise once access is enabled in that repository's Actions settings. Public repositories can call reusable workflows in other public repositories.
How do I debug a failing reusable workflow?
The caller's run page shows each called job with full logs. Enable debug logging by setting the ACTIONS_STEP_DEBUG secret or variable to true. Test changes on a branch by pointing a test caller at @your-branch before tagging a release.
Related Topics
- GitHub Actions — The platform overview
- GitHub Actions Workflow Syntax — Jobs, steps, and outputs
- GitHub Actions Security — Pinning actions and scoping secrets
- Platform Engineering — Golden paths for delivery
- CI/CD — Pipeline design in general