wdelfuego/nova-wizard
Add multi-step wizards to Laravel Nova forms. nova-wizard splits long resource forms into guided, step-by-step flows, letting you group fields into panels and control navigation per step so complex admin forms stay simple and easy to complete.
wdelfuego/nova-wizard bolts a multi-step form experience onto Nova 4 by splitting a single "save" into a sequence of field-bearing steps. Start by installing and confirming the version aligns with your Nova:
composer require wdelfuego/nova-wizard
php artisan vendor:publish --tag=nova-wizard-assets # custom Vue/JS assets
php artisan optimize:clear
Where to look first, in order:
vendor/wdelfuego/nova-wizard/src/ — the abstract Wizard and Step base classes are short; read them before writing anything.vendor/wdelfuego/nova-wizard/resources/js/ — the Vue wizard shell. This tells you what the front-end actually submits and what shape the payload has in the controller.Minimal first use case: an "onboarding" flow that creates a single Company model plus its first User, split across three steps:
namespace App\Nova\Wizards;
use Wdelfuego\NovaWizard\Wizard;
use App\Nova\Wizards\Steps\CompanyDetails;
use App\Nova\Wizards\Steps\PrimaryContact;
use App\Nova\Wizards\Steps\ReviewStep;
class OnboardingWizard extends Wizard
{
public function title(): string
{
return 'New Customer Onboarding';
}
public function steps(): array
{
return [
new CompanyDetails(),
new PrimaryContact(),
new ReviewStep(),
];
}
}
namespace App\Nova\Wizards\Steps;
use Laravel\Nova\Fields\Text;
use Wdelfuego\NovaWizard\Step;
class CompanyDetails extends Step
{
public function title(): string { return 'Company'; }
public function fields(): array
{
return [
Text::make('Company Name', 'name')->rules('required|max:120'),
Text::make('VAT Number', 'vat')->rules('nullable|alpha_num'),
];
}
}
Register the wizard in App\Providers\NovaServiceProvider (or wherever you register Nova tools) using whatever registration hook the package's README documents — usually a Nova::wizards([...]) call or the convention-based app/Nova/Wizards directory. Once registered, the entry point shows up in the "Create" menu / tools drawer, and you navigate to /nova/wizards/onboarding-wizard.
One Wizard, many Steps, one save. The intended pattern is that each Step is only about presentation and validation; the Wizard (or a bound "handler" class) owns the final save. Keep all DB::transaction() logic in the wizard, not in the step. Treat steps as view-models.
Persist across steps via the request bag, not the model. Because Nova sends the whole wizard payload back to the server on each step transition, intermediate values arrive in the request. Compute derived data inside the wizard:
public function save(Request $request): Model
{
return DB::transaction(function () use ($request) {
$company = Company::create($request->only(['name', 'vat']));
$company->users()->create($request->only(['email', 'first_name', 'last_name']));
return $company;
});
}
Reuse your existing Resource fields. Point step fields at the same Text::make() / BelongsTo::make() instances you already use on your Nova resource — or better, call a shared static method that both resource and step use. This keeps validation rules and dependsOn consistent between the admin CRUD view and the wizard.
Conditional steps based on prior answers. Expose the raw request to the wizard's step resolver so you can return a different steps() array per request. A common pattern is a shouldShow(Request $request): bool on each step, with the wizard filtering them out — but be careful: the front-end will still render the shell expecting a stable list, so gate content rather than the number of steps unless you have confirmed the package supports dynamic steps on the front end.
Compose with Nova Actions. The cleanest "day to day" integration is a Nova Action on a Resource that redirects to the wizard with pre-filled query parameters (?company_id=123). The wizard then treats its first step as read-only context.
Reuse steps across wizards. Because a Step is a plain class with fields(), define an interface (interface SharedFieldSet { public function fields(): array; }) and have both a Step and a Resource delegate to the same FieldProvider class. This is the single highest-leverage refactor if you maintain multiple wizards.
Tests: hit the wizard's store endpoint, not the UI. Laravel testing goes a long way here:
$this->actingAs($user)
->postJson('/nova/wizards/onboarding-wizard', [
'name' => 'Acme',
'email' => '[email protected]',
'first_name'=> 'Ada',
])
->assertStatus(201);
Assert against the created model, not against the redirect, since the redirect target is Nova's Vue shell and not stable.
composer.json ("wdelfuego/nova-wizard": "^1.0"), and consider forking into packages/ if you plan to rely on it for revenue-critical flows. There is no upgrade path warranty.composer update, run php artisan vendor:publish --tag=nova-wizard-assets --force and php artisan nova:publish. Missing republishes are the #1 cause of "the wizard renders but buttons do nothing" — the Vue component is cached in public/vendor/nova-wizard/.rules() on individual fields is validated client-side per-step (nice UX), but anything that spans fields (e.g. "passwords match", "end date after start date") must be enforced again in the wizard's server-side validate(). Don't trust client-only validation.dependsOn across steps is fragile. Nova's conditional field dependency resolves against the same form payload; if a field lives in step 1 and the dependent field is in step 3, the dependency may not fire until the earlier value is present in the payload. Verify with Vue DevTools → Network and inspect the JSON body of the step transition.pending endpoint immediately, storing them under nova-temp/. If a user abandons the wizard mid-way, temp files leak. Add a cleanup scheduled command scanning storage/app/nova-temp for orphans older than 24h.BelongsTo, BelongsToMany) inside wizards. They work, but the wizard's save must run sync() on many-to-many relations manually — the wizard does not fire Nova's Resource save lifecycle. There is no afterSave hook for relationships, so build it yourself inside the transaction.Gate/Policy at the top of save() and in authorize(), because the wizard route itself is only guarded by Nova's Nova::check() middleware — not by the target model's policy.APP_DEBUG and watch storage/logs/laravel.log plus the browser console — the Vue component swallows 422 responses into a generic "something went wrong" toast. Adding a global axios interceptor in your Nova::script() is the fastest way to surface server validation errors while developing.Step class is designed to be extended; override title(), fields(), and validationRules() per step. The Wizard class typically exposes title(), uriKey(), steps(), and the save hook. If those are all you need, stay on the public API; if you need to intercept step navigation, that's when you fork and edit resources/js/components/Wizard.vue and rebuild assets with npm run prod inside the package.DB::transaction() and add a unique constraint on the natural key (e.g. company.name + company.vat), or use a Cache::lock() keyed by user + wizard uriKey.How can I help you explore Laravel packages today?