🚀 Executive Summary

TL;DR: Darian solved the problem of manually syncing Google and Outlook calendar events by developing a Python script. This script leverages Google Calendar and Microsoft Graph APIs for two-way synchronization, preventing duplicates through a unique event tagging mechanism.

🎯 Key Takeaways

  • Implementing two-way calendar sync requires obtaining API credentials from Google Cloud Console (OAuth client ID) and Azure AD (Application ID, Tenant ID, Client Secret) for secure access.
  • A Python script leverages `google-api-python-client` and `msal` to authenticate and interact with Google Calendar and Microsoft Graph APIs.
  • To prevent infinite sync loops, the script injects unique `[Synced from X: event_id]` tags into event descriptions, allowing it to identify and skip self-created events.

Syncing Calendar Events between Google and Outlook (Two-way)

Syncing Calendar Events between Google and Outlook (Two-way)

Hey team, Darian here. A few months back, I was constantly juggling my internal TechResolve meetings on Outlook and our client-facing project schedules on Google Calendar. I’d double-book myself or miss important syncs because I forgot to manually copy an event over. It was a classic context-switching problem and a major productivity drain.

I realized I was wasting valuable time on a task that was perfect for automation. So, I built a simple Python script to handle it. This guide walks you through that exact setup. It’s a set-and-forget solution that has genuinely saved me a couple of hours every month. Let’s get your calendars harmonized.

Prerequisites

Before we start, make sure you have the following ready:

  • Python 3.8 or newer.
  • A Google Account with the ability to create API projects.
  • A Microsoft 365 account with admin rights to register an application in Azure AD.
  • Access to a terminal to install a few Python packages. I’ll cover which ones.

The Step-by-Step Guide

Step 1: API Credentials – The Foundation

This is the most tedious part, but you only do it once. We need to tell Google and Microsoft that our script is authorized to access calendar data.

  1. Google Calendar API:
    • Go to the Google Cloud Console, create a new project.
    • In that project, search for and enable the “Google Calendar API”.
    • Navigate to “Credentials”, click “Create Credentials”, and choose “OAuth client ID”.
    • Select “Desktop app” as the application type.
    • Download the resulting JSON file. Rename this file to google_credentials.json and place it in your project folder. This file is highly sensitive; never commit it to version control.
  2. Microsoft Graph API (for Outlook):
    • Log in to the Azure Active Directory portal.
    • Go to “App registrations” and click “New registration”.
    • Give it a name (e.g., “CalendarSyncBot”) and leave the rest as default.
    • Once created, note down the “Application (client) ID” and “Directory (tenant) ID”.
    • Go to “API permissions”, click “Add a permission”, select “Microsoft Graph”, then “Delegated permissions”.
    • Search for and add Calendars.ReadWrite.
    • Finally, go to “Certificates & secrets”, create a “New client secret”, and copy the value immediately. You won’t see it again.

Step 2: Project Setup and Dependencies

Now, let’s get our local environment ready. I usually structure my automation scripts with a main file and a config file to keep secrets out of the code. I’ll skip the standard virtualenv setup since you likely have your own workflow for that. Just make sure you’re working in an isolated environment.

You’ll need to install the following Python libraries. You can do this by running a pip install command for each in your terminal, for example: pip install google-api-python-client.

  • google-api-python-client
  • google-auth-oauthlib
  • msal (Microsoft Authentication Library)
  • requests
  • python-dotenv (to handle our configuration)

Create a file named config.env to store our secrets. This file should be added to your .gitignore.

# config.env
MS_TENANT_ID=your_tenant_id_here
MS_CLIENT_ID=your_client_id_here
MS_CLIENT_SECRET=your_client_secret_here

# The user whose calendar we are syncing
# For delegated permissions, this will be your email
MS_USERNAME=your_email@yourdomain.com

# Scopes needed for Microsoft Graph
MS_SCOPES=["https://graph.microsoft.com/.default"]

Step 3: The Authentication Logic

Let’s write the Python code to connect to both services. The first time you run the Google authentication part, it will open a browser window asking you to log in and grant permission. It will then save a token.json file for future runs.

