---
title: "Defining steps — Onboarding for Laravel | Roundly"
description: "Build steps with titles, CTAs, targets, metadata, keys and ordering; decide completion and exclusion with closures."
url: https://roundly-consulting.com/open-source/docs/onboarding-for-laravel/steps
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Onboarding for Laravel](https://roundly-consulting.com/open-source/docs/onboarding-for-laravel.md)

# Defining steps

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:

```php
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

| Method | Purpose |
| --- | --- |
| `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:

```php
// 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. Stored completions and dismissals are keyed by subject and step key, so a key shared by two flows counts in both — see Persistence store. step() and hasStep() look up visible steps only:

```php
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:

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

## Predicates and readers

```php
$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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/onboarding-for-laravel.md)
