Recurring events
Create a series with an RRULE, then change or delete one occurrence, the rest of the series, or all of it.
Recurring events are where calendar APIs disagree the most, so calsdk describes every series the same way, with RFC 5545 rules, and translates them for each provider.
Create a series
A series is an event with recurrence, one RFC 5545 line per array item, and its start and end describe the first occurrence:
const { event: standup } = await createEvent({
calendar,
event: {
title: 'Standup',
start: { dateTime: '2026-11-09T09:30:00', timeZone: 'Europe/Berlin' },
end: { dateTime: '2026-11-09T09:45:00', timeZone: 'Europe/Berlin' },
recurrence: ['RRULE:FREQ=DAILY;COUNT=5'],
},
});
standup.kind; // 'series'A few rules you'll reach for often:
| Rule | Repeats |
|---|---|
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR | Every Monday, Wednesday and Friday |
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=TU | Every other Tuesday |
RRULE:FREQ=MONTHLY;BYDAY=1MO | The first Monday of each month |
RRULE:FREQ=MONTHLY;BYMONTHDAY=15;COUNT=6 | The 15th, six times |
RRULE:FREQ=DAILY;UNTIL=20261231T235959Z | Every day until the end of 2026 |
Add EXDATE lines to skip occurrences. The series repeats in its start.timeZone, so a 9:30 standup in Berlin stays at 9:30 local time when daylight saving time starts or ends, instead of drifting an hour.
Four kinds of event
Every event has a kind that says how it relates to a series:
| Kind | What it is | Has |
|---|---|---|
single | An event that does not repeat | |
series | The recurring event itself | recurrence |
occurrence | One instance, as the rule placed it | seriesId, originalStart |
exception | One instance someone changed | seriesId, originalStart |
listEvents returns occurrences and exceptions by default, and series with expandRecurring: false. originalStart is where the rule put an instance, and it stays put when the instance moves, so it's how you recognize Friday's standup even after someone dragged it to 10:00.
Change one occurrence, or more
This is the part every calendar UI asks about: "this event, or all events in the series?" updateEvent and deleteEvent take the id of an occurrence or exception, and a scope that answers it:
scope | Changes |
|---|---|
'this' (default) | Only that instance. It becomes an exception. |
'all' | The whole series. |
'thisAndFollowing' | That instance and every later one. |
Pass the series id itself to change the whole series; scope only applies to instance ids.
This deletes the second standup and renames the fourth onward:
const { events } = await listEvents({ calendar, timeMin, timeMax });
await deleteEvent({ calendar, eventId: events[1].id });
await updateEvent({
calendar,
eventId: events[3].id,
scope: 'thisAndFollowing',
patch: { title: 'Standup v2' },
});Reading the series back shows what happened. The deleted day became an EXDATE, and thisAndFollowing ended the first series and started a second one:
const { events: series } = await listEvents({ calendar, timeMin, timeMax, expandRecurring: false });
// evt_1 Standup ['RRULE:FREQ=DAILY;UNTIL=20261111T083000Z', 'EXDATE:20261110T083000Z']
// evt_2 Standup v2 ['RRULE:FREQ=DAILY;COUNT=2']A thisAndFollowing change always works this way, as two series. Ids of later occurrences change as a result, so read them again afterwards.
Moving a series
Patching an occurrence's start with scope 'all' moves the whole series by as much as that occurrence moved. What happens to instances someone already edited is up to the provider: Google resets them and Zoom schedules every meeting again, both with a warning. The provider pages have the details.
What each provider supports
| Provider | Repeats | thisAndFollowing | Notes |
|---|---|---|---|
| Any RRULE | Yes | Single-instance changes are reset when a series moves or splits, with a warning | |
| Outlook | Rules Outlook has a pattern for | No | RDATE, hourly rules and some others throw |
| CalDAV | Any RRULE, with expansion limits | No | Expanded by calsdk: rules repeating every minute or second, or with an INTERVAL above 1,000, list only their first instance |
| Zoom | Daily, weekly or monthly, up to 60 meetings | No | One meeting can only be moved, resized or deleted |
| Mock | One RRULE plus EXDATE lines | Yes | RDATE and EXRULE throw |
| Cal.com, Calendly | No | No | Bookings do not repeat |
| ICS | Any RRULE, read only, with expansion limits | No | Same limits as CalDAV |
When a provider can't store a rule, calsdk throws UnsupportedFunctionalityError rather than saving a schedule that's close but wrong. A standup that silently lands on the wrong days is worse than an error. Each provider page lists its exact limits.