DEV Community

Cover image for Distributed Background Processing: Scaling Temporal Workflows with Laravel
Michael Laweh
Michael Laweh

Posted on Originally published at klytron.com

Distributed Background Processing: Scaling Temporal Workflows with Laravel

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.

Core Architecture: Workflows vs. Activities

Temporal separates execution logic into two distinct concepts to ensure reliability and fault tolerance:

  • 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.

Setting Up Temporal in Laravel

To communicate with a Temporal cluster, install the official Temporal PHP SDK via Composer:

composer require temporal/sdk
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Writing Your First Workflow and Activity

Let’s implement the user onboarding flow described earlier using clean, maintainable PHP classes.

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;
}
Enter fullscreen mode Exit fullscreen mode

Now, implement the activities using Laravel’s dependency injection container:

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Define the Workflow Interface and Implementation

The workflow dictates the timing and business logic. It orchestrates the activities without executing side effects directly.

namespace App\Temporal\Workflows;

use Temporal\Workflow\WorkflowInterface;
use Temporal\Workflow\WorkflowMethod;

#[WorkflowInterface]
interface OnboardingWorkflowInterface
{
    #[WorkflowMethod]
    public function runOnboarding(int $userId);
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Keep this worker running in production using a battle-tested process manager like Supervisor.

Dispatching the Workflow

To trigger the onboarding process 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.']);
    }
}
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

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.

👉 Read the complete deep-dive with the full code repository and bonus security checklist on klytron.com

Top comments (0)