pixelworxio / livewire-workflows
Build multi-step workflows in Laravel / Livewire with zero boilerplate
Fund package maintenance!
Requires
- php: ^8.3
- illuminate/contracts: ^11.0||^12.0||^13.0
- illuminate/pipeline: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- livewire/livewire: ^3.0|^4.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^11.0.0||^10.0.0||^9.0.0
- pestphp/pest: ^5.0
- pestphp/pest-plugin-arch: ^5.0
- pestphp/pest-plugin-laravel: ^5.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.0
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
README
Livewire Workflows
Build powerful multi-step workflows in Laravel with zero boilerplate. Define complex user journeys—onboarding, checkouts, surveys—using an expressive, route-like DSL. Get automatic route registration, guard-based navigation, state persistence, and full Livewire 4.x integration out of the box. Supports Laravel 11, 12, and 13. Livewire 3.x is also supported.
// Define a complete workflow in seconds Workflow::flow('onboarding') ->entersAt(name: 'onboarding.start', path: '/onboarding') ->finishesAt('dashboard') ->step('verify-email') ->goTo(VerifyEmail::class) ->unlessPasses(EmailVerifiedGuard::class) ->order(10) ->step('profile') ->goTo(EditProfile::class) ->unlessPasses(ProfileCompletedGuard::class) ->order(20);
That's it. No manual routes. No state management headaches.
View the testbench repo, https://github.com/pixelworxio/livewire-workflows-testbench, for examples of how this package can help you with your project.
✨ Why Livewire Workflows?
| Feature | Livewire Workflows | Manual Implementation |
|---|---|---|
| Route Management | ✅ Auto-generated | ❌ Define every route manually |
| State Persistence | ✅ Built-in (Session/DB) | ❌ Roll your own |
| Navigation Logic | ✅ Guard-based pipeline | ❌ Complex conditionals everywhere |
| Back Button | ✅ History tracking | ❌ Custom session juggling |
| Progress Tracking | ✅ One method call | ❌ Calculate yourself |
| Events & Analytics | ✅ Fire automatically | ❌ Remember to dispatch |
Result: Spend time building features, not workflow infrastructure.
📦 Installation
composer require pixelworxio/livewire-workflows
Setup
php artisan workflows:install php artisan migrate
🚀 Quick Start (3 Minutes)
1. Define Your Workflow
Open routes/workflows.php:
use Pixelworxio\LivewireWorkflows\Facades\Workflow; Workflow::flow('onboarding') ->entersAt(name: 'onboarding.start', path: '/onboarding') ->finishesAt('dashboard') ->step('verify-email') ->goTo(\App\Livewire\Onboarding\VerifyEmail::class) ->unlessPasses(\App\Guards\EmailVerifiedGuard::class) ->order(10) ->step('profile') ->goTo(\App\Livewire\Onboarding\EditProfile::class) ->unlessPasses(\App\Guards\ProfileCompletedGuard::class) ->order(20);
Routes auto-registered:
- Entry:
GET /onboarding→onboarding.start - Step 1:
GET /onboarding/verify-email→onboarding.verify-email - Step 2:
GET /onboarding/profile→onboarding.profile
2. Create a Guard
Quick Generation:
php artisan make:workflow-guard EmailVerified
This generates a guard at App\Guards\EmailVerifiedGuard.php with the proper structure.
namespace App\Guards; use Illuminate\Http\Request; use Pixelworxio\LivewireWorkflows\Contracts\GuardContract; class EmailVerifiedGuard implements GuardContract { public function passes(Request $request): bool { return true; } public function onEnter(Request $request): void {} public function onExit(Request $request): void {} public function onPass(Request $request): void {} public function onFail(Request $request): void {} }
Guard Logic: passes() = true → skip step. passes() = false → show step.
3. Build Your Livewire Component
namespace App\Livewire\Onboarding; use Livewire\Component; use Pixelworxio\LivewireWorkflows\Attributes\WorkflowStep; use Pixelworxio\LivewireWorkflows\Livewire\Concerns\InteractsWithWorkflows; #[WorkflowStep(flow:'onboarding', key:'verify-email')] class VerifyEmail extends Component { use InteractsWithWorkflows; public function resend() { auth()->user()->sendEmailVerificationNotification(); session()->flash('message', 'Verification email sent!'); } public function goToNextStep(): void { // ... handle your logic $this->continue('onboarding'); // workflow name is optional when using WorkflowStep attribute } public function render() { return view('livewire.onboarding.verify-email'); } }
And in your Blade view:
<div> <h1>Verify Your Email</h1> <p>Check your inbox for the verification link.</p> <button wire:click="resend">Resend Email</button> <button wire:click="goToNextStep"> I've Verified — Continue </button> </div>
4. Done!
Visit /onboarding. The package:
- Evaluates guards in order
- Redirects to first incomplete step
- Tracks progress automatically
- Completes when all steps pass
📖 Core Concepts
Auto-Generated Routes
Routes are automatically registered from your DSL:
->entersAt(name: 'checkout.start', path: '/checkout') // Entry route ->step('shipping') // Auto: /checkout/shipping ->step('payment') // Auto: /checkout/payment
Generated:
checkout.start→/checkoutcheckout.shipping→/checkout/shippingcheckout.payment→/checkout/payment
Dynamic Routes with Parameters
Workflows support dynamic route parameters and route model binding:
Workflow::flow('user-checkout') ->entersAt(name: 'user-checkout.start', path: '/user/{user}/checkout/{product}') ->finishesAt('dashboard') ->step('shipping') ->goTo(\App\Livewire\Checkout\Shipping::class) ->unlessPasses(\App\Guards\HasShippingAddress::class) ->order(10) ->step('payment') ->goTo(\App\Livewire\Checkout\Payment::class) ->unlessPasses(\App\Guards\HasPaymentMethod::class) ->order(20);
Generated Routes:
user-checkout.start→/user/{user}/checkout/{product}user-checkout.shipping→/user/{user}/checkout/{product}/shippinguser-checkout.payment→/user/{user}/checkout/{product}/payment
Route Model Binding:
// Supports Laravel's route model binding syntax ->entersAt(name: 'checkout.start', path: '/user/{user:id}/product/{product:slug}')
Navigation Preserves Parameters:
Route parameters are automatically passed through all workflow navigation:
class Shipping extends Component { use InteractsWithWorkflows; protected ?string $workflowName = 'user-checkout'; // Livewire receives route parameters public function mount($user, $product) { // $user and $product are automatically injected } public function submit() { // Parameters are automatically preserved when continuing $this->continue($this->workflowName); // Still navigates with {user} and {product} } }
The continue() and back() methods automatically extract and pass route parameters from the current request, ensuring seamless navigation throughout the workflow.
Guard Behavior
Guards use positive semantics:
class ProfileCompleteGuard implements GuardContract { public function passes(Request $request): bool { return $request->user()->profile_completed; } }
| Guard Result | Step Behavior |
|---|---|
passes() = true |
Skip this step |
passes() = false |
Show this step |
Use unlessPasses(Guard::class): "Show step UNLESS guard passes."
Navigation Flow
- User visits entry route (
/onboarding) - Package evaluates guards in
order - Redirects to first failing guard's step
- User completes step, calls
$this->continue('onboarding') - Re-evaluates from entry → next unmet step or finish
🧩 Advanced Features
State Management
Setting Workflow Name
Use the #[WorkflowName] attribute to explicitly declare your component's workflow, when not using the WorkflowStep attribute:
use Pixelworxio\LivewireWorkflows\Attributes\WorkflowName; #[WorkflowName('checkout')] class CheckoutShipping extends Component { use InteractsWithWorkflows; // protected ?string $workflowName = 'checkout'; // No need to set }
Benefits:
- ✅ Eliminates boilerplate
- ✅ Makes workflow association explicit
- ✅ Auto-detected during component boot
- ✅ Falls back to route-based detection if not present
Persisting State
Persist data across workflow steps with the #[WorkflowState] attribute:
use Pixelworxio\LivewireWorkflows\Attributes\WorkflowName; use Pixelworxio\LivewireWorkflows\Attributes\WorkflowState; #[WorkflowName('checkout')] class CheckoutShipping extends Component { use InteractsWithWorkflows; #[WorkflowState] public ?string $address = null; #[WorkflowState(encrypt: true)] public ?string $creditCard = null; #[WorkflowState(namespace: 'shipping')] public ?string $method = null; }
Features:
- Auto-hydration on mount
- Auto-persistence on dehydrate
- Encryption support
- Namespace grouping
- Database storage
Resume Links
Generate signed URLs that drop users directly back into their current workflow step — ideal for abandoned onboarding or checkout emails.
// In a notification, mailable, or controller: $url = workflow('onboarding')->resumeUrlFor(user: $user); $url = workflow('onboarding')->resumeUrlFor(user: $user, expiresInMinutes: 2880); // 48h // Guest / explicit key: $url = workflow('checkout')->resumeUrlFor(userKey: 'guest-abc123', expiresInMinutes: 60);
Requirements:
- Must be using the Eloquent state repository (
WORKFLOWS_REPOSITORY=eloquent) APP_KEYmust be set (used to sign the URL)
The generated URL is a signed route pointing to the user's current incomplete step. If no step has been recorded yet, the user is redirected to the workflow entry route. URLs expire after 24 hours by default (configurable via WORKFLOWS_RESUME_EXPIRES env variable). Tampered or expired URLs return 403.
Security: A signed resume URL proves that the URL is valid. It does not identify the person who opens it. Apply authentication and authorization that match your workflow before you use a resume link for private data.
Progress Tracking
$progress = workflow('onboarding')->progressFor($request); // Returns: [ 'total' => 3, 'completed' => 1, 'remaining' => 2, 'percentage' => 33.33, 'current_step' => 'verify-email', 'next_step' => 'profile', 'is_complete' => false, ]
Events
Listen to workflow lifecycle:
use Pixelworxio\LivewireWorkflows\Events\{WorkflowAdvanced, WorkflowCompleted}; Event::listen(WorkflowAdvanced::class, function ($event) { Log::info("User {$event->userKey} moved from {$event->fromKey} to {$event->toKey}"); }); Event::listen(WorkflowCompleted::class, function ($event) { Mail::to($user)->send(new OnboardingComplete()); });
CLI Tools
# Install the package php artisan workflows:install # Migrate from session state to Eloquent (existing installs) php artisan workflows:upgrade # Generate a new workflow php artisan make:workflow checkout # Generate a guard class php artisan make:workflow-guard EmailVerified # Add a step to existing workflow php artisan make:workflow-step checkout payment \ --component=App\\Livewire\\Checkout\\Payment \ --guard=App\\Guards\\CartNotEmptyGuard \ --order=20 # Validate and document all workflows php artisan workflows:scan # Audit workflows for configuration issues php artisan workflows:audit
🎯 Real-World Examples
Multi-Page Checkout
Workflow::flow('checkout') ->entersAt(name: 'checkout.start', path: '/checkout') ->finishesAt('orders.confirmation') ->step('cart') ->goTo(ReviewCart::class) ->unlessPasses(CartNotEmptyGuard::class) ->order(10) ->step('shipping') ->goTo(ShippingAddress::class) ->unlessPasses(ShippingAddressGuard::class) ->order(20) ->step('payment') ->goTo(PaymentMethod::class) ->unlessPasses(PaymentMethodGuard::class) ->order(30);
Employee Onboarding
Workflow::flow('employee-onboarding') ->entersAt(name: 'onboard.start', path: '/onboard') ->finishesAt('employee.dashboard') ->step('paperwork') ->goTo(Paperwork::class) ->unlessPasses(PaperworkCompleteGuard::class) ->order(10) ->step('it-setup') ->goTo(ITSetup::class) ->unlessPasses(ITAccountGuard::class) ->order(20) ->step('training') ->goTo(TrainingModules::class) ->unlessPasses(TrainingCompleteGuard::class) ->order(30);
User Registration Flow
Workflow::flow('signup') ->entersAt(name: 'signup.start', path: '/signup') ->finishesAt('welcome') ->step('account') ->goTo(CreateAccount::class) ->unlessPasses(AccountCreatedGuard::class) ->order(10) ->step('verify-email') ->goTo(VerifyEmail::class) ->unlessPasses(EmailVerifiedGuard::class) ->order(20) ->step('preferences') ->goTo(SetPreferences::class) ->unlessPasses(PreferencesSetGuard::class) ->order(30);
🔧 Configuration
Publish and customize the config:
php artisan vendor:publish --tag=livewire-workflows-config
config/livewire-workflows.php:
return [ // State persistence: 'null', 'session', or 'eloquent' 'repository' => env('WORKFLOWS_REPOSITORY', 'eloquent'), // Middleware applied to all workflow routes 'middleware' => ['web', 'auth'], ];
State Repository Options
| Repository | Use Case | Persistence |
|---|---|---|
null |
Stateless workflows | None |
session |
Short-lived anonymous flows (deprecated) | Session lifetime |
eloquent |
Authenticated users, production | Database |
📊 API Reference
Workflow DSL
Workflow::flow(string $name) ->entersAt(name: string, path: string) ->finishesAt(string $routeName) ->step(string $key) ->goTo(string $componentClass) ->unlessPasses(string $guardClass) ->order(int $order);
Livewire Trait Methods
use InteractsWithWorkflows; $this->continue(string $flow): void $this->back(string $flow, string $currentKey): void $this->syncState(): void // Manually persist state
Note: When using the #[WorkflowStep] attribute, the $flow and $currentKey properties are optional
#[WorkflowStep(flow: 'my-flow', key: 'current-step', middleware: ['auth'])]
use InteractsWithWorkflows;
$this->continue(): void
$this->back(): void
Helper Functions
workflow(string $flow)->redirect(Request $request, ?string $doneRoute = null) workflow(string $flow)->nextRouteNameFor(Request $request, ?string $doneRoute = null) workflow(string $flow)->previousRouteNameFor(string $currentKey, Request $request) workflow(string $flow)->progressFor(Request $request)
State Management Helpers
// Automatic with attributes #[WorkflowState] public ?string $email = null; #[WorkflowState(encrypt: true)] public ?string $password = null; #[WorkflowState(namespace: 'profile')] public ?string $name = null; // Manual helpers $this->putWorkflowState(string $key, mixed $value) $this->getWorkflowState(string $key, mixed $default = null) $this->hasWorkflowState(string $key) $this->forgetWorkflowState(string $key) $this->clearWorkflowState(?string $namespace = null) $this->allWorkflowState()
🆚 Comparison with Alternatives
**Or so Claude.ai says...
| Feature | Livewire Workflows | Spatie Laravel Wizard | Custom Solution |
|---|---|---|---|
| Route Auto-Registration | ✅ | ❌ | ❌ |
| Guard-Based Navigation | ✅ | ❌ | Custom |
| State Persistence | ✅ Built-in | ❌ | Custom |
| Livewire 4 & 3 Native | ✅ | ⚠️ Limited | N/A |
| History/Back Support | ✅ | ⚠️ Basic | Custom |
| Progress Tracking | ✅ | ❌ | Custom |
| Events | ✅ | ❌ | Custom |
| Learning Curve | Low | Medium | High |
💬 FAQ
Does this support Livewire v4?
Yes. The package is built and certified for Livewire v4, with full backward compatibility for Livewire v3. All lifecycle hooks and the redirect/navigation API are identical across both versions.
Do I need to define routes manually?
No. Step routes are auto-registered from your DSL. You only need to define the finish route in your regular routes/web.php, if not already present.
Can I protect workflows with authentication?
Yes. Add 'auth' to the middleware array in config/livewire-workflows.php.
How do I skip steps dynamically?
Make the guard's passes() method return true when the step should be skipped.
Can workflows share state?
No. State is scoped per workflow and per user. This is by design for data isolation.
What happens if a user jumps to a step URL directly?
The step page loads, but calling continue() will re-evaluate guards and redirect appropriately.
🛠️ Requirements
- PHP 8.3+
- Laravel 11.x, 12.x, or 13.x
- Livewire 4.x (3.x also supported)
🤝 Contributing
Contributions are welcome! Please see CONTRIBUTING.md for details.
Development Setup
git clone https://github.com/pixelworxio/livewire-workflows.git cd livewire-workflows composer install composer test
📜 License
The MIT License (MIT). Please see License File for more information.
🙏 Credits
Built with ❤️ using Laravel, Livewire, and Spatie's Package Tools.
🌟 Show Your Support
Give a ⭐️ if this project helped you!