# Fix DTSTART and RRULE recurring event errors

Source: https://developer.nylas.com/docs/cookbook/calendar/fix-dtstart-rrule-errors/

You create a recurring event, the request succeeds, and then the series shows up wrong: occurrences land on the wrong day, the first instance is missing, or a strict validator rejects the rule with `DTSTART not synchronized with RRULE`. The cause is almost always the same. The first occurrence time and the repeat rule disagree about which day the series starts on.

This guide explains what the error means, the three mismatches that trigger it, and how to align the start time with the rule so every provider expands the series the same way.

## What does "DTSTART not synchronized with RRULE" mean?

It means the start of your event doesn't fall on a day the recurrence rule allows. `DTSTART` is the first occurrence; `RRULE` is the repeat pattern. When they disagree, the recurrence is unreliable. The iCalendar standard, [RFC 5545 §3.8.5.3](https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.5.3) published in 2009, defines the consequence directly:

> The recurrence set generated with a "DTSTART" property value not synchronized with the recurrence rule is undefined.

Undefined is the key word. The standard doesn't require a provider to reject the event, so behavior splits across the 4 calendar providers: some validators throw the error outright, while Google and Microsoft silently insert a phantom first occurrence or drop one. A request that returns a `200` status can still produce a broken series, which is why this surfaces after creation rather than during it.

## Why does the recurrence set come back undefined?

Three mismatches account for nearly every case, and the same rule must hold on all 4 providers: the first occurrence has to satisfy the `RRULE` on its own. If `BYDAY=MO` but the start time is a Tuesday, no valid Monday seeds the series, so the recurrence set is undefined and the provider improvises.

The list below covers the three causes in the order you should check them. The first one accounts for most reports, because day-of-week and day-of-month constraints are the easiest to get wrong when you compute a start time programmatically.

1. **Day mismatch.** The start time's weekday or month-day doesn't match `BYDAY`, `BYMONTHDAY`, or `BYMONTH`. A `BYDAY=MO` rule that starts on a Tuesday is the classic case.
2. **Value-type mismatch.** The start time is an all-day date but the rule implies a timed event (or the reverse). Mixing a date with a date-time confuses the comparison.
3. **UNTIL timezone mismatch.** When the start time is a timed value, `UNTIL` must be UTC with a trailing `Z`, per [RFC 5545 §3.3.10](https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10). A local-time `UNTIL` desynchronizes the boundary.

## How to fix a DTSTART and RRULE mismatch

Make the first occurrence satisfy the rule. With the Nylas Calendar API you never write `DTSTART` directly: the API derives it from the `when.start_time` you pass, then sends `DTSTART` plus your `recurrence` array to the provider. So the fix is to set `start_time` to a moment that the `RRULE` permits, then let the same rule repeat from there.

Here is the broken case. The rule repeats every Monday with `BYDAY=MO`, but `start_time` is `1736258400`, which is **Tuesday, January 7, 2025** at 9:00 a.m. Eastern. No Monday seeds the series, so the recurrence set is undefined and the first occurrence is unreliable.

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

The fix moves `start_time` to `1736172000`, which is **Monday, January 6, 2025** at 9:00 a.m. Eastern. Now the first occurrence is a Monday, the rule is satisfied, and every later occurrence inherits the same 9:00 a.m. slot. The request body is otherwise identical, and the same fix works across all 4 calendar providers.

```bash
# start_time is a MONDAY, matching BYDAY=MO -> series is well-defined
curl --request POST \
  --url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=<CALENDAR_ID>' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "title": "Weekly standup",
    "when": {
      "start_time": 1736172000,
      "end_time": 1736173800,
      "start_timezone": "America/New_York",
      "end_timezone": "America/New_York"
    },
    "recurrence": ["RRULE:FREQ=WEEKLY;BYDAY=MO"]
  }'
```

In application code, compute the first occurrence from the rule instead of writing a fixed timestamp by hand. The Node.js snippet below snaps a candidate date forward to the next Monday before sending it, so the start time always satisfies `BYDAY=MO`. It snaps in UTC, which is correct when the UTC and local weekday agree; for late-evening local start times, snap in the event's own timezone so the weekday matches there too.

```js
// Snap a date forward to the next Monday (day 1) so DTSTART matches BYDAY=MO
const snapToWeekday = (date, weekday) => {
  const shift = (weekday - date.getUTCDay() + 7) % 7;
  const snapped = new Date(date);
  snapped.setUTCDate(date.getUTCDate() + shift);
  return Math.floor(snapped.getTime() / 1000);
};

const startTime = snapToWeekday(new Date("2025-01-07T14:00:00Z"), 1); // -> a Monday

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

## DTSTART and RRULE errors by cause

Use this table to map a symptom to its fix without re-reading the whole rule. Each row pairs a recurrence rule with the start-time condition it requires, and the same fix applies on all 4 providers. They're variations of one principle: the first occurrence must independently satisfy every `BY` part in the rule.

| Symptom | Cause | Fix |
| --- | --- | --- |
| `DTSTART not synchronized with RRULE` | Start weekday isn't in `BYDAY` | Set `start_time` to a date whose weekday matches `BYDAY` |
| First monthly occurrence missing | Start day isn't the `BYMONTHDAY` value | Set `start_time` to that day of the month (for example the 15th) |
| Series drifts by an hour twice a year | No `start_timezone`, so the rule runs in UTC | Pass an IANA `start_timezone` like `America/New_York` |
| `UNTIL` ignored or off by a day | `UNTIL` uses local time, or Microsoft Graph's `+1` day quirk | Write `UNTIL` in UTC (`...T000000Z`) and account for Graph adding one day |

A quick sanity check before you send: confirm the weekday of `start_time` equals one of the `BYDAY` codes. For `FREQ=WEEKLY;BYDAY=MO,WE,FR`, the start must be a Monday, Wednesday, or Friday. For `FREQ=MONTHLY;BYMONTHDAY=15`, it must be the 15th. This single check prevents the majority of undefined-recurrence reports.

## How the unified Calendar API prevents RRULE errors

The Nylas Calendar API reduces the surface where these errors appear by deriving `DTSTART` from your `when` object and passing the `RRULE` to the provider unchanged. You manage one `recurrence` array and one `start_time` across Google, Microsoft, iCloud, and Exchange instead of four provider-specific recurrence models, so a rule that validates once validates everywhere.

A few provider behaviors still leak through, and knowing them prevents the off-by-one cases the standard leaves undefined. Microsoft Graph adds 1 day to the `UNTIL` date, so a series meant to end December 31 runs through January 1 unless you subtract a day. iCloud doesn't allow changing an event from recurring to non-recurring after creation. Across providers, deleted occurrences usually become `EXDATE` entries, except on Google, which returns the occurrence with `status` set to `cancelled`. The API normalizes the data model, but the rule still has to be valid: a synchronized `DTSTART` is your responsibility on every provider.

## What's next

- [Create recurring events](/docs/cookbook/calendar/create-recurring-events/) for the full RRULE part list and EXDATE handling
- [Recurring events and RRULE reference](/docs/v3/calendar/recurring-events/) for per-provider behavior and editing part of a sequence
- [List Google calendar events](/docs/cookbook/calendar/events/list-events-google/) to expand a series into individual occurrences with a date range
- [Events API reference](/docs/reference/api/) for every event field and query parameter