Open-source project
spatie/laravel-queueable-action avatar
spatie/laravel-queueable-action

laravel-queueable-action and the case for one business action, two ways to run it

Queueable actions in Laravel

701 stars54 forksPHPMIT

At a glance

What is it?
laravel-queueable-action lets a Laravel action object be executed immediately or pushed onto a queue with a one method difference, and adds chaining, tags, middleware, per-action backoff and test assertions on top. Its value is that the same class is both the command and the job, so there is no second implementation to keep in sync.
Who is it for?
laravel-queueable-action suits a Laravel codebase where business logic sits in action classes and the same logic is needed both inside a request and in the background, because the action class is written once and the execution mode is a method call rather than a second class.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One action class, one method call decides the mode

Actions are a way of structuring business logic in Laravel, and this package adds easy support to make them queueable. The whole interface is two lines.

Executing it right now is the plain call:

php
$myAction->onQueue()->execute();

And running it on a named queue differs only in the argument:

php
$myAction->onQueue('my-favorite-queue')->execute();

The consequence is that the action class does not change depending on where it runs. The same MyAction with the same execute method is the thing you call from a controller and the thing the queue worker later runs, which removes the usual duplication where a job class exists solely to call an action class.

The reasoning behind the pattern is not in the repository itself. The project points to a dedicated blog post on Stitcher for the reasoning behind actions and their asynchronous usage, which is a reasonable place to look but does mean the repository explains the what and not the why.

The package is MIT licensed, the primary language is PHP, and the homepage is spatie.be/open-source.

Installation, the config file, and the seam it exposes

The package installs with Composer:

bash
composer require spatie/laravel-queueable-action

The config file is optional, published with:

bash
php artisan vendor:publish --provider="Spatie\QueueableAction\QueueableActionServiceProvider" --tag="config"

And it is short enough to read in full:

php
return [
    /*
     * The job class that will be dispatched.
     * If you would like to change it and use your own job class,
     * it must extends the \Spatie\QueueableAction\ActionJob class.
     */
    'job_class' => \Spatie\QueueableAction\ActionJob::class,
];

That single key is the most interesting thing in the package, and it is the seam to understand before you rely on it. The action is not dispatched as the action itself, it is dispatched as a job, and the job class is what goes onto the queue. Replace it with your own class, extending the package ActionJob, and you have a place to add behaviour that applies to every queued action, whether that is a different base job, a different queue payload shape, or a different serialization strategy.

Everything else in the package is a hook on the action class rather than a global setting, which is the right shape for a library that many applications will want to bend in different directions.

The action class itself is ordinary PHP with a trait

An action is a plain class that uses the QueueableAction trait and has an execute method. The example in the project guide makes the structure clear, including the detail that constructor arguments can come from the container.

``` php class MyAction { use QueueableAction;

public function __construct( OtherAction $otherAction, ServiceFromTheContainer $service ) { // Constructor arguments can come from the container.

$this->otherAction = $otherAction; $this->service = $service; }

public function execute( MyModel $model, RequestData $requestData ) { // The business logic goes here, this can be executed in an async job. } }

code

The execute method takes the arguments the action needs, and the comment in the guide is the honest description of what those arguments are for. They are the data the action operates on, and they have to be serializable if the action is going on a queue, which is the constraint the pattern quietly imposes.

The call site is where the two modes sit side by side, which makes the difference obvious at a glance:

php class MyController { public function store( MyRequest $request, MyModel $model, MyAction $action ) { $requestData = RequestData::fromRequest($myRequest);

// Execute the action on the queue: $action->onQueue()->execute($model, $requestData);

// Or right now: $action->execute($model, $requestData); } } ```

There is also support for actions that use the __invoke() method instead of execute, detected automatically, and such actions are queued the same way by running execute after onQueue.

Chaining, tags, middleware and backoff are per-action hooks

Beyond queueing, the package adds four extension points, each implemented as a method on your action class.

Chaining works by wrapping actions in the ActionJob, which takes the action class or instance as its first argument followed by an array of the action's own arguments:

php
use Spatie\QueueableAction\ActionJob;

$args = [$userId, $data];

app(MyAction::class)
    ->onQueue()
    ->execute(...$args)
    ->chain([
        new ActionJob(AnotherAction::class, $args),
    ]);

Tags are for Horizon, and you override the tags() function to change what shows up there, returning an array of strings.

Middleware is the same shape. Overriding the middleware() function returns an array of middleware objects the action job passes through, and the example returns a RateLimited instance, so per-action rate limiting is a one-method change.

Backoff is a property rather than a method, and the documentation is explicit about the type, with the docblock saying the number of seconds to wait before retrying the action and the type being an array of ints or a single int:

