# Sync Google Calendar with Microsoft Outlook Using Google Apps Script & Microsoft Graph

**Sync Google Calendar with Microsoft Outlook** in a way that survives production: idempotent upserts (no duplicate events every run), explicit North American time zones, OAuth-backed **Microsoft Graph** calls, and automation via **ScriptApp** triggers. This pillar guide targets US and Canadian enterprise teams evaluating **Google Apps Script Outlook API** automation instead of paid desktop sync utilities.

---

## Executive summary: why “native” sync breaks for power users

Microsoft 365 and Google Workspace each ship **consumer-grade** calendar connections (subscribe via URL, basic Outlook/Google connectors). Those flows work for light personal use. They often fail operational expectations when:

- **Latency** is measured in hours, not minutes.
- **Two-way** semantics create duplicate series, drift on recurring edits, or silent conflicts.
- **Privacy** policies forbid publishing **public iCal URLs** with secret tokens in query strings.
- **Compliance** teams require **audit logs**, deterministic behaviour, and explicit **data residency** awareness.

**Automate Google Calendar to Outlook** with an **API-first** approach: your organization controls credentials, retention, and logging. The script-based pattern in this article is a **one-way Google → Outlook** push engine—the industry-realistic starting point before you invest in full bidirectional reconciliation.

---

## Two-way vs one-way: what is technically realistic?

| Mode | Complexity | Typical outcome |
|------|--------------|-----------------|
| **One-way push (Google → Outlook)** | Moderate | Reliable for reminders and executive assistants mirroring a “source of truth” calendar |
| **Two-way sync** | High | Requires conflict resolution, version vectors, tombstones, and recurring-series algebra |

**Two-way** sync between vendors is hard because neither side exposes a stable, universally comparable “version” of a human meeting. Recurring meetings are the worst case: exceptions, moved instances, renamed series, and attendee-driven updates do not round-trip cleanly.

**Production recommendation:** ship **one-way** first with **delta upserts** (update-in-place when possible). Add reverse direction only after you define conflict policy (last-write-wins is usually wrong for regulated teams).

---

## API vs public iCal: why Apps Script wins

| Approach | Freshness | Privacy | Control |
|----------|-----------|---------|---------|
| Public **iCal** subscription link | Slow refresh cycles; vendor-dependent | Secret URL leakage risk | Low |
| **Microsoft Graph API** + **Google Calendar** reads | Minutes via triggers | Tokens in Script Properties; no public URL | High |

iCal feeds are fine for low-stakes read-only subscriptions. They are a poor fit when you need **near-real-time** updates, **field-level mapping**, **private meetings**, and **identity-scoped** Microsoft accounts.

---

## Azure setup: App Registration (delegated Graph)

> [!IMPORTANT]
> In **Azure Portal**, open **Microsoft Entra ID** → **App registrations** → **New registration**. Choose a **supported account type** that matches your tenant. After creation, record **Application (client) ID** and **Directory (tenant) ID**.

> [!IMPORTANT]
> Under **Certificates & secrets**, create a **Client secret**. Copy the **Value** immediately—Azure will not show it again.

> [!IMPORTANT]
> Under **API permissions** → **Add a permission** → **Microsoft Graph** → **Delegated permissions**, add at minimum:

- `Calendars.ReadWrite`
- `offline_access`
- `openid`
- `profile`

Click **Grant admin consent for {tenant}** when your policy requires it.

> [!IMPORTANT]
> Under **Authentication**, add a **Redirect URI** compatible with your OAuth acquisition flow (web or mobile/desktop, depending on how you obtain the first refresh token). Google Apps Script projects often obtain refresh tokens via a one-time external OAuth exchange; store the resulting **`MS_REFRESH_TOKEN`** in **Script properties**.

This article’s `.gs` file uses the **refresh token** grant against `login.microsoftonline.com/{tenant}/oauth2/v2.0/token`.

---

## Script architecture at a glance