Here’s a basic script structure. Let’s call it sync_calendars.py.

import os
import datetime
from dotenv import load_dotenv
import msal
import requests
import json
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build

# Load environment variables from config.env
load_dotenv('config.env')

# --- Google Authentication ---
def get_google_service():
    SCOPES = ['https://www.googleapis.com/auth/calendar']
    creds = None
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    
    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'google_credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)
        
        with open('token.json', 'w') as token:
            token.write(creds.to_json())
            
    return build('calendar', 'v3', credentials=creds)

# --- Outlook (Microsoft Graph) Authentication ---
def get_ms_graph_session():
    TENANT_ID = os.getenv('MS_TENANT_ID')
    CLIENT_ID = os.getenv('MS_CLIENT_ID')
    CLIENT_SECRET = os.getenv('MS_CLIENT_SECRET')
    SCOPES = json.loads(os.getenv('MS_SCOPES_JSON', '["https://graph.microsoft.com/.default"]'))

    authority = f"https://login.microsoftonline.com/{TENANT_ID}"
    app = msal.ConfidentialClientApplication(
        CLIENT_ID, authority=authority, client_credential=CLIENT_SECRET)

    result = app.acquire_token_silent(SCOPES, account=None)

    if not result:
        print("No suitable token exists in cache. Acquiring a new one.")
        result = app.acquire_token_for_client(scopes=SCOPES)
    
    if "access_token" in result:
        session = requests.Session()
        session.headers.update({'Authorization': f'Bearer {result["access_token"]}'})
        return session
    else:
        print(result.get("error"))
        print(result.get("error_description"))
        return None

# --- Main logic will go here ---
if __name__ == '__main__':
    google_service = get_google_service()
    ms_session = get_ms_graph_session()

    if google_service and ms_session:
        print("Successfully authenticated with both Google and Microsoft.")
        # We will add the sync logic in the next step
    else:
        print("Authentication failed for one or more services.")

Pro Tip: In my production setups, I use a more robust secrets management system like HashiCorp Vault or AWS Secrets Manager instead of a config.env file. For a personal script, this method is perfectly fine, just don’t commit the file.

Step 4: The Two-Way Sync Logic

This is where the magic happens. The core challenge is preventing infinite loops (Google event creates Outlook event, which the script sees and creates back in Google, and so on). My solution is to inject a unique identifier into the body of any event this script creates.

The logic flow is:

  1. Fetch all events from Google for the next 7 days.
  2. Fetch all events from Outlook for the next 7 days.
  3. For each Google event: check if a corresponding event with its ID exists in Outlook. If not, create it in Outlook and add a tag like [Synced from Google: event_id] to the body.
  4. For each Outlook event: check if it has our “Synced from” tag. If it doesn’t, check if a corresponding event exists in Google. If not, create it in Google with a tag like [Synced from Outlook: event_id].

This “tagging” is our simple way to track state and prevent duplicate processing.

# Add these functions to your sync_calendars.py script

SYNC_TAG_G = "[Synced from Outlook:"
SYNC_TAG_O = "[Synced from Google:"

def sync_google_to_outlook(g_service, ms_session):
    # Fetch upcoming events from Google
    now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
    events_result = g_service.events().list(calendarId='primary', timeMin=now,
                                            maxResults=50, singleEvents=True,
                                            orderBy='startTime').execute()
    g_events = events_result.get('items', [])

    # Fetch corresponding Outlook events to check for duplicates
    ms_graph_url = f"https://graph.microsoft.com/v1.0/me/calendar/events?$select=subject,body"
    o_events_response = ms_session.get(ms_graph_url)
    o_events = o_events_response.json().get('value', [])
    o_event_bodies = [e.get('body', {}).get('content', '') for e in o_events]

    for g_event in g_events:
        g_id = g_event['id']
        # Check if this event was created by our Outlook sync
        if SYNC_TAG_G in g_event.get('description', ''):
            continue
        
        # Check if an event with this Google ID already exists in Outlook
        if not any(f"{SYNC_TAG_O} {g_id}]" in body for body in o_event_bodies):
            print(f"Found new Google event to sync: {g_event['summary']}")
            new_outlook_event = {
                "subject": g_event['summary'],
                "body": {
                    "contentType": "HTML",
                    "content": g_event.get('description', '') + f"<p>{SYNC_TAG_O} {g_id}]</p>"
                },
                "start": {"dateTime": g_event['start'].get('dateTime'), "timeZone": "UTC"},
                "end": {"dateTime": g_event['end'].get('dateTime'), "timeZone": "UTC"}
            }
            ms_session.post("https://graph.microsoft.com/v1.0/me/events", json=new_outlook_event)

