Skip to content
Skip to main content

Fix DTSTART and RRULE recurring event errors

Last updated:

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?

Section titled “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 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?

Section titled “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. A local-time UNTIL desynchronizes the boundary.

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.

# 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.

# 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.

// 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"],
},
});

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.

SymptomCauseFix
DTSTART not synchronized with RRULEStart weekday isn’t in BYDAYSet start_time to a date whose weekday matches BYDAY
First monthly occurrence missingStart day isn’t the BYMONTHDAY valueSet start_time to that day of the month (for example the 15th)
Series drifts by an hour twice a yearNo start_timezone, so the rule runs in UTCPass an IANA start_timezone like America/New_York
UNTIL ignored or off by a dayUNTIL uses local time, or Microsoft Graph’s +1 day quirkWrite 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

Section titled “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.