| Install | |
|---|---|
composer require pixelworxio/livewire-workflows |
|
| Latest Version: | 0.5.5b |
| PHP: | ^8.3 |
![]()
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.
| 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.
composer require pixelworxio/livewire-workflows
php artisan workflows:install
php artisan workflows:install --with-db
php artisan migrate
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:
GET /onboarding → onboarding.startGET /onboarding/verify-email → onboarding.verify-emailGET /onboarding/profile → onboarding.profileQuick 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.
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>
Visit /onboarding. The package:
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/paymentWorkflows 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}/paymentRoute 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.
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."
/onboarding)order$this->continue('onboarding')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:
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:
$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,
]
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());
});
# 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
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);
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);
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);
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', 'session'),
// Middleware applied to all workflow routes
'middleware' => ['web', 'auth'],
];
| Repository | Use Case | Persistence |
|---|---|---|
null |
Stateless workflows | None |
session |
Guest users, simple flows | Session lifetime |
eloquent |
Authenticated users, production | Database |
Workflow::flow(string $name)
->entersAt(name: string, path: string)
->finishesAt(string $routeName)
->step(string $key)
->goTo(string $componentClass)
->unlessPasses(string $guardClass)
->order(int $order);
use InteractsWithWorkflows;
$this->continue(string $flow): RedirectResponse
$this->back(string $flow, string $currentKey): ?RedirectResponse
$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(): RedirectResponse
$this->back(): ?RedirectResponse
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)
// 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()
**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 |
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.
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.
Yes. Add 'auth' to the middleware array in config/livewire-workflows.php.
Make the guard's passes() method return true when the step should be skipped.
No. State is scoped per workflow and per user. This is by design for data isolation.
The step page loads, but calling continue() will re-evaluate guards and redirect appropriately.
Contributions are welcome! Please see CONTRIBUTING.md for details.
git clone https://github.com/pixelworxio/livewire-workflows.git
cd livewire-workflows
composer install
composer test
The MIT License (MIT). Please see License File for more information.
Built with ❤️ using Laravel, Livewire, and Spatie's Package Tools.
Give a ⭐️ if this project helped you!