Generate recurring events with custom exceptions
Handle complex recurrence rules in code — every Tuesday except UK bank holidays.
Published Oct 28, 2025
Google Calendar’s built-in recurrence covers “every Tuesday” comfortably, but gets awkward the moment Northwind wants “every Tuesday except UK bank holidays, and never the week of the summer shutdown”. The UI forces you to create the series, then delete the offending instances by hand — fiddly, easy to forget, and impossible to audit.
Generating the series in code flips the model. The script knows about the weekly cadence and a list of dates to skip, and creates one independent event per occurrence. There is no underlying recurrence rule to fight, and the list of holidays lives next to the code so anyone can read what is excluded and why.
What you’ll need
- Edit access to the calendar the events should land on. The script uses your
default calendar — swap to
getCalendarByIdfor a shared team calendar. - A small list of YYYY-MM-DD dates to skip. The example uses two UK bank holidays, but the list can be as long as you like.
- Nothing else.
The script
// Default hour the recurring event starts on each occurrence (24h clock).
const EVENT_START_HOUR = 10;
// How long the event runs, in minutes.
const EVENT_DURATION_MINUTES = 60;
// The target day of the week — 0 = Sunday, 1 = Monday, ... 2 = Tuesday.
const TARGET_DAY_OF_WEEK = 2;
/**
* Creates `weeks` occurrences of a recurring event on the target day
* of the week, skipping any date that appears in `holidays`.
*
* @param {string} title Event title to use for every occurrence.
* @param {number} weeks How many occurrences to attempt.
* @param {string[]} [holidays] YYYY-MM-DD dates to skip.
*/
function createTuesdaySeries(title, weeks, holidays = []) {
const cal = CalendarApp.getDefaultCalendar();
const today = new Date();
// Normalise to midnight so the day-of-week maths is not affected by
// the trigger's time of day.
today.setHours(0, 0, 0, 0);
// Pre-compute how many days to step from today to land on the next
// occurrence of the target day. The `% 7` keeps the value in [0, 6].
const daysUntilFirst = (TARGET_DAY_OF_WEEK + 7 - today.getDay()) % 7;
let created = 0;
let skipped = 0;
for (let i = 0; i < weeks; i++) {
// 1. Walk forward in seven-day jumps from the first matching day.
const d = new Date(today);
d.setDate(today.getDate() + i * 7 + daysUntilFirst);
// 2. Compare the date against the holiday list in ISO form. Slicing
// `toISOString()` works because we are using UTC-anchored midnights.
const iso = Utilities.formatDate(d, 'GMT', 'yyyy-MM-dd');
if (holidays.includes(iso)) {
skipped++;
continue;
}
// 3. Set the start and end times for the occurrence. Mutating
// derived `Date` objects keeps the original `d` untouched.
const start = new Date(d);
start.setHours(EVENT_START_HOUR, 0, 0, 0);
const end = new Date(start.getTime() + EVENT_DURATION_MINUTES * 60_000);
cal.createEvent(title, start, end);
created++;
}
Logger.log('Created ' + created + ' events, skipped ' + skipped + ' holidays.');
}
/**
* Convenience wrapper — generates 12 weeks of the Northwind weekly sync,
* skipping the August and December bank holidays that fall on a Tuesday.
*/
function generateNorthwindWeekly() {
createTuesdaySeries('Northwind weekly sync', 12, [
'2026-08-25', // Summer bank holiday (UK)
'2026-12-29', // Holiday week shutdown
]);
}
How it works
createTuesdaySeriesnormalises today to midnight so the day-of-week arithmetic does not drift if the script runs in the afternoon.- It computes
daysUntilFirst— the offset from today to the first matching day of the week. Steppingi * 7 + daysUntilFirstdays lands on each subsequent occurrence. - For each candidate occurrence it formats the date as
yyyy-MM-ddin UTC and compares against the holiday list. A match is skipped; a miss continues to event creation. - The start time is derived by setting hours on a fresh copy of the date, and the end time by adding the configured duration in milliseconds.
cal.createEventcreates an independent event per occurrence — no recurrence rule, so removing one instance never cascades into the rest of the series.generateNorthwindWeeklyis the convenience wrapper that supplies the studio’s actual title, length, and exception list.
Example run
Run generateNorthwindWeekly on Monday 18 May 2026 and the calendar receives
twelve Tuesday slots between 19 May and 4 August, except where the holiday
list intervenes:
| Tuesday | Outcome |
|---|---|
| 19 May 2026 | Event created, 10:00 - 11:00 |
| 26 May 2026 | Event created |
| 2 Jun 2026 | Event created |
| … | … |
| 25 Aug 2026 | Skipped (summer bank holiday) |
| 29 Dec 2026 | Skipped (shutdown) |
The log line reads Created 10 events, skipped 2 holidays. — exactly what
you’d expect for a 12-week run with two excluded dates.
Run it
This is a generate-once kind of script — you do not want it triggered weekly or it will pile up duplicate events. Run it by hand at the start of each quarter:
- In the Apps Script editor, select
generateNorthwindWeeklyand click Run. - Approve the calendar authorisation prompt the first time.
- Open Google Calendar to confirm the new events.
If you do want a schedule, set a trigger that fires on the first of each quarter and bumps the start date forward by twelve weeks each time. Just keep in mind that the script as written does not check for existing events, so re-runs will create duplicates.
Watch out for
- The script creates one independent event per occurrence rather than a true recurring series. That is the point — exceptions are easy — but it also means moving the time later requires editing each instance. If the time rarely changes, this trade is worth it.
- The holiday list is plain strings in script. If your team needs richer rules (“the Tuesday after every UK bank Monday”), feed the list from a Sheet or a public iCal feed and parse it once at the top of the function.
Utilities.formatDate(d, 'GMT', ...)is reliable here because every event starts at midnight in the script’s perspective. If you change the script to use local times, format with the script time zone instead, or the comparison can slip by a day around midnight.- Re-running the script duplicates every event. Add a
getEventscheck on the start date if you want it idempotent, or delete the old series in the UI before running again.
Related
Schedule personal habits and routines
Block recurring habits on Awadesh's calendar — gym, walks, deep-work mornings.
Updated Nov 5, 2025
Sync birthdays and anniversaries to Calendar
Populate recurring personal dates from a Sheet — the Northwind team rituals calendar.
Updated Nov 1, 2025
Auto-reschedule low-priority conflicts
Move flexible Northwind events around fixed ones — focus blocks bend, client calls don't.
Updated Oct 16, 2025
Build a contract-renewal calendar
Track Northwind's recurring-revenue renewal dates as calendar events for proactive sales.
Updated Oct 8, 2025
Apply attendee rules by event type
Auto-invite the right Northwind people from a Sheet — design reviews get design, client calls get account leads.
Updated Oct 4, 2025