Laravel Queues are a game-changer for developers building video streaming applications. Queues provide a reliable orchestration layer for running processes in parallel and making complex workflows faster and easier to manage.
My streaming app, RoboTube, lives and breathes Mux Robots’ hosted AI workflows. It moderates content on upload, summarizes videos, generates chapters, finds key moments, and selects the best thumbnails once you press or click upload. It's even got its own custom Mux Robots video player. But none of those workflows fit within the lifetime of a normal HTTP request. Some steps need captions, some can run in parallel, remote jobs can finish at different times, and webhooks can arrive late or fail to arrive at all.
That's where Mux Robots Directives and Laravel Queues come in. Directives let Mux own the video-specific workflow dependencies, while Laravel Queues provide a durable application boundary for triggering work, processing webhooks, and syncing results back to my app.
RoboTube
If you’ve seen some of my other blog posts or talks, I’ve been building RoboTube — a video streaming app that implements just about every feature from Mux Robots. When a user uploads a video, Mux Robots workflows automatically kick off in sequence, powered by Directives, and the results are pushed to the user through Laravel Queues.

YouTube was the inspiration; however, the actual product turned out to be everything that happens after the upload, and that’s where Directives and Queues came in.

Directives handle the video-specific sequencing and dependencies, while Laravel Queues handle the durable application work around them.
My first version made Laravel the video orchestrator
Before Directives, each Mux Robots workflow was its own Laravel Queue job.
This design was reliable for the first iteration of this project, largely because of Laravel Cloud and durable database state. Laravel Queues were really powerful because you could retry API calls, enforce rate limits, and recover work after a worker crashed.
The mistake was asking Laravel to know that Generate Chapters, Find Key Moments, and Caption Translations cannot start until a caption track is ready.
Directives took the caption wait
Instead of asking Laravel Cloud to create four remote jobs and coordinate them, Laravel can trigger one Directive run:
POST /robots/v0/directives/{DIRECTIVE_ID}/runs{
"asset_id": "MUX_ASSET_ID"
}I prefer this because Directives allow Mux to own the majority of the video-specific work, such as:
- Reusing a caption track when one is already ready
- Generating captions when they are missing
- Waiting for that caption resource to become ready
- Dispatching chapters and key moments after their caption dependency is satisfied
- Running independent workflows in parallel
- Tracking the child Mux Robots jobs inside one Directive run
Beforehand, Laravel was orchestrating all of these conditions with a ton of application-layer code. It ended up being a pain to manage, so I decided to switch things up.
The new way I split it looks like this:
Laravel Queue
↓
trigger moderation Directive
↓
Mux runs moderation
↓ webhook
Laravel evaluates the moderation result
├── rejected → stop and sync the result
└── passed → trigger enrichment Directive
↓
Mux owns fan-out and caption dependencies
↓
Robots webhooks return results
↓
Laravel syncs results to ConvexMux Robots Directives do not replace Laravel Queues. They change what the queues are responsible for. Instead of using separate Laravel jobs to start, sequence, and monitor every Mux Robots workflow, the application triggers a Directive and lets Mux handle the fan-out and video-specific dependencies, such as waiting for captions before generating chapters or key moments. Laravel Queues remain valuable at the application layer: triggering the moderation and structural Directives reliably, processing Mux webhooks, synchronizing results back to Convex (where my app’s data is stored), and recovering from missed events. This produces a more manageable separation of responsibilities.
Think of it like this: Mux orchestrates the video-intelligence workflows, while Laravel provides durable integration with the rest of the application.
Not one but two Directives
You might think you have to put moderation, summaries, chapter generation, key moments, and thumbnail selection into one large Directive, but I thought of a different way to architect this.
RoboTube deliberately uses two Directives:
- A Moderation Directive with only the moderation workflow.
- A Structural Enrichment Directive:
Summaries ➡️ Find Key Moments ➡️ Find Best Thumbnails
I split these two directives because of the user experience I needed for uploading videos. I separated moderation from the rest because if a user uploads a video and moderation rejects it, I do not want the rest of the structural enrichment pipeline to start. This makes the infrastructure code much simpler, saves money, and automatically protects my users from inappropriate content. This is a great separation of concerns because Laravel remains responsible only for orchestrating the two directives that serve different purposes. Directives manage the complexity of automation and ensure your desired workflows execute without extra infrastructure code.
if ($passed) {
SyncRobotubeResults::dispatch($run->id)
->onQueue(Queues::name('sync'));
RunMuxDirective::dispatch(
$run->id,
MuxDirectivePlan::ENRICHMENT,
);
return;
}
$this->skipNonTerminalJobs($run);
$run->update([
'status' => RobotRunStatus::Rejected,
'finished_at' => now(),
]);Simple, but that’s how I like it
The moderation upload job is intentionally small.
class RunMuxDirective implements ShouldBeUnique, ShouldQueue
{
use Queueable;
public int $tries = 3;
public int $uniqueFor = 300;
public function __construct(
public int $robotRunId,
public string $stage,
) {
$this->onQueue(Queues::name('robots'));
}
public function uniqueId(): string
{
return "{$this->robotRunId}:{$this->stage}";
}
}It loads a durable run, finds the Directive ID, and makes one API request.
$directiveId = MuxDirectivePlan::directiveId($this->stage);
$remote = $client->trigger($directiveId, $run->mux_asset_id);I used to have an Laravel job for each Mux Robots workflow:
- Summarize
- Generate Chapters
- Find Key Moments
- Find Best Thumbnails
Replacing that batch does not mean the queue is decorative. Laravel still does retries, backoffs, and rate limits at the boundary. A worker can reserve a job, call Mux Robots, and crash. When it retries, it won’t blindly start the same expensive work again. When Mux Robots accepts a Directive run, it returns a Directive ID, and the application stores that receipt.
data_set($input, "mux_directive_runs.{$this->stage}", [
'directive_id' => $directiveId,
'run_id' => $remote['run_id'],
'status' => $remote['status'],
'started_at' => now()->toIso8601String(),
]);
$run->update(['input' => $input]);Every retry checks for that receipt first:
$existingRunId = data_get(
$run->input,
"mux_directive_runs.{$this->stage}.run_id",
);
if ($existingRunId) {
SyncMuxDirectiveRun::dispatch($run->id, $this->stage);
return;
}If that remote run already exists, Laravel reconciles it instead of creating another one.
Recovery is still Laravel's job
Mux Robots Directives make workflow execution durable. They do not make my app’s state durable. A webhook can miss the endpoint, a queue worker can restart after receiving an event, or my Convex database can be temporarily unavailable when Laravel tries to sync.
So I added a poller.
Schedule::command('robotube:poll-pending-jobs')
->everyFiveMinutes()
->withoutOverlapping();
Schedule::command('robotube:repair-stuck-runs')
->everyTenMinutes()
->withoutOverlapping();
Schedule::command('robotube:retry-dead-webhooks')
->everyFifteenMinutes()
->withoutOverlapping();It fetches the Directive run to recover child Mux Robots job IDs, then fetches any child job whose completion webhook looks missing, and it feeds those results through the same state transition the webhook handler uses. The poller and the webhook handler do not get two different ideas of success and failure.
It’s your app. You make the rules.
The great thing about APIs is that you can use them however you want. In RoboTube, I’m still using Mux Robots without Directives, but there’s a reason for that.
RoboTube lets users request captions or audio translations for a number of languages. This kind of workflow doesn’t work well with Directives because users can select any target language. Earlier in this post, we triggered Directives using their IDs. There’s no point in creating a Directive for every translation scenario a user might choose unless I want every upload translated into the same fixed set of languages. Workflow parameters are part of the stored Directive definition, so for now, dynamic translations remain individually queued Mux Robots jobs.
So it’s still Mux Robots—just for two different types of user behavior. Summaries, chapters, key moments, and thumbnails are stable product behaviors, while translations selected at request time are dynamic inputs.
Try it yourself
The combination of Mux Robots Directives and Laravel Queues is an unlikely combo that I’m glad worked out. It removed complexity and added a durable boundary that helps ensure RoboTube users have a better experience.
The working code is on the RoboTube Repo. Star it, fork it, break it apart.



