Skip to content

stechstudio/laravel-sprocket

Repository files navigation

Sprocket

Database-driven workflow orchestration for Laravel.

Define multi-step workflows that are triggered by events or polling, persist their state to your database, and can suspend to wait for external input before resuming. Workflow definitions live in the database, so they can be created and modified at runtime — by your code or eventually by your users through a UI.

Installation

composer require stechstudio/laravel-sprocket

Publish and run the migrations:

php artisan vendor:publish --tag=sprocket-migrations
php artisan migrate

Optionally publish the config:

php artisan vendor:publish --tag=sprocket-config

Defining a Workflow

Use the Sprocket facade to define workflows with a fluent builder. Call save() to persist the definition.

use STS\Sprocket\Facades\Sprocket;

Sprocket::define('document-approval')
    ->description('Review and approve uploaded documents')
    ->on(DocumentUploaded::class)
    ->step('validate', ValidateDocument::class)
    ->step('send-for-review', SendReviewEmail::class)
    ->step('wait-for-decision')
        ->waitFor(DocumentReviewed::class, ['document_id'])
        ->timeout(days: 7, then: EscalateToManager::class)
    ->step('approve', FinalizeDocument::class)
        ->runIf('data.decision', '=', 'approved')
    ->step('reject', NotifyRejection::class)
        ->runIf('data.decision', '=', 'rejected')
    ->save();

The builder is idempotent — calling save() again with the same workflow name will update the existing definition and clean up removed steps.

Writing Step Handlers

Each step handler implements the Step contract and receives a Context with everything it needs:

use STS\Sprocket\Contracts\Step;
use STS\Sprocket\Values\Context;
use STS\Sprocket\Values\Result;

class ValidateDocument implements Step
{
    public function handle(Context $context): Result
    {
        $documentId = $context->get('document_id');

        // Do your work...

        return Result::succeed(['validated' => true]);
    }
}

A step handler returns a Result:

  • Result::succeed(array $data = []) — Step completed. Data is merged into the workflow's accumulated output and available to subsequent steps via $context->get().
  • Result::fail(string $error) — Step failed. The workflow run is marked as failed.
  • Result::suspend(...) — Step needs to wait. See Suspending Steps.

Context

The Context object passed to each step handler provides three layers of data:

$context->config;   // Step-level configuration (set via withConfig())
$context->payload;  // Original trigger data (from the event that started the workflow)
$context->data;     // Accumulated output from all previous steps
$context->run;      // The WorkflowRun model instance

$context->get('key');  // Searches data first, then payload

Triggers

Event Triggers

Workflows can be triggered by any Laravel event:

Sprocket::define('onboarding')
    ->on(CustomerRegistered::class)
    ->step('welcome-email', SendWelcomeEmail::class)
    ->save();

When CustomerRegistered is dispatched, Sprocket will automatically start a new workflow run. The event's public properties become the workflow payload.

You can filter which events should trigger the workflow:

Sprocket::define('pdf-processing')
    ->on(DocumentUploaded::class)
    ->when('type', '=', 'pdf')
    ->when('size', '<', 10000000)
    ->step('process', ProcessPdf::class)
    ->save();

Polling Triggers

For workflows that need to check an external condition on a schedule:

Sprocket::define('check-inbox')
    ->poll(CheckForNewEmails::class, every: 300)
    ->step('process', ProcessEmail::class)
    ->save();

The poll handler implements the PollHandler contract:

use STS\Sprocket\Contracts\PollHandler;

class CheckForNewEmails implements PollHandler
{
    public function check(): mixed
    {
        $emails = // check for new emails...

        return count($emails) > 0 ? ['emails' => $emails] : null;
    }
}

Return a truthy value (used as the workflow payload) when the condition is met, or a falsy value to keep waiting.

Run the poll command on a schedule in your routes/console.php:

Schedule::command('sprocket:poll')->everyMinute();

Suspending Steps

Steps can pause execution and wait for an external signal before the workflow continues.

Wait for an Event

A step with no handler that waits for an event acts as a pause point:

->step('wait-for-approval')
    ->waitFor(ApprovalReceived::class, ['document_id'])

The second argument defines correlation fields — the incoming event's document_id must match the workflow payload's document_id for the step to resume.

Wait via Polling

->step('wait-for-signature')
    ->pollStep(CheckDocuSignStatus::class, every: 3600)

Timeouts

Add a timeout to any suspended step:

->step('wait-for-approval')
    ->waitFor(ApprovalReceived::class, ['document_id'])
    ->timeout(days: 7, then: EscalateToManager::class)

The timeout handler implements the same Step contract. It can return Result::succeed() to advance the workflow, Result::fail() to stop it, or Result::suspend() to keep waiting with new conditions.

Runtime Suspension

Step handlers can also suspend at runtime:

class SendContractForSignature implements Step
{
    public function handle(Context $context): Result
    {
        // Send the contract...

        return Result::suspend(
            event: ContractSigned::class,
            match: ['contract_id' => $contract->id],
            timeout: now()->addDays(14),
            onTimeout: ContractTimeoutHandler::class,
        );
    }
}

Conditional Steps

Use runIf() to skip steps based on accumulated workflow data or the original payload:

->step('approve', FinalizeDocument::class)
    ->runIf('data.decision', '=', 'approved')
->step('reject', NotifyRejection::class)
    ->runIf('data.decision', '=', 'rejected')

Supported operators: =, !=, >, <, >=, <=.

Multi-Tenancy

Scope workflows to a tenant:

Sprocket::define('tenant-onboarding')
    ->forTenant($tenantId)
    ->on(TenantUserRegistered::class)
    ->step('setup', ProvisionTenantResources::class)
    ->save();

Workflows and their runs are isolated by tenant. The same workflow name can exist independently for different tenants.

Step Configuration

Pass static configuration to a step handler:

->step('send-email', SendEmail::class)
    ->withConfig(['to' => 'admin@example.com', 'template' => 'review-needed'])

Access it in the handler via $context->config.

Configuration

The published config file (config/sprocket.php) contains:

return [
    'queue' => env('SPROCKET_QUEUE', 'default'),
    'connection' => env('SPROCKET_QUEUE_CONNECTION'),
    'retries' => [
        'max_attempts' => 3,
        'backoff' => [10, 60, 300],
    ],
];

Individual steps can override queue, connection, retries, and backoff via withConfig().

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages