Reusable Workflows at Github Actions
Don't Repeat Yourself (DRY) approach for workflow management at Github Actions.
I bet every one of us has been there, building a Python linting workflow—you know, setting up Python, installing black, running it. Seems straightforward enough, right? But then it hits you: if you’re doing the same thing across multiple projects, you’re basically copy-pasting the same YAML into each repo. Boring. Easy to mess up. And a nightmare when you need to update versions or adjust logic. This doesn’t align with the DRY methodology at all!
That’s where reusable workflows step in like a trusty toolkit you reach for again and again. You write your workflow once, then “call” it whenever you need it. Think of it like defining a function in code—you write it once, and call it everywhere. At this article, I am going to walk you through this concept.
Why bother with reusable workflows?
Ever find yourself copy-pasting the same GitHub Actions logic across different repos—setting up Python, installing dependencies, running checks—and thinking, “I know I’ll want to tweak this later”? That deep sigh you feel every time you make a change in one place and realize you now need to update ten more? Reusable workflows cut through that pain.
With a reusable workflow, you write your linting or testing logic once, in one file, and then “call” it from wherever you need. You change it in one place, and boom—all your workflows get the update. It’s like writing a helper function instead of copy-pasting the same code. The appeal isn’t just efficiency—it’s consistency, maintainability, and the peace of mind that your CI logic is cohesive across your projects.
When does it make sense?
Stick with normal workflows when you’re working on one-off logic in a single repo. But if you’re rolling out a pattern across projects—say, a lint check for every Python repo, or a deployment flow for each microservice—that’s prime time for reusable workflows. Especially when those workflows evolve—like bumping Python versions or adding new checks—you want to update once and propagate everywhere effortlessly. Also, reusable workflows let you log clearly (you’ll see each step and job in the run log), accept inputs and secrets, and even call other workflows if you need nesting. It’s cleaner, more powerful than composite actions when your logic spans multiple jobs or needs secret or input flexibility.
Demo: Reusable Linting Workflow
I will use the same linting example from the previous article to demonstrate how to implement reusable workflows. The linting workflow below is a generic GitHub Actions configuration that checks Python code using Python 3.9. It will raise an error if the linting does not pass as expected.
# .github/workflows/python-lint-reusable.yml
name: Python Linting Workflow
on:
workflow_call:
inputs:
python-version:
required: false
type: string
default: '3.9'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ inputs.python-version }}
- run: pip install black
- run: black --check .
workflow_call
is a special trigger that turns a regular GitHub Actions workflow into a reusable one. Unlike triggers like push, pull_request, or schedule that react to repo events, workflow_call signals: “Hey, I’m meant to be invoked by another workflow.” It’s the essential ingredient that makes a workflow callable from elsewhere.
When we add this to your reusable workflow, we are giving permission for other workflows—whether in the same repo or another—to invoke it. The event context that this workflow receives is actually the same as the one from its caller. It’s as if the caller triggered it directly.
Example 1: Calling from the Same Repository
Imagine you’re building in the same project, and you’ve defined a reusable workflow file called python-lint-reusable.yml inside .github/workflows:
# .github/workflows/pr-lint-call.yml
name: PR Lint Check (Same Repo)
on:
pull_request:
branches: [main]
jobs:
run-lint:
uses: ./.github/workflows/python-lint-reusable.yml
with:
python-version: '3.11'
Here, uses: ./.github/workflows/python-lint-reusable.yml points to the local reusable workflow—meaning it runs from the exact same commit as the caller. Pretty convenient and perfectly supported.
Example 2: Calling from Another Repository
Now let’s say you keep your reusable linting logic in a central repo—org/reusable-workflows-repo— and want to call it from team/my-project. Then, in your consuming repo (team/my-project), create a caller workflow:
# .github/workflows/pr-lint-external.yml
name: PR Lint Check (External Repo)
on:
pull_request:
branches: [main]
jobs:
run-lint:
uses: org/reusable-workflows-repo/.github/workflows/python-lint-reusable.yml@main
with:
python-version: '3.10'
You’re specifying the repo path (org/reusable-workflows-repo/.github/workflows/python-lint-reusable.yml), pointing to the main branch (@main). That tells GitHub to fetch that workflow from the external repo and run it as if it were defined in yours. Inputs like python-version work the same as before.
In both scenarios, workflow_call makes your workflow callable—one way to write once and use it anywhere. In the same repo, it’s especially simple (no repo path or version needed), while cross-repo calls grant you powerful centralization—maintain workflows in one place, apply them across many projects.
Conclusion
And there it is—why you’d want reusable workflows, when they make sense, and how to wire one up, both within your repo and across to another. At the end of the day, reusable workflows are your best friend when you want clean, consistent CI/CD logic that doesn’t demand copy-pasting and fiddly edits across projects. Define it once, call it from anywhere, tweak it once and see it everywhere.