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

Recurring appointments

Give the builder a RecurrenceData rule and call createRecurring(). Each occurrence is materialised as its own appointment, linked by a shared recurrence_group UUID:

use RoundlyConsulting\Appointments\DataTransferObjects\RecurrenceData;
use RoundlyConsulting\Appointments\Enums\Frequency;
use RoundlyConsulting\Appointments\Facades\Appointments;

$series = Appointments::schedule('Weekly standup')
    ->startingAt('2026-07-06 09:00')
    ->lasting(30)
    ->withParticipant($team)
    ->recurring(new RecurrenceData(Frequency::Weekly, count: 8, byWeekday: [1])) // Mondays
    ->createRecurring();                // Collection<int, Appointment>

Rule options

FieldTypeDefaultMeaning
frequencyFrequency—Frequency::Daily, Frequency::Weekly or Frequency::Monthly.
intervalint1Every Nth day, week or month — at least 1.
until?CarbonImmutablenullLast allowed start, inclusive. A value at midnight is read as a date and includes that whole day.
count?intnullMaximum number of occurrences — at least 1; capped by recurrence.max_occurrences.
byWeekdaylist of int[]ISO weekdays that occur in each active day, week or month — 1 = Monday … 7 = Sunday.

Examples

use Carbon\CarbonImmutable;
use RoundlyConsulting\Appointments\DataTransferObjects\RecurrenceData;
use RoundlyConsulting\Appointments\Enums\Frequency;

new RecurrenceData(Frequency::Daily, count: 5);                   // five consecutive days
new RecurrenceData(Frequency::Daily, interval: 2, count: 3);      // every other day, three times
new RecurrenceData(Frequency::Weekly, interval: 2, until: CarbonImmutable::parse('2026-12-31 23:59'));
new RecurrenceData(Frequency::Monthly, count: 3);                 // from Jan 31: Feb 28, then Mar 31 — no overflow
new RecurrenceData(Frequency::Weekly, count: 4, byWeekday: [1, 3]); // Mondays and Wednesdays
new RecurrenceData(Frequency::Weekly, interval: 2, count: 6, byWeekday: [1, 3]); // Monday and Wednesday of every other week
new RecurrenceData(Frequency::Monthly, count: 8, byWeekday: [1]);  // every Monday of each month
new RecurrenceData(Frequency::Daily, until: CarbonImmutable::parse('2026-07-31')); // midnight: a date, July 31 included

new RecurrenceData(Frequency::Daily, interval: 0);                 // throws InvalidRecurrenceException

How a series is built

  • Every occurrence carries everything a single create() would — participants, meta, location, coordinates, guest contacts and its own approval request.
  • The series is all-or-nothing: it is created in one database transaction, so if any occurrence fails — for example with a SchedulingConflictException under preventConflicts() — none of it is kept, and since events wait for the commit, no AppointmentCreated or participant event fires for any of it.
  • The rule is expanded in the appointment’s timezone, so a weekly 09:00 stays 09:00 local across a daylight-saving change.
  • Without a weekday filter, the first occurrence is the start itself and each next one is interval days, weeks or months on. Monthly steps never overflow: a series starting on the 31st lands on the last day of shorter months.
  • With byWeekday, every listed weekday of each active period occurs, from the start on — the period being the day, the week (Monday-based) or the month the frequency names, and every interval-th period being active. interval: 2 with byWeekday: [1, 3] is Monday and Wednesday of every other week; Frequency::Monthly with byWeekday: [1] is every Monday of each active month.
  • until is inclusive — an occurrence starting exactly at until is kept. An until at midnight is read as a date and includes that whole day.
  • An interval or count below one, or a weekday outside 1–7, throws InvalidRecurrenceException when the RecurrenceData is built.
  • Without count or until, a rule expands to recurrence.max_occurrences (365 by default) — always bound open-ended rules. count is capped by the same limit.

From a DTO

Appointments::createRecurring() takes an AppointmentData and a rule — or falls back to the DTO’s own recurrence. With no rule at all it creates the single appointment:

use RoundlyConsulting\Appointments\DataTransferObjects\RecurrenceData;
use RoundlyConsulting\Appointments\Enums\Frequency;
use RoundlyConsulting\Appointments\Facades\Appointments;

$series = Appointments::createRecurring($appointmentData, new RecurrenceData(Frequency::Daily, count: 5));
$series = Appointments::createRecurring($appointmentData); // falls back to $appointmentData->recurrence

Working with a series

Load a series by its recurrence_group, or preview the start times a rule produces with Appointments::occurrences() — nothing is written, and the same max_occurrences cap applies:

use Carbon\CarbonImmutable;
use RoundlyConsulting\Appointments\Facades\Appointments;
use RoundlyConsulting\Appointments\Models\Appointment;

// every appointment of one series
Appointment::query()
    ->where('recurrence_group', $series->first()->recurrence_group)
    ->ordered()
    ->get();

// preview the start times a rule produces, without writing anything
Appointments::occurrences(CarbonImmutable::parse('2026-07-06 09:00'), $rule); // list<CarbonImmutable>

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.