Blog

GitHub Actions - Reusable workflows vs. custom actions.

Published Oct 17, 2025

Within the world of GitHub Actions, reusable workflows and custom (composite) actions solve much of the same problem: callable bundles of functionality, much like functions in normal programming languages.

Since they solve the same problem, it’s very difficult to gauge which one you should use for a particular use case. GitHub has a short document on differences, but I don’t feel that it catches the differences that well. This blog post is my attempt at writing down some of the things I feel that they’ve missed.

I’ll be writing from the point-of-view of a DevOps team offering reusable GitHub Actions functionality to an organization consisting of many developer teams. Some of the observations might not make sense if you’re just writing reusable functionality for yourself.

TL;DR: if you provide reusable functionality to other people, you should almost always use custom actions. If you provide reusable functionality to yourself/your own team, and you don’t need much flexibility, use reusable workflows. In some cases you might have to break that rule of thumb, if you need something that only one of them implements (e.g. JS-based actions, or org-wide required workflows).

A brief summary

GitHub Actions workflows consist of one or more jobs, which each consists of one or more steps. They also contain one or more triggers. You can think of jobs as individual containers, which can be orchestrated as needed, and steps as the instructions that are executed within that container.

That leads us to the most obvious difference between reusable workflows and custom actions: the level at which they wrap functionality. Reusable workflows are essentially entire workflows consisting of one or more jobs (but crucially not a reusable trigger), while custom actions only contains steps. This means that reusable workflows must be called at the root level of your own workflow, while custom actions must be called within an existing job.

It’s very easy to mistakenly think of reusable workflows as “workflows that can be reused”, making them significantly different from custom actions which can be thought of as “functionality that can be called”. Unfortunately that’s not entirely true: workflows have triggers, which is an important part of how they act. Reusable workflows, on the other hand, are simply callable functionality that the caller chooses when to call.
I highlight this because it’s often a mental bias when considering whether to use one or the other: a process like “deploy the app”, which conceptually is an entire workflow, could just as well be a custom action as a reusable workflow.

Differences

Flexibility

The most obvious difference between the two are how flexible they are in different situations:

Reusable workflows… don’t have a lot going for them in terms of flexibility. That can be a good thing though! If your company processes are very strongly opinionated, or you’re writing GHA functionality for your own team and know exactly what you need, it can be a big win to not allow individual call sites to modify the functionality.

If you’re writing generic functionality for an entire organization, with different people wanting to use it in ways you hadn’t planned, that inflexibility is going to come bite you in the foot later.

Data passing

Since reusable workflows consist of jobs, they inherit some of the data passing difficulties that comes with it.

User Experience

OIDC and secret containment

A poorly documented difference between custom actions and reusable workflows is how they impact OIDC tokens.

OIDC tokens are tokens that workflows can generate to authenticate themselves to third parties. It consists of a small JSON Web Token, signed by GitHub, which contains information about what repo the workflow is running in, what caused it to run, etc. It will also contain the claim job_workflow_ref, whose value is a reference to the running job — including the repository, if it’s a reusable workflow.

// sample claims from an OIDC token:
{
"sub": "repo:my-org/team-1:environment:prod",
"repository": "my-org/team-1",
"repository_owner": "my-org",
"job_workflow_ref": "my-org/actions-shared/.github/workflows/push-to-gitops-repo.yml@refs/heads/main",
// ...
}

This means that reusable workflows can be used in cases where you need to tell an external service “it’s alright, I’m from an official reusable workflow”. This could e.g. be fetching secrets from AWS, without allowing that secret to be accessed by anyone else in the organization (although in that case you need to customize the sub claim, since AWS only lets you filter on that part of the OIDC token).

Which one to choose?

I think custom actions are superior in the role of “callable GHA functionality”, especially when it comes to offering functionality from a DevOps team that might not know exactly what the downstream team wants. At my current team we’ve had great success with offering custom actions - either composite or JS-based - with docs alongside them to support users in how to best make use of them (we’ve chosen a monorepo for all of our actions, instead of having one repo for each action - but that’s a talk for another day).

What we’ve also seen is that developer teams usually prefer wrapping their reusable functionality in reusable workflows. Since the implementors are closer to the action in those cases, it’s far easier to know exactly what’s needed at the call site - and the lack of flexibility makes it easier for other team members to just use the reusable workflow without putting too much thought into it.

This approach, with teams implementing reusable workflows if they need them, but the central DevOps team delivering more flexible custom actions, has worked well for us. Your mileage might vary.

my-org/actions my-org/team-1-workflows my-org/team-2-workflows my-org/team-1-app-a my-org/team-1-app-b my-org/team-2-app-a my-org/team-2-app-b Custom actions Reusable workflows Workflows Workflows my-org/team-2-app-c my-org/team-3-app-a Workflows