# How to create recurring events

Source: https://developer.nylas.com/docs/cookbook/calendar/create-recurring-events/

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](/docs/cookbook/calendar/events/create-events-google/). This page focuses only on the recurrence piece.

## Create a recurring event

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.

```bash
curl --request POST \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=<CALENDAR_ID>' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "title": "Weekly standup",
    "busy": true,
    "when": {
      "start_time": 1674604800,
      "end_time": 1674606600,
      "start_timezone": "America/New_York",
      "end_timezone": "America/New_York"
    },
    "recurrence": ["RRULE:FREQ=WEEKLY;BYDAY=MO"]
  }'
```

```js [createRecurring-Node.js SDK]
const Nylas = require("nylas").default;

const nylas = new Nylas({ apiKey: "<NYLAS_API_KEY>" });

const event = await nylas.events.create({
  identifier: "<NYLAS_GRANT_ID>",
  queryParams: { calendarId: "<CALENDAR_ID>" },
  requestBody: {
    title: "Weekly standup",
    busy: true,
    when: {
      startTime: 1674604800,
      endTime: 1674606600,
      startTimezone: "America/New_York",
      endTimezone: "America/New_York",
    },
    recurrence: ["RRULE:FREQ=WEEKLY;BYDAY=MO"],
  },
});

console.log(event);
```

```python [createRecurring-Python SDK]
from nylas import Client

nylas = Client(api_key="<NYLAS_API_KEY>")

event = nylas.events.create(
    identifier="<NYLAS_GRANT_ID>",
    query_params={"calendar_id": "<CALENDAR_ID>"},
    request_body={
        "title": "Weekly standup",
        "busy": True,
        "when": {
            "start_time": 1674604800,
            "end_time": 1674606600,
            "start_timezone": "America/New_York",
            "end_timezone": "America/New_York",
        },
        "recurrence": ["RRULE:FREQ=WEEKLY;BYDAY=MO"],
    },
)

print(event)
```

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.

## Write the RRULE

An RRULE is a single string built from semicolon-separated parts, defined by [RFC 5545](https://www.rfc-editor.org/rfc/rfc5545#section-3.8.5). 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.

| Part       | What it controls                       | Example                          |
| ---------- | -------------------------------------- | -------------------------------- |
| `FREQ`     | Repeat unit: `DAILY`, `WEEKLY`, `MONTHLY`, `YEARLY` | `FREQ=WEEKLY`           |
| `INTERVAL` | Step between repeats (default `1`)     | `INTERVAL=2` (every 2 weeks)     |
| `BYDAY`    | Days of the week, or an ordinal day    | `BYDAY=MO,WE,FR` or `BYDAY=1MO`  |
| `COUNT`    | Total number of occurrences            | `COUNT=10` (stops after 10)      |
| `UNTIL`    | End 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](/docs/v3/calendar/recurring-events/).

## Update or cancel a series vs one instance

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.

```bash
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](/docs/v3/calendar/recurring-events/) covers editing part of a sequence (the "this and following" case), which needs an extra step.

## Things to know about recurring events

Recurrence is governed by [RFC 5545](https://www.rfc-editor.org/rfc/rfc5545), 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.

### Timezone and DST handling

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](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) like `America/New_York`.

### Google vs Microsoft differences

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.

### Exceptions and overrides

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.

### Expanding instances when you list

Recurring events expand into individual occurrences only when you read a date range. A [list events](/docs/cookbook/calendar/events/list-events-google/) 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.

## What's next

- [Create Google calendar events](/docs/cookbook/calendar/events/create-events-google/) for single events, Google Meet auto-creation, and participant invites
- [Recurring events and RRULE reference](/docs/v3/calendar/recurring-events/) for the full token list, EXDATE handling, and per-provider behavior
- [Checking calendar availability](/docs/v3/calendar/calendar-availability/) to find open slots before you schedule a series
- [Events API reference](/docs/reference/api/) for every event field and query parameter