1. **Read** Google Calendar events in a rolling forward window (default 90 days).
2. **Lookup** prior sync mapping (`GoogleEventId` → `OutlookEventId`) in a hidden sheet tab.
3. **Upsert** via Graph:
   - If mapping exists → **PATCH** `/me/events/{id}`
   - Else → **POST** `/me/events`
4. **Stamp** `singleValueExtendedProperties` with a stable Google event id for diagnostics.
5. **Log** errors to `CalendarSyncLog`.

---

## Section 1: Authentication (OAuth2 for Microsoft)

The sample implementation exchanges a **refresh token** for an **access token** using:

- `AZURE_TENANT_ID`
- `AZURE_CLIENT_ID`
- `AZURE_CLIENT_SECRET`
- `MS_REFRESH_TOKEN`

Rotation: when Microsoft returns a new refresh token, the script persists it back to Script properties when present.

---

## Section 2: Mapping Google Calendar fields to Outlook JSON

Timed meetings map to:

```json
"start": { "dateTime": "2026-04-21T14:00:00.000", "timeZone": "America/Chicago" },
"end":   { "dateTime": "2026-04-21T14:30:00.000", "timeZone": "America/Chicago" }
```

All-day events map to Graph **date** boundaries (UTC anchors), preserving exclusive end semantics.

---

## Section 3: The sync loop with error logging

The loop records each failure on the `CalendarSyncLog` sheet with UTC timestamps. Operators can monitor failure bursts that correlate with token expiry, Graph throttling, or permission regressions after tenant policy changes.

---

## Time zones: EST, CST, MST, PST and Canadian zones

Set **File → Project settings → Time zone** in Apps Script to your corporate headquarters zone (for example **America/Toronto** or **America/Chicago**). The script emits **explicit** `timeZone` fields to Graph alongside local `dateTime` strings.

If your tenant rejects IANA names, set Script property **`GRAPH_TIMEZONE_MODE`** to **`windows`** to emit Windows names like **Eastern Standard Time** (see mapping table inside `.gs`).

---

## Automation: triggers every 15 or 30 minutes

Installable triggers:

```javascript
ScriptApp.newTrigger("syncGoogleCalendarToOutlook")
  .timeBased()
  .everyMinutes(30)
  .create();
```

Some Workspace policies restrict minimum intervals—validate against your admin console.

---

## Edge cases (recurring, all-day, private)

- **Recurring meetings:** Instance-level IDs can change when series are edited. Treat sync as “best effort within window”; operational teams often sync **next N days** only.
- **All-day events:** Map `date` fields carefully—Google uses exclusive end dates; Graph expects consistent `start.date` / `end.date`.
- **Private events:** When `CalendarApp` exposes visibility, the sample maps to Graph `sensitivity` where possible.

---

## Comparison: Apps Script vs Power Automate vs paid tools

| Dimension | Apps Script + Graph | Power Automate | Third-party paid sync |
|-----------|---------------------|----------------|------------------------|
| Monthly license tax | Low (Workspace + M365 already paid) | Flow premiums may apply | Often per mailbox |
| Custom mapping | Full JavaScript control | Low-code; complex branching gets messy | Varies |
| Auditability | Your Sheet logs + tenant logs | Cloud flow run history | Vendor-dependent |
| Time to enterprise review | Code review + secret rotation | Connector review | Vendor DPA |

---

## Full Apps Script source

Download:

- `/public/code/sync-google-calendar-to-outlook-graph.gs`

Bind the script to a Google Sheet **or** set **`SYNC_MAP_SPREADSHEET_ID`** so mapping tabs exist.

---

## Enterprise migration CTA

This guide is **free** and intended for engineers. If your organization has **50+ users**, cross-tenant routing, compliance gates, or hybrid Exchange complexity, App Script Expert provides **custom enterprise calendar migration** services: discovery workshops, Graph permission models, runbooks, and production cutover support.

Contact: `https://appscriptexpert.com/contact`

---

## Deep dive: the “two-way dream” and why vendors still sell siloed products

