---
title: "Statuses and lifecycle — Events for Laravel | Roundly"
description: "Six states on lifecycle-for-laravel — publish, postpone, reschedule, cancel, complete and move online — the guards, phases and your own workflow."
url: https://roundly-consulting.com/open-source/docs/events-for-laravel/statuses
language: en
---

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

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

# Statuses and lifecycle

The status is owned by [Lifecycle for Laravel](https://roundly-consulting.com/open-source/docs/lifecycle-for-laravel.md) and never written directly. The shipped Lifecycle\\EventLifecycle, with its states in Enums\\EventStatus:

| State | Phase | Stamp | Expiry |
| --- | --- | --- | --- |
| `draft` | Draft | — | — |
| `scheduled` | Scheduled | `published_at` | complete at ends\_at |
| `postponed` | Postponed | `postponed_at` | never — a postponed event never auto-completes |
| `rescheduled` | Rescheduled | `rescheduled_at` | complete at ends\_at |
| `cancelled` | Cancelled (terminal) | `cancelled_at` | — |
| `completed` | Completed (terminal) | `completed_at` | — |

## The verbs

```php
$handle = Events::for($event)->by($organizer);

// Each verb on its own — the table below says from which state.
$handle->publish();                                      // draft → scheduled
$handle->unpublish();                                    // back to draft while nobody is going or holding seats
$handle->postpone('The band is ill');                    // dates kept, the new date still open
$handle->reschedule('2026-12-05 20:00', reason: 'New date');   // the only path to rescheduled
$handle->cancel('Venue flooded');                        // holds expire, promotion stops, dates kept
$handle->complete();                                     // not before the start; automatic at ends_at
$handle->moveOnline('https://meet.example.com/jazz', keepVenue: true);   // attendance becomes mixed
$handle->transition('approve', reason: 'Looks good');   // the generic path for your own lifecycle's steps
```

| Verb on the handle | From | Rules |
| --- | --- | --- |
| `publish()` | `draft` | Not after ends\_at. |
| `unpublish()` | `scheduled` | Refused while anyone is going or holding seats — postpone or cancel instead. |
| `postpone(?$reason)` | `scheduled, rescheduled` | Keeps the dates. |
| `reschedule($start, ?$end, ?$reason)` | `scheduled, postponed, rescheduled` | Times written in the same transaction; previous\_starts\_at keeps the originally announced start; the doors time is cleared; holds ending after an earlier new start end at it; seats freed meanwhile go to the waitlist. On a draft it is a plain edit. |
| `cancel(?$reason)` | `scheduled, postponed, rescheduled` | Holds expire, promotion stops, dates kept. |
| `complete()` | `scheduled, rescheduled` | Not before starts\_at; automatic at ends\_at (lifecycle:sweep). |
| `moveOnline($url, keepVenue: false)` | any but cancelled and completed | Sets the join link; drops the venue (online) or keeps it (mixed); moved\_online\_at, SEQUENCE raised, EventMovedOnline. On a draft it is an ordinary edit. |
| `transition($name, ?$reason, $payload)` | any | The generic path for a host lifecycle’s own transitions. |

Every transition allows the system — a call without by() is trusted code; with by($actor) the event policy decides first. Every status change raises the calendar SEQUENCE and fires EventStatusChanged plus the phase event (EventPublished, EventPostponed, …). A denied transition throws EventTransitionDeniedException: denials() lists lifecycle’s codes, and a rate limit answers 429 with retryAfterSeconds().

## A reschedule moves the date, always

The shipped reschedule transition carries the Lifecycle\\RescheduledThroughEvents guard (denial code rescheduled\_through\_events): it passes only for Events::for($event)->reschedule(), which writes the new times in the same transaction. Every other path — Lifecycles::for($event, 'status')->asSystem()->apply('reschedule'), $event->transition('reschedule'), a lifecycle schedule of reschedule or the generic Events::for($event)->transition('reschedule') — is denied, because it would mark the event rescheduled with its old date and announce it.

| Guard | Denial code | On |
| --- | --- | --- |
| `Lifecycle\NoAttendees` | `no_attendees` | unpublish: anyone going or holding seats. |
| `Lifecycle\RescheduledThroughEvents` | `rescheduled_through_events` | reschedule: anything but Events::for($event)->reschedule(). |

Scheduled and Rescheduled expire at ends\_at through complete — lifecycle’s sweep runs it, and changing ends\_at re-arms it. A postponed event never completes by itself. The transition names are constants on the definition: EventLifecycle::PUBLISH, UNPUBLISH, POSTPONE, RESCHEDULE, CANCEL and COMPLETE.

## Phases

```php
use RoundlyConsulting\Events\Enums\EventPhase;

$event->phase;                              // EventPhase of the current state
$event->phase->isActive();                  // Scheduled or Rescheduled: RSVPs and check-in run
$event->phase->isTerminal();                // Cancelled or Completed
$event->phase === EventPhase::Postponed;
$event->status->color();                    // the shipped EventStatus: gray, green, amber, blue, red, zinc
```

Every state maps to an EventPhase, and every package rule — RSVPs, check-in, feeds, JSON-LD, notices — reads phases, never state names. That is what makes the workflow swappable.

## Your own lifecycle

Point events.lifecycle at a LifecycleDefinition implementing Contracts\\EventLifecycle — or extend the shipped Lifecycle\\EventLifecycle, which is not final, and adjust define():

```php
use RoundlyConsulting\Events\Contracts\EventLifecycle;
use RoundlyConsulting\Events\Enums\EventVerb;
use RoundlyConsulting\Lifecycle\Definition\LifecycleBuilder;
use RoundlyConsulting\Lifecycle\Definition\LifecycleDefinition;

final class ApprovalLifecycle extends LifecycleDefinition implements EventLifecycle
{
    public function define(LifecycleBuilder $lifecycle): void
    {
        $lifecycle->states(MyStatus::class)->initial(MyStatus::Draft);   // MyStatus implements Contracts\EventState (phase())
        // … an approve step between draft and scheduled, the rest as you need
    }

    public function transitionFor(EventVerb $verb): ?string
    {
        return match ($verb) { EventVerb::Publish => 'submit', EventVerb::Cancel => 'cancel', default => null };
    }
}

// config/events.php
'lifecycle' => App\Lifecycles\ApprovalLifecycle::class,
```

- Every state maps to an EventPhase through Contracts\\EventState, and the initial state must be in the Draft phase.
- A verb your lifecycle does not offer — transitionFor() returns null — throws UnknownEventTransitionException, naming transition() as the generic path.
- A definition that breaks the contract throws InvalidEventLifecycleException, and events:doctor reports it.
- A step of yours that must only happen together with a date change can carry the same guard: ->when(RescheduledThroughEvents::class).

## 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/events-for-laravel.md)
