---
title: "Creating and editing events — Events for Laravel | Roundly"
description: "Build and publish events fluently or from CreateEventData, then edit, find, list, set posters, transfer, delete and restore them."
url: https://roundly-consulting.com/open-source/docs/events-for-laravel/creating-events
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)

# Creating and editing events

Events::make($title) returns an EventBuilder: create() stores a draft, publish() creates and publishes in one transaction. A title, an organizer and a start are all it needs:

```php
use RoundlyConsulting\Events\Enums\AttendanceMode;
use RoundlyConsulting\Events\Enums\GuestList;
use RoundlyConsulting\Events\Enums\Visibility;
use RoundlyConsulting\Events\Facades\Events;

$event = Events::make(['en' => 'Jazz Night', 'sk' => 'Jazzový večer'])  // EventBuilder
    ->description(['en' => 'A live quartet.'])
    ->slug(['en' => 'jazz-night'])                 // optional manual slugs (strict: a taken one throws)
    ->organizedBy($user)                           // Model&Organizer
    ->coHostedBy($partnerTeam)
    ->at($venue)                                   // Venue, a venue id, or null
    ->online('https://meet.example.com/jazz')      // stored encrypted; managers and going attendees only
    ->attendanceMode(AttendanceMode::Mixed)        // else derived: venue → offline, link → online, both → mixed
    ->timezone('Europe/Bratislava')
    ->startsAt('2026-11-14 20:00')->duration('PT3H')   // or ->endsAt(), or ->allDay($from, through: $to)
    ->doorsOpenAt('2026-11-14 19:00')
    ->visibility(Visibility::Unlisted)             // or ->unlisted(), ->private()
    ->capacity(120)->maxGuests(2)->waitlist(true, autoPromote: true)
    ->rsvpWindow(opensAt: '2026-10-01 09:00', closesAt: '2026-11-14 18:00')
    ->guestList(GuestList::Attendees)->showCounts(true)
    ->category('jazz')->labels(['Live', 'Late night'])
    ->poster($upload)                              // UploadedFile, a local path, or draft: $token
    ->tier('Free entry', key: 'free')
    ->reminders(['P1D', 'PT2H'])
    ->meta(['source' => 'import'])
    ->by($user)                                    // checks create (and organizer approval)
    ->create();                                    // a draft — or ->publish(): create + publish in one transaction
```

Setters validate eagerly where they can — timezone, durations, URL, guests ≥ 0. Organizing as anyone but the actor, such as a team, needs $organizer->allowsEventManagementBy($actor), else InvalidOrganizerException. Every setter is listed on The Events facade.

- A title is at most 255 characters per locale (200 over the API), a description at most 20,000 and a join link at most 2048 (http or https).
- Control characters are stripped from titles and names; descriptions keep line breaks and tabs.
- Without a venue or a link the event is offline with the place to be announced. Otherwise the attendance mode is derived — a venue is offline, a link online, both mixed.
- Defaults come from events.defaults.\*: visibility, guests per RSVP, guest list, counts, waitlist (with automatic promotion on), duration and timezone.

## The DTO form

```php
use RoundlyConsulting\Events\DataTransferObjects\CreateEventData;
use RoundlyConsulting\Events\DataTransferObjects\ScheduleData;
use RoundlyConsulting\Events\DataTransferObjects\TierData;
use RoundlyConsulting\Events\Facades\Events;

$event = Events::create(new CreateEventData(
    title: ['en' => 'Jazz Night'],
    organizer: $user,
    schedule: new ScheduleData(startsAt: '2026-11-14 20:00', duration: 'PT3H', timezone: 'Europe/Bratislava'),
    capacity: 120,
    tiers: [new TierData(name: 'Free entry', key: 'free')],
    actor: $user,
));
```

## From the organizer

```php
// OrganizesEvents: the builder with the organizer already set
$event = $team->organizeEvent(['en' => 'Hack night'])
    ->at($venue)
    ->startsAt('2026-12-03 18:00')
    ->by($user)                 // the team must allow $user (allowsEventManagementBy)
    ->publish();
```

## Editing