If you are an IT lead, you have already seen the calendar problem in the wild: a sales team on **Microsoft 365** and a product team on **Google Workspace**, with executives trying to “just make the calendars match.” The market’s honest answer is that **universal two-way sync** is a *distributed systems* problem masquerading as a *product feature*.

When a user drags a single instance of a recurring meeting, the system must answer hard questions. Which object is canonical? If both systems change, do you merge attendees, or do you fail closed? How do you map time zones for people who work in **Eastern** and **Pacific** on the same series? The **Microsoft Graph API** and the **Google Calendar** model do not share the same internal representation of exceptions, so a naïve “copy object A to B” either duplicates, drops exceptions, or overwrites in ways that look like data loss to a user.

This is why a **one-way** “source of truth” design is the most defensible first architecture. Operations teams explicitly designate Google as canonical (common for startups and product-led orgs) or Outlook as canonical (common in regulated finance and legacy enterprise). Everything else follows: one direction of truth, deterministic upserts, and predictable incident response.

---

## Detailed Azure Portal walkthrough (delegated Graph for one user mailbox)

Follow these UI steps carefully—your terminology must match Azure’s current navigation.

1. Sign in to **Azure Portal** as someone who can register applications (often Cloud Application Administrator or Global Administrator for consent).
2. Search for **Microsoft Entra ID** (formerly Azure Active Directory).
3. Under **Manage**, choose **App registrations**.
4. Click **New registration**.
5. **Name** the application something operators will recognize in audit logs—for example `Google-to-Outlook Calendar Push (Apps Script)`.
6. Choose **Accounts in this organizational directory only** unless you have a multi-tenant requirement (most enterprises do not for this workload).
7. Click **Register**.

After creation:

1. Copy **Application (client) ID** into your password manager—this maps to **`AZURE_CLIENT_ID`**.
2. Copy **Directory (tenant) ID**—this maps to **`AZURE_TENANT_ID`**.
3. Navigate **Certificates & secrets**.
4. Click **New client secret**, choose an expiry aligned with your security policy (many teams pick 12–24 months with a renewal calendar reminder).
5. Copy **Value** immediately—this maps to **`AZURE_CLIENT_SECRET`** (never reuse the Secret ID).

Then configure permissions:

1. Go to **API permissions** → **Add a permission**.
2. Choose **Microsoft Graph** → **Delegated permissions**.
3. Add **`Calendars.ReadWrite`**, **`offline_access`**, **`openid`**, **`profile`**.
4. Click **Grant admin consent** when required by policy.

Authentication configuration depends on how you acquire the initial refresh token in your organization. Enterprises frequently use:

- A securely hosted OAuth authorization page operated by IT, **or**
- An IT-approved scripted exchange using device code flows, **or**
- Azure **Conditional Access** policies requiring compliant devices—your OAuth client must satisfy those constraints.

Regardless of acquisition mechanics, your Apps Script runtime ultimately needs **`MS_REFRESH_TOKEN`** stored as a **Script Property**, not pasted into source code.

> [!IMPORTANT]
> Treat client secrets like production credentials: rotate predictable calendar events, restrict who can edit Script Properties, and revoke secrets immediately when staff with access depart.

---

## OAuth token mechanics Apps Script engineers must understand

The `.gs` implementation focuses on **`refresh_token` → `access_token`** exchange because Apps Script cron triggers run **headlessly**. Interactive browser consent cannot occur every fifteen minutes.

Implications:

- Your first-time consent flow must capture **`offline_access`** so refresh tokens remain valid according to tenant policy.
- Conditional Access might limit refresh longevity—monitor Graph **401 Unauthorized** spikes in logs.
- Some tenants issue **rolling refresh tokens**—the sample persists new refresh tokens when Graph returns them.

Security teams commonly ask whether storing refresh tokens in Script Properties is acceptable. Compared to publishing an iCal URL publicly, delegated OAuth secrets are broadly considered **better**—but they still require RBAC around who can edit script settings and who can view logs.

---

## Delta sync design: why delete/recreate is wrong

Naïve integrations often **delete** events and **recreate** them to “refresh” state. That approach produces:

- New **Outlook global object IDs**, breaking attendee tracking and conference bridges.
- Notification spam for attendees if Outlook interprets changes as cancellations/rebooks.
- Higher Graph API volume and throttling risk.

The sample engine uses **stable correlation**:

1. Google event **ID** is the primary natural key for one-way push.
2. A hidden mapping sheet stores **`GoogleEventId` → `OutlookEventId`**.
3. Graph calls **PATCH** when mapping exists; **POST** only when unknown.

Additionally, the payload includes a **singleValueExtendedProperty** carrying the Google event id. That property supports diagnostics (find the Outlook item when the mapping sheet is missing a row) and future enhancements (Graph `$filter` queries on extended properties when your tenant supports them reliably).

If PATCH fails because the Outlook item was deleted manually, the script falls back to **POST** and rewrites mapping—this is intentional recovery behaviour.

---

## Field mapping reference (Google → Graph)

| Google concept | Graph property | Notes |
|----------------|----------------|-------|
| Title | `subject` | Keep concise for mobile Outlook clients |
| Description | `body` HTML | Escape HTML to avoid injection surprises |
| Start/end (timed) | `start` / `end` `dateTime` + `timeZone` | Always pair explicit zone with local `dateTime` |
| All-day span | `start.date` / `end.date` | Exclusive end semantics—match Google |
| Privacy | `sensitivity` | Best-effort based on CalendarApp visibility |

---

## Time zones: North American coverage checklist

When users say **EST**, engineers implement **America/New_York** (or Windows **Eastern Standard Time**). Similar mappings apply:

| Colloquial | IANA (examples) | Windows (examples) |
|------------|-----------------|---------------------|
| Eastern | `America/New_York`, `America/Toronto` | `Eastern Standard Time` |
| Central | `America/Chicago`, `America/Winnipeg` | `Central Standard Time` |
| Mountain | `America/Denver`, `America/Edmonton` | `Mountain Standard Time` |
| Pacific | `America/Los_Angeles`, `America/Vancouver` | `Pacific Standard Time` |

Set Apps Script project time zone intentionally. If your script runs in UTC while your business operates in Central time, you will ship subtle off-by-hours bugs unless your mapping layer normalizes consistently.

---

## Automation: triggers, quotas, and operational realities

Apps Script provides **installable triggers** suitable for fifteen- or thirty-minute cadence (subject to Workspace policies). Pair triggers with:

- Bounded sync windows (`SYNC_DAYS_FORWARD`) to reduce runtime.
- Logging that captures token failures distinctly from Graph **429** throttling.
- Alerting—many teams mirror `CalendarSyncLog` errors into Slack via a second tiny webhook script.

Also note the **six-minute execution limit** for triggers in many contexts: large calendars require smaller windows or chunked processing strategies.

---

## Operational playbook for enterprise reviewers

Provide security stakeholders:

- Which identity owns the OAuth tokens (service mailbox vs delegated user).
- Which Graph scopes are required and why read/write is scoped to calendar only.
- Where secrets live (Script Properties vs Azure Key Vault patterns for advanced orgs).
- Logging retention for incident response.

---

## Script limitations you should disclose internally

- **Bidirectional parity** is not attempted here.
- **Conference links** (Google Meet vs Teams) do not magically unify—those fields differ by platform.
- **Shared calendars** may require different Graph endpoints (`/users/{id}/events`)—the sample targets `/me/events`.

---

## FAQ

**Does this replace BitTitan/Sync2 for every scenario?**
Paid migration suites excel at bulk historical migration and tenant-to-tenant moves. This article targets **ongoing synchronization** controlled by your team.

**Will Microsoft throttle Graph calls?**
Yes—enterprise tenants often enforce rate limits. Mitigate with conservative cadence and bounded windows.

**Can we run this headlessly for a shared mailbox?**
Often yes with the correct Graph identity and permissions, but it is a separate architecture review.

---

## Closing: why this earns authoritative backlinks