``` php class BackoffAction { use QueueableAction; /** * The number of seconds to wait before retrying the action. * * @var array<int>|int */ public $backoff = 3; } ```

For anything more involved than a fixed delay, a backoff method on the action class is the documented alternative.

The pattern across all four is the same, and it is the pattern worth remembering: the package asks you to override one conventional method on your own class, and Laravel's own job mechanisms then do the work.

Test assertions that check the action, not the job

The testing support is the part that decides whether a package like this survives contact with a growing test suite.

The assertions live in a class called QueueableActionFake, and the example is a standard PHPUnit test with the standard Laravel fake in front of it:

php

/** @test */
public function it_queues_an_action()
{
    Queue::fake();

    (new DoSomethingAction)->onQueue()->execute();

    QueueableActionFake::assertPushed(DoSomethingAction::class);
}

The important detail is that you must call Queue::fake() first, because the package's assertions read Laravel's own fake rather than replacing it.

The available assertions are a small, deliberate set:

php
QueueableActionFake::assertPushed(string $actionClass);
QueueableActionFake::assertPushedTimes(string $actionClass, int $times = 1);
QueueableActionFake::assertNotPushed(string $actionClass);
QueueableActionFake::assertPushedWithChain(string $actionClass, array $expextedActionChain = [])
QueueableActionFake::assertPushedWithoutChain(string $actionClass)

They assert on the action class rather than on the job class, which is the right level given the design, and the chain variants let you assert that one action was followed by another. The project asks for pull requests if any of the other QueueFake assertions are missing, which is a small sign of how the package expects to evolve.

What this package does not solve

Three limits are worth naming.

The first is that it assumes you have already adopted actions. The package is a queueing layer for a pattern, not a pattern. If your business logic currently lives in controller methods or in service classes with a different shape, adopting this package means restructuring first, which is a much larger change than adding a trait.

The second is serialization. The action and its arguments have to survive being written to a queue, which means models passed in by identifier rather than by object, and value objects rather than entities. The example passes a MyModel and a RequestData, and whether those are serializable is up to your classes, not to the package. Nothing in the guide discusses this constraint, which means you will meet it at dispatch time rather than at design time.

The third is that the queueing is per action class rather than per call. The onQueue method takes a queue name, but everything else about the job, its connection, its timeout and its retry behaviour, comes from the ActionJob base class you configure globally. That is a reasonable default for a package that most applications will not customise, and it is a limitation for the ones that need per-queue connection settings.

The release history suggests a stable, slow-moving package. The listed versions are 2.16.1 from 2025-01-19, 2.16.2 from 2025-02-21 and 2.17.0 from 2026-02-22, and the last push to the repository was on 2026-06-02.

Editorial conclusion

laravel-queueable-action suits a Laravel codebase where business logic sits in action classes and the same logic is needed both inside a request and in the background, because the action class is written once and the execution mode is a method call rather than a second class. It does not suit a project that has not settled on actions as a pattern, since the package assumes that structure rather than introducing it, and it does not suit a team that wants fine control over job serialization, since the action is serialized as a job payload. Before adding it, read the config option for the job class, because that is the seam where you substitute your own job, and note that the last push was on 2026-06-02 with 2.17.0 published on 2026-02-22.

Frequently asked questions

What does spatie/laravel-queueable-action do?

It makes Laravel actions queueable. The same action class can be run immediately with execute or pushed onto a queue by calling onQueue first, so the same business logic serves both a request and a background job without a second implementation.

How do I install laravel-queueable-action?

Run composer require spatie/laravel-queueable-action. The config file is optional and published with php artisan vendor:publish using the QueueableActionServiceProvider provider and the config tag. It contains a single job_class key naming the job class that will be dispatched.

How do I chain queued actions?

Wrap the next action in an ActionJob and pass it to the chain method, which takes the action class or instance as its first argument followed by an array of that action's own arguments. The chain assertions assertPushedWithChain and assertPushedWithoutChain let you test the order.

Can I customize the Horizon tags and middleware of a queued action?

Yes, by overriding the tags() and middleware() functions on your action class. The tags function returns an array of strings that show up in Horizon, and the middleware function returns an array of middleware objects the action job passes through, for example a rate limiter.

How do I set a retry backoff for a queued action?

Define a backoff property on the action class, typed as an array of ints or a single int, giving the number of seconds Laravel waits before retrying. For more complex logic, define a backoff method on the action class instead.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. spatie/laravel-queueable-action on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/spatie-laravel-queueable-action.svg)](https://hysenlabs.com/projects/spatie-laravel-queueable-action)

Was this useful?