Guide
Recurring Calendar Events: RRULE Explained
A weekly standup is one event in the database and fifty-two on the screen. Every calendar system handles that gap with recurrence rules — RRULE strings from RFC 5545 — and every integration eventually hits the same questions: what the rule syntax means, who expands the series into instances, and what happens when one occurrence moves. This guide answers all three, with the Google, Microsoft Graph, and CalDAV specifics side by side.
Written by Hazik Director of Product Management
Command references used in this guide: nylas calendar events list and nylas calendar events show.
What is an RRULE?
An RRULE is the recurrence rule grammar defined in RFC 5545 (the iCalendar specification) that describes a repeating schedule as a single string. One rule replaces hundreds of stored rows: the event exists once, and software derives the occurrences. All three major calendar systems either store RRULE directly or translate to and from it at their boundaries.
The grammar has one required part, FREQ, and a set of modifiers. The 6 you'll actually use are below — and one constraint from the RFC matters more than any of them: the spec states the UNTIL and COUNT parts "MUST NOT occur in the same 'recur'". A rule ends one of exactly 3 ways: never, after N occurrences, or at a date.
| Part | Meaning | Example |
|---|---|---|
FREQ | Base cadence (required) | FREQ=WEEKLY |
INTERVAL | Every Nth period (default 1) | INTERVAL=2 — biweekly |
BYDAY | Weekday selector; supports ordinals | BYDAY=MO,WE,FR or BYDAY=-1FR (last Friday) |
BYMONTHDAY | Day-of-month selector | BYMONTHDAY=15 |
COUNT | End after N occurrences | COUNT=12 |
UNTIL | End at a UTC timestamp | UNTIL=20261231T235959Z |
# Every Monday and Wednesday for 12 occurrences
RRULE:FREQ=WEEKLY;BYDAY=MO,WE;COUNT=12
# Biweekly Friday, forever
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=FR
# Last Friday of each month through 2026
RRULE:FREQ=MONTHLY;BYDAY=-1FR;UNTIL=20261231T235959ZWho expands the series into instances?
Expansion, turning one rule into dated occurrences, happens in a different place on each API, and that placement decides how much recurrence code you write. Google expands server-side on request, Graph stores a structured object and exposes an instances view, and CalDAV ships the raw rule and leaves the math to the client. The 3 models in brief:
- Google Calendar API — pass
singleEvents=truetoevents.listand the response contains individual instances within your time window, each carrying arecurringEventId. Google's recurring events guide covers the master-vs-instance model. - Microsoft Graph — events store a patternedRecurrence object with a
pattern(cadence) andrange(bounds) instead of a raw RRULE string; instance expansion comes from the series master'sinstancesview, bounded by start and end parameters. - CalDAV — the server returns the VEVENT with its RRULE verbatim, and the client expands it, including timezone math. Getting this right unassisted is the hardest path; RFC 5545's recurrence section runs to dozens of pages for a reason.
The practical rule: if your integration only needs "what happens this week," prefer an API (or tool) that expands for you. Writing a correct client-side expander is a project, not a function. For how these 3 APIs differ beyond recurrence (auth, availability, webhooks), see the calendar API comparison; this page stays on the recurrence problem.
What happens when one occurrence moves or is cancelled?
A modified single occurrence becomes an exception: a standalone record that overrides the rule for one date while the rest of the series follows the master. Each system encodes this differently — Google creates a separate event with an originalStartTime field, Graph models it as an exception occurrence type, and iCalendar adds a second VEVENT with a RECURRENCE-ID matching the moved instance.
Exceptions are where naive sync code breaks. Two failure modes account for most bugs: treating an exception as a brand-new event (the meeting appears twice: once from rule expansion, once as the override), and applying a series edit on top of an exception (the user's one-time room change silently reverts). Sync logic has to check for an exception marker before applying either path — a 2-branch check that's much cheaper than the duplicate-meeting support ticket.
How do you read recurring events from the CLI?
The nylas calendar events list command returns expanded instances inside the window you request, so a weekly series shows up as dated occurrences without any RRULE parsing on your side. Each instance of a recurring series carries a master_event_id linking back to the series master, and the instance's own ID embeds its occurrence timestamp — both visible in the JSON output.
# Expanded instances for the next 30 days
nylas calendar events list --days 30 --json | jq '.[] | {
id: .id,
title: .title,
master: .master_event_id,
start: .when.start_time
}'
# Group instances by series to spot every recurring meeting
nylas calendar events list --days 30 --json | jq '
[.[] | select(.master_event_id != null)]
| group_by(.master_event_id)
| map({series: .[0].title, occurrences: length})'The same 2 commands work against Google, Microsoft, and iCloud accounts, which removes the per-API expansion differences from the previous section for read paths. One honest limitation: creating events with the CLI's calendar events create command doesn't take a recurrence flag — series creation stays in your provider's UI or API, and the CLI handles the reading, filtering, and scripting side.
Why do recurring events shift around daylight saving time?
A weekly 9:00 meeting is 9:00 in a named timezone, not a fixed UTC offset — so twice a year, its UTC time moves by an hour. Rules expanded against raw UTC drift after each DST transition; correct expansion resolves each occurrence through the timezone database (America/Toronto, not UTC-5). This is most of why client-side CalDAV expansion is hard, and it's also encoded in RFC 5545's requirement that UNTIL timestamps be UTC while event starts carry timezone references.
When auditing a sync bug that appears in March or November, check which clock the expander used before anything else. The instance JSON above includes start_timezone on timespan events, so a 1-line jq filter for instances whose timezone differs from your expectation finds the drifted series fast.
Next steps
- Calendar API Compared: Google, Microsoft, CalDAV — auth, availability, and recurrence differences in one place
- Sync calendars across providers — where recurrence exceptions bite hardest
- Manage Google Calendar from the CLI — the Google-specific event workflow
- Manage Yahoo Calendar from the CLI — recurring reads on Yahoo's CalDAV-backed calendars
- Full command reference — every calendar events flag documented