```php
use RoundlyConsulting\Events\DataTransferObjects\UpdateEventData;
use RoundlyConsulting\Events\Enums\GuestList;

Events::for($event)->by($user)->edit()
    ->description(null)                      // null clears a nullable field
    ->doorsOpenAt('2026-11-14 19:00')
    ->guestList(GuestList::Attendees)
    ->showCounts(false)
    ->save();

Events::for($event)->by($user)->edit()->title('Renamed')->capacity(150)->save();
Events::for($event)->update(new UpdateEventData(capacity: 150, description: null));   // the DTO form
```

- Only the fields given change: every UpdateEventData field defaults to NotGiven, and null clears a nullable one. The editor’s rsvpWindow() changes only the bounds given; waitlist($on) leaves automatic promotion alone unless autoPromote: is passed.
- A new start (or a new all-day first day) on an announced event is refused with invalid\_event — use reschedule(). Repeating the current start, as an edit form sending every field back does, is an ordinary update.
- A change of the start, end, all-day flag, timezone, venue, join link or attendance mode raises the calendar SEQUENCE and, on an announced event, notifies going and held attendees — unless silently().
- A higher capacity, or waitlistAutoPromote switched back on, moves the waitlist up. EventUpdated lists the changed attributes; nothing fires when nothing changed.

## Visibility

| Visibility | Who sees it |
| --- | --- |
| `public` | Everyone, once announced. |
| `unlisted` | Anyone with the uuid link; findBySlug() only for managers, invitees and attendees. Never listed or in feeds. |
| `private` | Managers, invitees (pending or accepted) and anyone with an active RSVP. Never listed or in feeds. |

Drafts are visible to managers only. The full matrix and the policy are on Privacy and authorization.

## Finding and listing

```php
Events::find($uuid);                         // live events only; no visibility rule — authorize view yourself
Events::findOrFail($uuid);                   // EventNotFoundException otherwise
Events::findBySlug('jazz-night', $viewer);   // along the locale chain; null for what $viewer may not see
Events::for($uuid);                          // a handle; an unknown or trashed uuid throws EventNotFoundException

Events::browse($viewer)->upcoming()->paginate();   // visible, listed (public), announced
Events::visibleTo($viewer)->get();                 // the policy's view as a query — managed drafts included
```

Per-locale slugs come from the title and are stable once set, so a renamed event keeps its links. They are unique per locale with trashed events included and get a sequential suffix on collision — a manual slug() never does: a taken one throws. With events.slugs.history on, old slugs keep redirecting. Route-model binding uses the uuid, never the id or the slug.

## Poster

```php
Events::for($event)->by($organizer)->poster($request->file('poster'));   // an UploadedFile or a local path
Events::for($event)->poster(draft: $token);    // media-library's upload-first flow
Events::for($event)->poster();                 // no file and no draft removes it

$event->poster;                  // ?Media
$event->posterUrl('card');       // the variant once generated, else the original, else events.poster.fallback_url
```

The poster bucket takes one JPEG, PNG, WebP or AVIF image within events.poster.max\_kb and at least 600×315 — public by nature, with queued webp variants (thumb, card, square, landscape, wide). posterUrl() never throws: a pending variant falls back to the original, no poster to events.poster.fallback\_url. PosterChanged fires; a force delete removes the poster files and a soft delete keeps them for a restore.

## Transfer, delete and restore

```php
Events::for($event)->by($organizer)->transferTo($team, keepAsHost: true);
Events::for($event)->by($organizer)->delete();     // soft; refused while people are going (postpone or cancel first)
Events::for($event)->by($organizer)->restore();
Events::for($event)->delete(force: true);          // the row, its RSVPs, ticket types, co-hosts, invitations, tags and poster files
```

- transferTo() needs the transfer ability — the organizer’s approved users or an admin — and a new organizer who allows the actor, unless the actor is an admin. See Organizers, co-hosts and invitations.
- delete() is soft and refused (invalid\_event) while an announced, unfinished event has people going or holding seats — postpone or cancel first. delete(force: true) follows the same rule and removes the row, a trashed one too, with its RSVPs, ticket types, co-hosts, invitations, tags and poster files.
- A trashed event is read-only: every write throws EventTrashedException (409) until restore(). EventDeleted (with $force) and EventRestored fire.

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