Why you should care
Working across multiple organizations means juggling multiple calendars. One uses Office 365, another uses Google Workspace. I was manually checking both before scheduling meetings—until I double-booked myself twice in one month.
This is how I fixed it using Google Apps Script, without needing OAuth permissions from IT.
The problem
I needed to:
- See Office 365 events in Google Calendar
- See Google events in Office 365
- Prevent double-booking
Sounds simple, right?
Attempt 1: Existing tools
I tried OneCal, CalendarBridge, and SyncGene. All around $5-10/month. All promised bidirectional sync.
None of them worked.
Why? OAuth restrictions. My Office 365 organization blocks external app connections. The OAuth screen said "Administrator approval required."
Sure, I could file a ticket with IT asking them to approve a calendar sync tool. But let's be realistic—that's not happening.
Attempt 2: Build my own?
I considered building a full bidirectional sync tool. Then I realized what that would involve:
- Infinite loop prevention (A→B→A→B...)
- Event ID mapping (Google IDs ≠ Outlook IDs)
- Conflict resolution (simultaneous edits)
- Deletion handling (what if someone deletes on one side?)
That's basically rebuilding OneCal from scratch. Hard pass.
Let me reframe the requirement: I don't need perfect sync. I just need to avoid double-booking.
The solution: One direction at a time
Google → Outlook (5 minutes)
This was easy. Outlook Web has an "Add calendar from Internet" feature.
- Get your Google Calendar's ICS URL (Settings → Integrate → Secret address in iCal format)
- Paste it into Outlook's "Add calendar from Internet"
- Done
Outlook now shows my Google events and considers them for availability.
Time spent: 5 minutes.
Outlook → Google (5 days)
Same approach, right? Get Outlook's ICS URL, subscribe to it in Google Calendar.
It worked—sort of. I could see the Outlook events in Google Calendar.
But here's the catch: Google Calendar's URL subscriptions are read-only and don't count toward availability.
When I tried to create a new event in Google, it didn't warn me about conflicts with my subscribed Outlook calendar. The time showed as "available" even though I had a meeting.
This defeats the whole purpose.
The fix: Create actual events with GAS
If subscribed calendars don't count toward availability, I'll create real events.
The plan:
- Read events from the Outlook ICS
- Create "Block" events in a dedicated Google Calendar
- Sync only time ranges (no titles, no details—security win)
- These are real events, so Google respects them for availability
This approach:
- ✅ Bypasses OAuth restrictions (ICS is a public URL)
- ✅ Prevents double-booking (real events)
- ✅ Protects sensitive info (titles aren't synced)
Implementation hell
Issue 1: Time format inconsistency
I assumed all timestamps in ICS would be YYYYMMDDTHHMMSS.
Nope. Outlook sometimes outputs YYYYMMDDTHHMM (no seconds).
DTSTART;TZID=Tokyo Standard Time:20260109T143000
Fixed with dual regex patterns:
// Try YYYYMMDDTHHMM first
let m = val.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(Z)?$/);
if (m) { /* parse */ }
// Then try YYYYMMDDTHHMMSS
m = val.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z)?$/);
if (m) { /* parse */ }
Issue 2: Recurring events share UIDs
Weekly meetings were only partially syncing. Turns out, Outlook reuses the same UID for all instances of a recurring event.
Example master event:
BEGIN:VEVENT
UID:040000008200E00074C5B7101A82E008...
DTSTART:20251223T130000
DTEND:20251223T140000
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=TU
END:VEVENT
If one instance is moved to a different time, there's an exception:
BEGIN:VEVENT
UID:040000008200E00074C5B7101A82E008... (same UID!)
RECURRENCE-ID;TZID=Tokyo Standard Time:20260106T130000
DTSTART;TZID=Tokyo Standard Time:20260109T143000
DTEND;TZID=Tokyo Standard Time:20260109T153000
END:VEVENT
Using only UID as the key meant events kept overwriting each other.
Fix: Use UID + start time as the key.
const instanceId = e.recurrenceIdRaw || formatIcsLocalKey_(start);
const key = `${e.uid}|${instanceId}`;
Issue 3: Duplicate events
Some events appeared twice—same time, same title.
Root cause: Outlook outputs both the master event (with RRULE) AND individual instances (with RECURRENCE-ID) for the same occurrence.
Fix: Normalize events. When multiple events have the same start time, prefer the one with RECURRENCE-ID.
function normalizeEvents_(events) {
const map = new Map();
for (const e of events) {
const k = `${e.uid}|${formatIcsLocalKey_(e.dtstart)}`;
if (!map.has(k)) {
map.set(k, e);
continue;
}
const existing = map.get(k);
// Prefer RECURRENCE-ID version
if (!existing.recurrenceIdRaw && e.recurrenceIdRaw) {
map.set(k, e);
}
}
return Array.from(map.values());
}
Cutting scope
At this point, I stepped back.
The goal is avoiding double-booking, not perfect calendar replication.
I don't need:
- Event titles
- Event descriptions
- Attendee lists
I just need: "This time slot is busy."
Final spec:
- Events with the same time range → merge into one block
- Title: always "Block"
- Description: empty
- If the block calendar gets corrupted, just delete and resync
This simplification made everything easier.
The key became simply: start_time + end_time
const eventKey = `${startTime.getTime()}_${endTime.getTime()}`;
Same time = same key = one block. Done.
Final architecture
Source: Office 365 calendar (subscribed in Google Calendar via ICS)
Target: Dedicated "Block" calendar in Google Workspace
Sync frequency: Every 4 hours
Sync method: Differential (not full rebuild)
Why differential? Efficiency. If I have 100 events and do full delete + recreate every 4 hours, that's 1,200 API calls per day. Differential sync only touches events that changed.
The sync stores a mapping (event key → Google event ID) in Script Properties:
const map = JSON.parse(props.getProperty(PROP_MAP) || '{}');
Sync logic:
- Fetch events from source calendar
- Calculate key (start + end time) for each event
- If key exists in map → update event
- If key is new → create event
- Keys not seen this run → delete event
Total code: ~200 lines.
Results
Deployed today. So far, so good.
When I create an event in Google Calendar, it correctly warns me if that time slot has an Outlook event. No more double-booking.
The 4-hour sync delay is fine for my use case. If I need tighter sync, I can adjust the trigger frequency.
What I learned
Outlook ICS quirks
- Time formats inconsistent (seconds optional)
- Recurring events share UIDs
- Exception instances use
RECURRENCE-ID - Master + exception dual representation
Google Calendar ICS subscriptions
- Read-only, don't affect availability
- Real events do affect availability
RFC 5545 (iCalendar spec)
- Line folding exists
-
RECURRENCE-IDis crucial for recurring events
Takeaways
I couldn't use existing tools due to OAuth restrictions.
Building a full sync tool was overkill.
The sweet spot: Do the minimum that solves the problem.
Tradeoffs I accepted:
- No event titles → actually good for security
- 4-hour delay → fine for my workflow
- Multiple events at same time → collapsed into one block
The key decision: "I just need to know when I'm busy."
That clarity made everything else fall into place.
If you're interested in more about decision-making in engineering and balancing technical constraints with practical needs, I write about it here:
Top comments (0)