Skip to content
Skip to main content

How to create recurring events

Last updated:

Your team runs a standup every weekday at 9:00 a.m., and you don’t want to create 260 separate events for the year. A recurring event solves this with one request and one schedule rule. The hard part is the rule itself: it’s an RRULE string with cryptic tokens like FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR. This recipe shows you how to write that string and attach it to an event.

For a single non-repeating event, including Google Meet auto-creation and participant invites, see Create Google calendar events. This page focuses only on the recurrence piece.

Send a POST to /v3/grants/{grant_id}/events?calendar_id={calendar_id} with a recurrence array and a when object. The recurrence array holds one RRULE string that defines the repeat pattern, and when sets the time and timezone of the first occurrence. One request creates all 52 weekly occurrences, and every later occurrence inherits that same time of day.

The request below creates a weekly standup that repeats every Monday. The start_timezone and end_timezone fields anchor the series to America/New_York, so the 9:00 a.m. slot stays correct through daylight saving changes. Without a timezone, the series falls back to UTC.

The response returns the created event with an id, a recurrence array that echoes your RRULE, and the when object you sent. Save that id. You’ll need it to update or cancel the series later. The same request body works for Google, Microsoft, iCloud, and Exchange accounts with no provider-specific changes.

An RRULE is a single string built from semicolon-separated parts, defined by RFC 5545. At minimum you set FREQ (how often) and then narrow it with parts like INTERVAL, BYDAY, COUNT, or UNTIL. Nylas passes the string to the provider unchanged, so the same rule behaves consistently across all 4 supported calendar providers.

The 5 parts below cover most real schedules. Combine them in one string, separated by semicolons, with no spaces. For example, RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO repeats every other Monday.

PartWhat it controlsExample
FREQRepeat unit: DAILY, WEEKLY, MONTHLY, YEARLYFREQ=WEEKLY
INTERVALStep between repeats (default 1)INTERVAL=2 (every 2 weeks)
BYDAYDays of the week, or an ordinal dayBYDAY=MO,WE,FR or BYDAY=1MO
COUNTTotal number of occurrencesCOUNT=10 (stops after 10)
UNTILEnd date in UTC (YYYYMMDDTHHMMSSZ)UNTIL=20251231T000000Z

Here’s how those parts map to common scenarios:

  • Every Monday: RRULE:FREQ=WEEKLY;BYDAY=MO
  • Every 2 weeks on Monday: RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO
  • First Monday of each month: RRULE:FREQ=MONTHLY;BYDAY=1MO
  • Weekdays for 10 occurrences: RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;COUNT=10

To skip specific dates in a series, add an EXDATE string as a second array entry, like ["RRULE:FREQ=WEEKLY;BYDAY=MO", "EXDATE:20251110T000000Z"]. Nylas doesn’t support the EXRULE or RDATE parts. For the full token list and provider behavior, see the recurring events reference.

Editing a recurring event changes either one occurrence or the whole series, depending on which id you target. Each occurrence has its own event id and a shared master_event_id that links it to the parent. Send a PUT to /v3/grants/{grant_id}/events/{event_id} against an occurrence id to change just that instance, or against the parent id to change every occurrence.

The request below reschedules a single Monday standup to a later time without touching the rest of the series. Because you target one occurrence id, the other 51 weekly events stay put. The response includes an original_start_time field marking the instance you moved.

curl --request PUT \
--url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events/<EVENT_ID>?calendar_id=<CALENDAR_ID>' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer <NYLAS_API_KEY>' \
--header 'Content-Type: application/json' \
--data '{
"when": {
"start_time": 1674820800,
"end_time": 1674822600,
"start_timezone": "America/New_York",
"end_timezone": "America/New_York"
}
}'

To cancel one occurrence, send a DELETE to the same path with that occurrence’s id. To cancel the entire series, send a DELETE with the parent id. Each participant receives an event notification per change, so a single-occurrence edit on a busy series can still fan out widely. The recurring events reference covers editing part of a sequence (the “this and following” case), which needs an extra step.

Recurrence is governed by RFC 5545, the same iCalendar standard every major provider implements, but providers diverge in small ways that surface through the API. The notes below cover the 4 behaviors that trip people up most when building against real Google and Microsoft accounts.

RRULE defines a time of day, not an absolute instant, so daylight saving transitions matter. A 9:00 a.m. weekly event stays at 9:00 a.m. local time even when the clock shifts in spring and fall, as long as you set start_timezone and end_timezone. If you omit the timezone, the series runs in UTC and appears to drift by an hour for participants during the 2 annual DST changes. Always pass an IANA timezone like America/New_York.

The 2 largest providers handle pattern edits differently. When you change a series, for example from weekly to daily, Google keeps existing overrides that still fit the new pattern, while Microsoft removes them. Microsoft also can’t create an event that recurs monthly on multiple weekdays, such as FREQ=MONTHLY;BYDAY=1TH,3TH, because its recurrence model puts the index on the month, not the day. Build for the stricter of the two if you support both.

An override is a single occurrence that differs from the parent, for example one standup you moved to the afternoon. The provider stores it separately and links it through master_event_id. Deleted occurrences become EXDATE entries, except on Google, which returns the deleted occurrence with status set to cancelled instead of adding an EXDATE. Editing an EXDATE value after creation returns a 200 but doesn’t remove the event, so use a DELETE request to drop an occurrence.

Recurring events expand into individual occurrences only when you read a date range. A list events request with start and end query parameters returns each occurrence in that window as its own object sharing one master_event_id. Without a date range, you get the parent event and its RRULE, not the expanded list. Set a bounded window so an open-ended FREQ=DAILY series doesn’t try to return thousands of instances.