Skip to content

Recurring events across Google Calendar and Outlook: RRULE vs Graph recurrence patterns

Updated

TL;DR

Google stores a series as RFC 5545 lines (RRULE, EXDATE, RDATE) in recurrence[]; Graph stores a patternedRecurrence with six pattern types and three range types. Occurrences are identified by recurringEventId and originalStartTime on Google, and by type (occurrence, exception, seriesMaster) on Graph. Many RRULEs have no Graph equivalent, such as rules more often than daily or a fifth Monday, and neither API has a single call for "this and following".

How each provider stores a series

Google keeps a recurrence array: "List of RRULE, EXRULE, RDATE and EXDATE lines for a recurring event, as specified in RFC5545. Note that DTSTART and DTEND lines are not allowed". The event's own start is the first occurrence, and start.timeZone is required, because it "specifies the time zone in which the recurrence is expanded."

JSON
"recurrence": [
  "RRULE:FREQ=WEEKLY;BYDAY=TU,TH;UNTIL=20261231T235959Z",
  "EXDATE;TZID=America/New_York:20261126T090000"
]

Microsoft Graph keeps a patternedRecurrence, with a pattern and a range:

JSON
"recurrence": {
  "pattern": { "type": "weekly", "interval": 1, "daysOfWeek": ["tuesday", "thursday"], "firstDayOfWeek": "sunday" },
  "range": { "type": "endDate", "startDate": "2026-11-03", "endDate": "2026-12-31" }
}

The pattern type is one of daily, weekly, absoluteMonthly, relativeMonthly, absoluteYearly or relativeYearly, with index (first to fourth, or last) for the relative ones. The range is endDate, noEnd or numbered; startDate "Must be the same value as the start property of the recurring event", and endDate is inclusive.

Rules Outlook can't express

RRULE is far more expressive than Graph's patterns. Rules with no Outlook equivalent include:

  • anything more often than daily (FREQ=HOURLY), or with BYHOUR, BYMINUTE, BYYEARDAY or BYWEEKNO,
  • a weekday position other than first to fourth or last, such as the fifth Monday or the second-to-last Friday,
  • more than one BYSETPOS, or more than one day of the month,
  • a weekly rule with numbered weekdays, or BYDAY together with BYMONTHDAY.

If you translate RRULEs to Graph, refuse these with a clear error. Saving the closest pattern silently gives users a different series from the one they asked for.

Occurrences and exceptions

On Google, list with singleEvents=true to get occurrences instead of series, or call events.instances for one series. Each occurrence has recurringEventId, "the id of the recurring event to which this instance belongs", and originalStartTime, which "uniquely identifies the instance within the recurring event series even if the instance was moved to a different time." A cancelled occurrence comes back with "status": "cancelled", and it is "only guaranteed to have values for the id, recurringEventId and originalStartTime fields populated."

On Graph, every event has a type: singleInstance, occurrence, exception or seriesMaster. /events returns "single instance meetings and series masters"; calendarView and /instances return occurrences and exceptions. Cancelled occurrences are missing from /instances and listed on the series master in cancelledOccurrences.

One difference catches sync code: on Google every occurrence shares the series' iCalUID, while Graph's iCalUId "is different for each occurrence in a recurring series".

Editing one occurrence

Google: read the instance, change it and update it by its own ID; to cancel it, set its status to cancelled. Google's guide warns: "Do not modify instances individually when you want to modify the entire recurring event, or 'this and following' instances."

Graph: update the occurrence by its ID. An occurrence "cannot be moved to or before the day of the previous occurrence, and cannot be moved to or after the day of the following occurrence", and Graph answers ErrorOccurrenceCrossingBoundary if you try.

"This and following"

Neither API has a call for it. The approach both need is the one Google used to document: end the original series just before the target occurrence, by setting UNTIL (or COUNT) on Google or the range's endDate on Graph, then create a new series with the same details, starting at the target occurrence, with the change applied. Edits to individual occurrences after the split point don't carry over to the new series, so copy any you need.

How Pyas does it

Pyas gives every recurring event one shape across Google, Outlook and Pyas calendars:

  • recurrence is a single RRULE, such as FREQ=WEEKLY;BYDAY=TU,TH, written once and translated for each provider. Deleted occurrences stay deleted when you update a Google series.
  • series on an occurrence gives the series ID, the occurrence's original start in UTC and whether it is an exception, on both providers.
  • Rules Outlook can't hold are refused, with an error that says why (for example "Outlook cannot represent this recurrence: a fifth Monday"), never saved as a different rule.
  • GET /v2/events/{eventId}/instances lists a series' occurrences between two times, and expand=true returns occurrences instead of series when you list events.
  • Updating an occurrence updates only that occurrence.
  • One webhook per series change. When a whole series changes, your webhook says so once, with isSeries: true, instead of once per occurrence.

Times are expanded in the series' own zone, so a 9:00 meeting stays at 9:00 across clock changes. See time zones in calendar APIs for why that matters.

Time zones in calendar APIs: IANA vs Windows names, clock changes and all-day eventsTwo-way calendar sync with Google Calendar and Outlook: the architecture, and the edge cases that break it