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

A Step is a single onboarding task. It is complete unless a completeIf rule says otherwise, and never excluded unless an excludeIf rule says so. Both closures receive the flow’s bound subject — or null when there is none:

use App\Models\User;
use RoundlyConsulting\Onboarding\Step;

Step::make('Verify email')
    ->cta('Resend verification')
    ->action('verification.notice')
    ->meta(['icon' => 'mail'])
    ->completeIf(fn (?User $user) => $user?->hasVerifiedEmail())
    ->excludeIf(fn (?User $user) => $user?->is_guest);

Step builders

MethodPurpose
title(?string)Display title; may be a translation key.
cta(?string)Call-to-action label; may be a translation key.
action(?string)Free-form hint for your frontend; also the legacy redirect fallback.
route(?string $name, array|Closure $parameters = [])Named route the middleware redirects to, with its parameters — an array or a closure that receives the bound subject.
url(?string)Absolute URL or path the middleware redirects to.
meta(array)Arbitrary metadata, passed through to the DTO.
key(?string)Stable identifier; falls back to a slug of the title.
order(?int)Explicit sort position within the flow.
group(?string)Section key for grouped progress.
optional(bool = true)Guide the user without blocking completion.
required()Mark the step required again (the default).
translatable(?bool = true)Force (true), disable (false) or auto-detect (null) translation of title and CTA.
dismissible(bool = true)Let the user dismiss an optional step (needs a store).
completeIf(?Closure) / completeWhen()Completion rule; receives the subject or null.
excludeIf(?Closure) / excludeWhen()Exclusion rule; receives the subject or null.
for($subject)Bind a subject — the flow does this for you.

Every builder property is also a named argument of Step::make() (and of the constructor), so a step can be declared in a single call:

// Every builder property is also a named argument of Step::make()
Step::make(
    title: 'Add a bio',
    cta: 'Write your bio',
    key: 'bio',
    optional: true,
    group: 'profile',
    route: 'profile.bio',
);

Keys

Every step has a stable key — set it with key() or let it fall back to a slug of the title. A fixed key survives rewording and translation, so your frontend, events and tests can address the step reliably. step() and hasStep() look up visible steps only:

Step::make('Upload Photo')->stepKey();                  // 'upload-photo' — slugged from the title
Step::make('Upload Photo')->key('photo')->stepKey();     // 'photo' — explicit key wins

$flow->step('photo')?->isCompleted();   // look a visible step up by key
$flow->hasStep('photo');                // bool

Ordering

Steps follow insertion order by default. Give any step an explicit order() and the flow sorts by it; steps without an order then go after the ordered ones:

Step::make('Verify email')->order(1);
Step::make('Upload photo')->order(2);

Predicates and readers

$step->isCompleted();      // true when no completeIf rule is set
$step->isNotCompleted();
$step->isExcluded();       // false when no excludeIf rule is set
$step->isNotExcluded();
$step->isOptional();
$step->isRequired();
$step->isDismissible();
$step->isDismissed();      // reads the manager's store; false without one
$step->completedAt();      // ?DateTimeInterface from the manager's store; null without one
$step->stepKey();          // explicit key, or a slug of the title
$step->groupName();        // ?string
$step->routeParameters();  // array — the named route's parameters, resolved against the subject
$step->resolvedTitle();    // ?string — translation keys resolved
$step->resolvedCta();      // ?string

A step read through a flow — steps(), all(), step(), currentStep() — is already bound to the flow’s subject, so its predicates answer for that subject.

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.