NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Onboarding for Laravel

Registering flows

Register flows once — typically in the boot() method of one of your application’s service providers:

namespace App\Providers;

use App\Models\User;
use Illuminate\Support\ServiceProvider;
use RoundlyConsulting\Onboarding\Facades\Onboarding;
use RoundlyConsulting\Onboarding\Flow;
use RoundlyConsulting\Onboarding\Step;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Onboarding::register(Flow::make('Profile Onboarding')->of([
            Step::make('Upload Photo')
                ->cta('Upload Now')
                ->action('users@photo@upload') // a route name, URL, or any hint your frontend understands
                ->completeIf(fn (?User $user) => (bool) $user?->photo),
        ]));
    }
}

Registering without a key stores the flow under the default key, default (held in OnboardingManager::$default). Register additional flows under explicit keys:

Onboarding::register('admin', Flow::make('Admin Setup')->of([
    Step::make('Invite your team'),
    Step::make('Connect billing')->completeIf(fn (?User $user) => $user?->hasBilling()),
]));

You may also register a plain array of steps — it is wrapped in a Flow for you, and with no key it becomes the default flow:

Onboarding::register([
    Step::make('Upload Photo')->cta('Upload Now'),
]);

Fluent registration

Onboarding::flow() creates, registers and returns an empty flow under the key — replacing any flow already registered there. add() appends a step and returns that Step, so step builders chain straight off it:

Onboarding::flow('default')
    ->title('Profile Onboarding')
    ->add('Upload Photo')->key('upload-photo')->cta('Upload Now')
        ->action('users@photo@upload')
        ->completeIf(fn (?User $user) => (bool) $user?->photo);

Because add() returns the Step rather than the flow, start each further step from a flow variable — or append ready-made steps with addStep():

$flow = Onboarding::flow('default')->title('Profile Onboarding');

// add() returns the new Step, so start every step from the flow
$flow->add('Upload Photo')->key('upload-photo')->completeWhenFilled('avatar_path');
$flow->add('Verify email')->key('verify-email')->completeWhenTrue('hasVerifiedEmail');

// or append ready-made steps (variadic, returns the Flow)
$flow->addStep(
    Step::make('Invite team')->completeWhenHas('teams'),
    Step::make('Add a bio')->optional()->completeWhenFilled('bio'),
);

Custom flow classes

For reusable flows, extend Flow and configure it in the protected setup() hook, which runs from the constructor:

namespace App\Onboarding;

use RoundlyConsulting\Onboarding\Flow;
use RoundlyConsulting\Onboarding\Step;

final class UserOnboarding extends Flow
{
    protected function setup(): void
    {
        $this->title('User Onboarding')->of([
            Step::make('Verify email')->key('verify-email')->completeWhenTrue('hasVerifiedEmail'),
            Step::make('Upload photo')->key('photo')->route('profile.photo')->completeWhenFilled('avatar_path'),
            Step::make('Add a bio')->key('bio')->optional()->completeWhenFilled('bio'),
        ]);
    }
}

Register a custom flow by its class string — the registry instantiates it:

use App\Onboarding\UserOnboarding;
use RoundlyConsulting\Onboarding\Facades\Onboarding;

Onboarding::register(UserOnboarding::class);            // stored as the default flow
Onboarding::register('user', UserOnboarding::class);    // stored under the "user" key

Managing the registry

Onboarding::find('admin');                 // ?Flow — the registered definition
Onboarding::find('missing', new Flow());   // the fallback when the key is unknown
Onboarding::has('admin');                  // bool
Onboarding::all();                         // Collection<string, Flow>, keyed by flow key
Onboarding::forget('admin');               // remove one flow
Onboarding::flush();                       // remove all flows and the resolver

find() returns the registered definition itself — use it to extend a flow at boot, and read through Onboarding::for() so no subject is bound to the shared instance. Every manager method is listed in The Onboarding facade.

Registering a class string that is not Flow or a subclass throws InvalidFlowException — see Extending & exceptions.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.