I built an Apps Script add-on that imports Jewish and Christian holidays directly into a user’s main Google Calendar, with duplicate protection, one-day-before reminders, color coding, and an annual auto-refresh. No external backend, no JSON feed — just two official Google holiday calendars, user selections saved in PropertiesService, and pure Apps Script UI.
This article explains what the add-on does, why the exact implementation matters, and the key code decisions you’ll want to copy.
The concrete problem
Subscriptions and ICS imports fail in predictable ways:
- ICS imports create duplicates or orphaned events.
- Users forget to re-import for the next year.
- Many “holiday feeds” are regional or incomplete.
- Calendar add-ons are awkward because CardService is under-documented.
I needed a small, reliable tool that copies relevant holiday events into the user’s primary calendar and keeps them up to date each year.
What this add-on actually does (precisely)
Sources: reads events from two Google-managed holiday calendars:
- en.judaism#holiday@group.v.calendar.google.com
- en.christian#holiday@group.v.calendar.google.com
Filter: matches event titles against a built-in defaultHolidays array or the user’s saved selection.
Target: inserts events directly into the user’s main calendar (Session.getActiveUser().getEmail()).
Duplicate protection: checks existing events on the same day for identical titles before adding.
Reminders: creates a pop-up reminder on the previous day at the user-selected hour (default 21:00).
Colors: sets color IDs per source calendar.
Auto-refresh: deletes old imported holidays and re-imports up to one year forward; runs via a yearly trigger (guarded by yearlyHolidayCheck()).
No external data files: everything needed is in script files and PropertiesService.
Key code snippets (behavioral highlights)
Source calendars and target calendar
const jewishCalendarId = "en.judaism#holiday@group.v.calendar.google.com";
const christianCalendarId = "en.christian#holiday@group.v.calendar.google.com";
const targetCalendarId = Session.getActiveUser().getEmail();
Which holidays are considered
Hard-coded fallback list:
const defaultHolidays = [
"Christmas","Good Friday","Easter","Pentecost",
"Palm Sunday","Ascension","Assumption","All Saints",
"Rosh Hashanah","Yom Kippur","Sukkot",
"Passover","Shavuot","Hanukkah"
];
Or user-selected list stored in SELECTED_HOLIDAYS via PropertiesService.
Duplicate-safe import (title match)
const existing = targetCal.getEventsForDay(startTime).find(e => e.getTitle() === title);
if (existing) continue;
One day before the pop-up reminder at the chosen hour
const reminderTime = new Date(startTime);
reminderTime.setDate(reminderTime.getDate() - 1);
reminderTime.setHours(reminderHour, 0, 0, 0);
const minutesBefore = (startTime - reminderTime) / (1000 * 60);
newEvent.addPopupReminder(minutesBefore);
Annual refresh and email confirmation
autoRefreshHolidays() calls deleteHolidayEvents() then syncHolidayEvents() and emails the user a completion note. The trigger is created with:
ScriptApp.newTrigger("yearlyHolidayCheck")
.timeBased()
.onMonthDay(1)
.atHour(0)
.create();
yearlyHolidayCheck() includes a guard that verifies the date is Jan 1 before running.
UI and settings
- CardService (UI.gs) provides the add-on Homepage and onboarding flow inside Calendar.
- An HTML wizard (Onboarding.html) and a Sidebar.html provide an alternate modal/sidebar interface for initial setup and settings.
- Settings and user selections are saved to PropertiesService (no external storage).
Resources
If you want the full production-ready version of this add-on (complete UI flow, trigger setup, onboarding wizard, and settings sidebar), I’ve packaged the template here.
→ Get it here
Top comments (0)