Laravel's queue system is excellent. Redis-backed queues and supervisors can handle millions of standard jobs efficiently. However, when background processing evolves into complex, multi-day, retry-sensitive state machines, standard queues begin to show limits.
Consider a multi-step user onboarding flow:
- Send a welcome email.
- Wait 3 days.
- Check if the user uploaded a profile picture.
- If not, send a reminder.
- Wait another 4 days.
- If still incomplete, flag the account for manual sales outreach.
Implementing this with standard Laravel jobs requires writing complex database state tracking, configuring multiple delayed dispatch loops, and managing manual retry intervals. If a server reboots mid-process, tracking which step a user was on becomes an operational nightmare.
Temporal solves this. It is a workflow orchestration engine that guarantees state progression. It allows you to write standard PHP code while Temporal handles state persistence, timeouts, queryable statuses, and complex retries.
Here is how to integrate Temporal into your Laravel application.
1. Core Architecture: Workflows vs. Activities
Temporal separates execution logic into two concepts:
- Workflows: The orchestrator. Workflows must be deterministic. They dictate the flow of execution, handle sleep intervals, and coordinate steps. Because they are deterministic, they must not interact directly with external systems, databases, or random functions.
- Activities: The execution layer. Activities can be non-deterministic. They perform the actual work: making database queries, querying third-party APIs, sending emails, or writing files.
2. Setting Up Temporal in Laravel
To communicate with a Temporal cluster, install the official Temporal PHP SDK:
composer require temporal/sdk
Next, configure your Temporal environment. In your .env, define the location of your Temporal address (by default, a local installation runs on port 7233):
TEMPORAL_ADDRESS=127.0.0.1:7233
3. Writing Your First Workflow and Activity
Let’s implement the user onboarding flow described earlier.
Step 1: Define the Activity Interface and Implementation
Activities contain your standard Laravel logic, such as Eloquent database queries and mailing services.
namespace App\Temporal\Activities;
use Temporal\Activity\ActivityInterface;
use Temporal\Activity\Method\ActivityMethod;
#[ActivityInterface]
interface OnboardingActivitiesInterface
{
#[ActivityMethod]
public function sendWelcomeEmail(int $userId): void;
#[ActivityMethod]
public function checkProfileStatus(int $userId): bool;
#[ActivityMethod]
public function sendProfileReminder(int $userId): void;
}
Now, implement the activities using Laravel’s dependency injection:
namespace App\Temporal\Activities;
use App\Models\User;
use App\Mail\WelcomeMail;
use App\Mail\ReminderMail;
use Illuminate\Support\Facades\Mail;
class OnboardingActivities implements OnboardingActivitiesInterface
{
public function sendWelcomeEmail(int $userId): void
{
$user = User::findOrFail($userId);
Mail::to($user->email)->send(new WelcomeMail($user));
}
public function checkProfileStatus(int $userId): bool
{
$user = User::findOrFail($userId);
return !empty($user->avatar_path);
}
public function sendProfileReminder(int $userId): void
{
$user = User::findOrFail($userId);
Mail::to($user->email)->send(new ReminderMail($user));
}
}
Step 2: Define the Workflow Interface and Implementation
The workflow dictates the timing and business logic. It orchestrates the activities.
namespace App\Temporal\Workflows;
use Temporal\Workflow\WorkflowInterface;
use Temporal\Workflow\WorkflowMethod;
#[WorkflowInterface]
interface OnboardingWorkflowInterface
{
#[WorkflowMethod]
public function runOnboarding(int $userId);
}
Implement the workflow, utilizing Temporal’s built-in time mechanics:
namespace App\Temporal\Workflows;
use Carbon\CarbonInterval;
use Temporal\Workflow;
use App\Temporal\Activities\OnboardingActivitiesInterface;
class OnboardingWorkflow implements OnboardingWorkflowInterface
{
private $activities;
public function __construct()
{
// Bind the activities interface with retry settings
$this->activities = Workflow::newActivityStub(
OnboardingActivitiesInterface::class,
Workflow\ActivityOptions::new()
->withStartToCloseTimeout(CarbonInterval::seconds(30))
);
}
public function runOnboarding(int $userId)
{
// 1. Send welcome email immediately
yield $this->activities->sendWelcomeEmail($userId);
// 2. Sleep for 3 days
yield Workflow::timer(CarbonInterval::days(3));
// 3. Check if they uploaded an avatar
$hasUploadedAvatar = yield $this->activities->checkProfileStatus($userId);
if (!$hasUploadedAvatar) {
// 4. Send reminder email
yield $this->activities->sendProfileReminder($userId);
}
}
}
4. Booting the PHP Worker
Unlike standard Laravel queues which run via artisan queue:work, Temporal requires a worker daemon to boot, register your workflows and activities, and poll the Temporal server for tasks.
Create an Artisan command app/Console/Commands/TemporalWorker.php:
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Temporal\WorkerFactory;
use App\Temporal\Workflows\OnboardingWorkflow;
use App\Temporal\Activities\OnboardingActivities;
class TemporalWorker extends Command
{
protected $signature = 'temporal:work';
protected $description = 'Start the Temporal PHP worker';
public function handle(): void
{
$this->info("Starting Temporal PHP Worker...");
$factory = WorkerFactory::create();
$worker = $factory->newWorker('onboarding-task-queue');
// Register implementations
$worker->registerWorkflowTypes(OnboardingWorkflow::class);
$worker->registerActivityImplementations(new OnboardingActivities());
$factory->run();
}
}
Keep this worker running in production using a process manager like Supervisor.
5. Dispatching the Workflow
To trigger the onboarding process (for instance, when a user registers), dispatch the workflow from your Laravel Controller or Event Listener:
namespace App\Http\Controllers;
use Temporal\Client\WorkflowClientInterface;
use App\Temporal\Workflows\OnboardingWorkflowInterface;
class AuthController extends Controller
{
public function register(WorkflowClientInterface $workflowClient)
{
// Create user registration record...
$user = Auth::user();
// Start onboarding workflow asynchronously
$run = $workflowClient->start(
$workflowClient->newWorkflowStub(OnboardingWorkflowInterface::class)
);
$workflowClient->start($run, $user->id);
return response()->json(['message' => 'Registration complete.']);
}
}
Conclusion
Temporal shifts the complexity of managing background state machines away from your application database. By separating timing orchestration (workflows) from operational code (activities), you build highly readable, fault-tolerant background systems that scale reliably.
If you are looking to design complex distributed backends, migrate legacy cron structures, or scale transaction-sensitive workflows, let's connect.