---
title: "Events — Onboarding for Laravel | Roundly"
description: "Announce progress with record() — StepCompleted and FlowCompleted for analytics, notifications or your own persistence."
url: https://roundly-consulting.com/open-source/docs/onboarding-for-laravel/events
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)

# Events

Because the package is stateless it can’t detect transitions on its own — but it announces the current truth when you ask. Call record() at your own checkpoint and it dispatches StepCompleted for every complete visible step and FlowCompleted when the whole flow is complete. Reads never dispatch; only record() does.

```php
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Onboarding\Events\FlowCompleted;
use RoundlyConsulting\Onboarding\Events\StepCompleted;
use RoundlyConsulting\Onboarding\Facades\Onboarding;

Onboarding::for($user)?->record();   // announce every complete step (+ FlowCompleted when done)
$user->onboarding()?->record();      // the same through the trait
```

Pass a step key to announce just that step, if it is visible and complete. It fires FlowCompleted only when that completion finishes the flow — the step is required, was announced (not already recorded in a store), and the flow is now complete. An unknown, incomplete or optional step never fires it:

```php
Onboarding::for($user)?->record('upload-photo');   // announce just one step
```

## Payloads

| Event | Properties | Fired when |
| --- | --- | --- |
| `StepCompleted` | `$step, $for` | record() announces a complete step that the store has not already recorded. |
| `FlowCompleted` | `$flow, $for` | record() runs while the whole flow is complete; record($key) only when it announced that required step and the flow is now complete. |

Both events live under RoundlyConsulting\\Onboarding\\Events and carry the bound subject in $for — a Model, any Authenticatable, or null.

## Listening

```php
Event::listen(StepCompleted::class, function (StepCompleted $e): void {
    // $e->step (Step), $e->for (the subject — Model, Authenticatable or null)
});

Event::listen(FlowCompleted::class, function (FlowCompleted $e): void {
    // $e->flow (Flow), $e->for (the subject)
});
```

## Synchronous, idempotent listeners

Both events carry the live Step or Flow object, which holds closures — the completion and exclusion logic — so they can’t be serialized. A listener that implements ShouldQueue makes record() throw (Serialization of 'Closure' is not allowed), even on the sync queue. For queued work, listen synchronously and dispatch your own job with what it needs:

```php
Event::listen(StepCompleted::class, function (StepCompleted $e): void {
    SyncOnboardingProgress::dispatch($e->step->stepKey(), $e->for); // your job, SerializesModels
});
```

Listeners must also be idempotent. A bound store suppresses steps it already reports as completed, but that check isn’t an atomic claim: two concurrent record() calls that both run before your listener persists the completion both announce it. Write completions so a second announcement is harmless — a unique index on subject and step key with insertOrIgnore(), or firstOrCreate().

## A checkpoint in a controller

```php
public function update(Request $request): RedirectResponse
{
    $request->user()->update([
        'avatar_path' => $request->file('photo')->store('avatars'),
    ]);

    // a checkpoint of your choosing: announce the step this request may have completed
    Onboarding::for($request->user())?->record('upload-photo');

    return back();
}
```

Without a persistence store, every record() call re-announces every complete step. Plug in a store with Onboarding::useStore() and steps it already reports as completed aren’t announced again — see Persistence store.

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