# You would then create a similar function: sync_outlook_to_google()
# It would do the reverse: fetch Outlook events, check for the SYNC_TAG_O, 
# and if not present, create them in Google with SYNC_TAG_G.

# Update the main block:
if __name__ == '__main__':
    google_service = get_google_service()
    ms_session = get_ms_graph_session()

    if google_service and ms_session:
        print("Running sync: Google to Outlook...")
        sync_google_to_outlook(google_service, ms_session)
        # print("Running sync: Outlook to Google...")
        # sync_outlook_to_google(google_service, ms_session) # Implement this as an exercise!
        print("Sync complete.")
    else:
        print("Authentication failed.")

Pro Tip: A more advanced method is to use “extended properties” on both Google and Outlook events. This lets you store metadata (like the source event ID) in a hidden field instead of cluttering the event description. It’s cleaner and more robust for production use.

Step 5: Automation

The final step is to run this script automatically. I use a simple cron job on a Linux server for this. You could also use a Windows Task Scheduler or a serverless function (like AWS Lambda or Azure Functions) for a more cloud-native approach.

To run this every hour, your cron job entry would look something like this. Remember, do not use absolute paths that could be flagged.

0 * * * * python3 sync_calendars.py

Make sure to run the command from within your project directory where the script and credentials files are located.

Common Pitfalls (Where I Usually Mess Up)

  • Timezones: This is the big one. An event at 2 PM PST is not the same as 2 PM EST. My code snippet normalizes to UTC, which is a best practice. Always convert event times to UTC before sending them to the other service to avoid off-by-a-few-hours errors.
  • API Rate Limiting: If you have a very busy calendar and run the script too frequently, you might hit API rate limits. The fix is to implement exponential backoff logic in your API requests. For personal use, running it once an hour is usually fine.
  • Forgetting to Handle All-Day Events: All-day events have a ‘date’ field instead of a ‘dateTime’ field in the API response. My simple script doesn’t handle this, but for a complete solution, you’d need to add logic to check for and correctly format all-day events.

Conclusion

And that’s the core of it. This script provides a solid foundation for a two-way calendar sync. The initial API setup is a bit of a hurdle, but the payoff in terms of saved time and reduced mental overhead is massive. You can expand on this by handling event updates, deletions, and recurring events for a truly comprehensive solution. Automating these small, repetitive tasks is a cornerstone of effective DevOps. Now, go enjoy your unified calendar!

– Darian Vance

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ How can I automate two-way calendar synchronization between Google and Outlook?

Automate two-way sync using a Python script that integrates with Google Calendar API and Microsoft Graph API. The script fetches events, checks for existing counterparts using unique tags in event descriptions, and creates missing events in the respective calendar.

âť“ How does this custom script compare to other calendar synchronization methods?

This custom Python script offers a highly customizable, open-source alternative to manual syncing or commercial tools. It provides granular control over the sync logic and can be deployed on personal servers via cron jobs or in cloud environments using serverless functions like AWS Lambda or Azure Functions.

âť“ What is a common implementation pitfall for two-way calendar sync, and how is it addressed?

A common pitfall is handling timezones, which can lead to ‘off-by-a-few-hours’ errors. This is addressed by normalizing all event times to UTC before sending them to either Google Calendar or Microsoft Outlook APIs, ensuring consistent scheduling.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading