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:

RuleRepeats
RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FREvery Monday, Wednesday and Friday
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=TUEvery other Tuesday
RRULE:FREQ=MONTHLY;BYDAY=1MOThe first Monday of each month
RRULE:FREQ=MONTHLY;BYMONTHDAY=15;COUNT=6The 15th, six times
RRULE:FREQ=DAILY;UNTIL=20261231T235959ZEvery 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:

KindWhat it isHas
singleAn event that does not repeat
seriesThe recurring event itselfrecurrence
occurrenceOne instance, as the rule placed itseriesId, originalStart
exceptionOne instance someone changedseriesId, 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:

scopeChanges
'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

ProviderRepeatsthisAndFollowingNotes
GoogleAny RRULEYesSingle-instance changes are reset when a series moves or splits, with a warning
OutlookRules Outlook has a pattern forNoRDATE, hourly rules and some others throw
CalDAVAny RRULE, with expansion limitsNoExpanded by calsdk: rules repeating every minute or second, or with an INTERVAL above 1,000, list only their first instance
ZoomDaily, weekly or monthly, up to 60 meetingsNoOne meeting can only be moved, resized or deleted
MockOne RRULE plus EXDATE linesYesRDATE and EXRULE throw
Cal.com, CalendlyNoNoBookings do not repeat
ICSAny RRULE, read only, with expansion limitsNoSame 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.

On this page