Engineering forums reward posts that acknowledge **recurring meeting reality**, show **mapping tables**, teach **Azure consent**, and ship **copy-paste-run** automation with **logging**. This guide’s companion `.gs` file is intentionally conservative: upsert-first, explicit zones, refresh-token OAuth, and sheet-backed idempotency—the minimum bar for serious **Google Apps Script Outlook API** work.

---

## Power Automate vs Apps Script: when each tool wins

**Power Automate** is excellent when your integration stays inside Microsoft’s connector ecosystem and your logic is primarily declarative (“when an event is created, copy fields”). It becomes expensive to reason about once you introduce complex branching, custom payload shaping, recursive exception handling logic, or cross-cloud identity gymnastics that are naturally expressed in code.

**Apps Script** excels when:

- Google Calendar is canonical and your team lives in Sheets/Drive culture.
- You must store **deterministic mappings** (hidden tabs) alongside operator-friendly logs.
- You want everything versioned like application code—functions, triggers, peer review—even if UX is engineer-first.

Neither tool removes the requirement for OAuth governance. The strategic difference is maintainability inside your engineering culture.

---

## Graph throttling and transient errors you will see

Microsoft Graph responds with HTTP **429** when you exceed usage limits. Enterprise tenants may also return **503** during platform incidents. Production scripts should:

- Back off exponentially on 429 responses (the sample focuses on clarity; add backoff if you expand batch sizes).
- Keep batch windows modest—calendar sync rarely needs thousands of writes per minute.
- Log response bodies on failure—Graph error payloads often include `innerError` codes that pinpoint permission versus payload problems.

---

## Step-by-step: deploy the Apps Script project like a software release

1. Create or choose a Google Sheet used only for automation metadata (recommended), or reuse an internal ops workbook.
2. Click **Extensions → Apps Script**.
3. Paste `sync-google-calendar-to-outlook-graph.gs` into `Code.gs` (or a module file).
4. Open **Project Settings** → **Script properties** and enter Azure keys and refresh token values.
5. Set **Project settings → Time zone** to your headquarters region (this drives correct local `dateTime` strings).
6. Run **`syncGoogleCalendarToOutlook`** once manually to validate Graph writes.
7. Install **`installSyncTriggerEvery30Minutes`** (or the 15-minute variant) once triggers are approved by policy.
8. Verify hidden tabs **`CalendarSyncMap`** and **`CalendarSyncLog`** appear and stay updated.

Document rollback: disable triggers first, then revoke Azure client secret rotation if credentials leak.

---

## Final positioning for procurement conversations

When finance asks why not buy a boxed sync product, translate engineering facts into money:

- **Vendor subscription** scales with seats and features your IT team may not need.
- **Graph + Apps Script** scales primarily with **cloud consumption you already pay for** and **engineering time you control**.
- **Vendor lock-in** risk drops when your integration is code you can audit.

That framing is how technical founders and IT directors align on **Automate Google Calendar to Outlook** initiatives without boiling the ocean on day one.

---

## Appendix: recurring meetings—what engineers wish product managers knew

Calendar products expose **series masters**, **exceptions**, **cancelled instances**, and **detached instances**. When you synchronize only a finite forward window, you are inherently synchronizing **instances**, not “the recurrence object” as humans imagine it. That is acceptable for operations like “show my next thirty days in Outlook,” but unacceptable if your compliance requirement is “every historical edit must match forever.”

If your programme requires historical fidelity, you need a migration project with explicit acceptance criteria: what happens when a single instance moves rooms, when an attendee declines, or when an organizer changes the Teams link. Those scenarios are why **Microsoft Graph API Google Apps Script tutorial** content must never promise magical parity—and why this pillar articulates upsert semantics and realistic limitations instead.

When you communicate scope to stakeholders, use test cases they recognize: **DST transitions** in March and November, **floating holidays**, **cross-country flights** that shift local times, and **all-day** events that must not notify attendees at midnight. If your automation behaves correctly across those cases in pilot, you have earned the right to expand traffic and shorten trigger intervals.



