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:
-
Custom actions can be called after running other code. Since they run as a step, the caller can place them before/after doing arbitrary things. Need to generate the source code that should be deployed before calling the
deployaction? Need to use a file generated by thescan-vulnerabilitiesaction? You can do that easily if you use a custom action, but need to update the reusable workflow if you want to implement that there. -
Custom actions can be placed anywhere in the file tree. This is especially useful for DevOps teams that provide functionality for other teams. Reusable workflows must be placed in
.github/workflowsjust like all other workflows. This makes it somewhat cumbersome to colocate documentation alongside them, or separate them from your test workflows. Custom actions on the other hand can be placed anywhere, so you can organize them in a way that makes sense for your setup. -
Custom actions can implement open-ended functionality. Do you have a
my-orgCLI that can do 20 different things? If you implement asetup-my-org-cliaction, you can let the caller decide what to do with it. -
Custom actions can be implemented in other languages. It’s a lot easier to test JS-based actions than GHA workflows. Custom actions can be implemented in JS, which - despite having its own issues - is definitely recommended when your actions start growing past a manageable amount of YAML.
-
Custom actions can natively be impacted by GitHub Environments. GitHub implements manual gates through something called ‘GitHub Environments’. These can be attached to jobs, but - unless the reusable workflow has been designed to take an environment name as an input - cannot be attached to reusable workflows.
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.
- Custom actions can pass data easily. Since custom actions run within the same container as the surrounding caller code, data can be passed in or out of them in a variety of ways: GHA-native inputs/outputs, environment variables, or files in the file system. Reusable workflows must pass data across a container boundary, meaning either GHA-native inputs/outputs (for simple data) or workflow artifacts (for complex data). GitHub also silently blocks secrets from passing the container boundary, so watch out for that.
User Experience
-
Reusable workflows expands into a useful UI. Reusable workflows expand into the same UI users are familiar with from their own workflows: jobs and steps are easy to navigate individually. Custom actions are instead displayed as one long step, even if it internally consists of multiple steps. For longer functionality this makes it very difficult to follow along.
-
Reusable workflows are shorter to call, but use longer paths. Reusable workflows hide the entire ‘steps’ aspect of workflows, giving callers slightly fewer things to think about. If your organization favors ease-of-use over flexibility, this might be worth valueing.
call-reusable-workflow.yml on:pull_request:jobs:call-workflow-passing-data:uses: my-org/reusable-workflows/.github/workflows/reusable-workflow.yml@v6call-custom-action.yml on:pull_request:jobs:my_job:runs-on: ubuntu-lateststeps:- uses: my-org/actions/my-custom-action@v6
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.