iCalForge

Recurring events and RRULE: every-second-Tuesday without losing your mind

6 min read · Updated September 2026

A laptop showing a spreadsheet with charts next to a tablet held open at a year calendar

"Every second Tuesday of the month" is four words in English and one line in a calendar file. That line is RRULE, the recurrence rule, and it is the densest part of the iCalendar standard: a handful of parameters that combine into almost any pattern a human calendar can hold. It is also where most calendar bugs live, because a rule that looks right can generate dates that are subtly wrong — and nobody notices until somebody misses a meeting.

One line instead of a hundred events

RRULE lives inside a VEVENT, next to DTSTART. The whole series is a single event with a single UID — not a hundred copies — which is why editing a recurring meeting in any calendar app offers you the choice between "this event" and "the whole series". DTSTART is the first occurrence and quietly supplies the defaults: the time of day, and, where the rule does not say otherwise, the weekday and the day of the month.

If your pattern is not a pattern at all — six dates agreed with a client, say — you do not need RRULE. RDATE lists extra dates explicitly, and a series can consist of RDATE lines alone. It is more verbose but perfectly legal, and easier to read when the dates have no rhythm.

FREQ and INTERVAL: the skeleton of the rule

Every rule starts with FREQ. In practice you will use DAILY, WEEKLY, MONTHLY and YEARLY; the standard also defines SECONDLY, MINUTELY and HOURLY, which calendar apps support unevenly and which you should avoid in files meant for other people. INTERVAL multiplies the frequency and defaults to 1, so FREQ=WEEKLY;INTERVAL=2 is a fortnightly meeting and FREQ=MONTHLY;INTERVAL=3 is quarterly.

WKST decides which day starts the week, and it only matters for weekly rules with an interval greater than one — but there it matters a lot. The RFC default is Monday. Some older clients assume Sunday, and the result is a fortnightly series that lands on the wrong weeks for half the year. Writing WKST=MO explicitly costs six characters and removes the ambiguity.

BYDAY, BYMONTHDAY and BYSETPOS

BYDAY takes two-letter codes — MO, TU, WE, TH, FR, SA, SU — and is how a weekly rule covers several days: FREQ=WEEKLY;BYDAY=MO,WE,FR. Inside a monthly or yearly rule, each code may carry an ordinal prefix: BYDAY=2TU means the second Tuesday, and BYDAY=-1FR means the last Friday. That prefix is what makes "every second Tuesday of the month" a single, exact line: FREQ=MONTHLY;BYDAY=2TU.

BYMONTHDAY selects days by number and accepts negatives counted from the end, so BYMONTHDAY=-1 is the last day of every month. A positive number that some months do not have is simply skipped: FREQ=MONTHLY;BYMONTHDAY=31 produces seven occurrences a year, not twelve, and never lands on 30 or 28 February. That is the specified behaviour, not a bug, and it catches people out when they set a monthly invoice reminder on the 31st.

BYSETPOS is the one that feels like magic. It selects positions from the set the rest of the rule generated within each period. FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1 means "the last working day of the month", which no other combination can express. BYSETPOS=1 with the same BYDAY gives the first working day. Support is good in Apple Calendar and Thunderbird, decent in Google Calendar, and historically patchy in older Outlook versions — worth testing if your audience is corporate.

Ending a series: COUNT or UNTIL

A rule may carry COUNT or UNTIL, but never both, and a rule with neither runs forever. COUNT is a number of occurrences including the first one, so COUNT=8 on a fortnightly meeting covers sixteen weeks. UNTIL is a date-time and is inclusive: an occurrence falling exactly on it still happens.

UNTIL has one rule that breaks more files than any other part of RRULE: its value type must match DTSTART, and when DTSTART is a date-time with a TZID, UNTIL must be written in UTC with a trailing Z. If DTSTART is a plain date (an all-day event), UNTIL must be a plain date too. Writing UNTIL=20261231T235959 next to a DTSTART with a TZID is the single most common validation error in generated calendar files.

Endless series are legal and useful for things like a standing weekly call, but they are heavier on the client: some apps materialise a bounded window of instances and re-expand it as you scroll, and a few older ones silently stop after a few hundred. If the series genuinely has an end, say so — it also makes the file easier for a human to reason about.

Skipping a date and changing a single instance

EXDATE removes dates from the series. It is a list of date-times that must match DTSTART in both value type and time zone: if DTSTART carries TZID=Europe/Tallinn and a 09:00 time, every EXDATE value must be 09:00 in that same zone. A value that differs by even a minute matches nothing and the occurrence stays. When you delete one instance of a recurring meeting in a calendar app, this is what the app writes for you.

Changing a single instance rather than removing it needs a second VEVENT with the same UID and a RECURRENCE-ID property naming the original start time of the occurrence you are overriding. That second block can then carry a different time, title or location. It is the mechanism behind "this meeting only, moved to Thursday", and it is why a file describing one series can legitimately contain several VEVENT components.

Daylight saving, and the quirks you will meet

Recurrence is expanded in wall-clock time, not in absolute instants. A weekly 09:00 stand-up with TZID=Europe/Tallinn stays at 09:00 through the March and October clock changes, which means the UTC moment it happens shifts by an hour twice a year. That is almost always what people want, and it is the reason you should not pin a recurring meeting to a UTC time: written as ...T070000Z, the same meeting would drift to 10:00 local every summer.

The awkward case is an occurrence that lands in an hour the clock skips. In most European zones the hour between 03:00 and 04:00 simply does not exist on one Sunday in March. RFC 5545 leaves the handling to the client; in practice clients push the occurrence forward to the first valid time. Avoid the question entirely by not scheduling recurring events at 03:30.

Clients also disagree at the edges. Google Calendar rewrites imported rules into its own representation and may simplify an exotic one; Outlook's own recurrence editor cannot express everything RRULE can, so it approximates when you open such an event; Apple Calendar handles ordinal BYDAY well but shows BYSETPOS rules in a generic "custom" form. The safe habit is the same everywhere: look at the first ten generated dates before you send the file.

The takeaway

Build the rule from the outside in — frequency, then interval, then the BY- parts, then the ending — and always read back the dates it produces. Write WKST=MO on fortnightly rules, keep UNTIL in UTC when DTSTART has a time zone, use EXDATE for a skipped week and RECURRENCE-ID for a moved one, and let the wall clock, not UTC, define a recurring meeting.

Related tool
iCalForge · .ics generator

Build an event and download the .ics file