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.
composer require stechstudio/laravel-sprocketPublish and run the migrations:
php artisan vendor:publish --tag=sprocket-migrations
php artisan migrateOptionally publish the config:
php artisan vendor:publish --tag=sprocket-configUse 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.
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.
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 payloadWorkflows 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();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();Steps can pause execution and wait for an external signal before the workflow continues.
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.
->step('wait-for-signature')
->pollStep(CheckDocuSignStatus::class, every: 3600)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.
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,
);
}
}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: =, !=, >, <, >=, <=.
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.
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.
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().
MIT