DEV Community

Cover image for Solved: Syncing Notion Databases with Google Calendar for Project Management
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: Syncing Notion Databases with Google Calendar for Project Management

🚀 Executive Summary

TL;DR: Fragmented project management across Notion, spreadsheets, and Google Calendar leads to inefficiencies and missed deadlines. This guide provides a powerful, cost-effective solution by automating Notion database synchronization with Google Calendar using custom Python scripts and their respective APIs, offering a unified, real-time project timeline.

🎯 Key Takeaways

  • Automated synchronization is achieved via custom Python scripts interacting with Notion and Google Calendar APIs, requiring specific integration tokens and database IDs.
  • Google Calendar API access is secured using a Google Cloud Service Account with a JSON key file and ‘Make changes to events’ permission granted to the target calendar.
  • Idempotency is maintained by storing the Notion page ID within Google Calendar event’s ‘extendedProperties’, preventing duplicate events and enabling updates to existing ones.

Syncing Notion Databases with Google Calendar for Project Management

As Senior DevOps Engineers and Technical Writers at TechResolve, we frequently encounter the challenge of fragmented project management workflows. Teams often juggle tasks in Notion, deadlines in spreadsheets, and meetings in Google Calendar, leading to manual data entry, missed deadlines, and a lack of centralized visibility. The problem intensifies with growing project complexity: valuable time is wasted on redundant data transfers, and critical updates might not propagate across all platforms. While various SaaS solutions promise to bridge these gaps, they often come with a hefty price tag or rigid integration limitations.

This tutorial presents a powerful, cost-effective solution: automating the synchronization of your Notion databases with Google Calendar using their respective APIs. By the end of this guide, you will have a custom Python script that fetches tasks and events from your Notion database and creates or updates corresponding events in your Google Calendar, providing a unified, real-time overview of your project timelines directly within your calendar. Say goodbye to manual updates and hello to streamlined project management!

Prerequisites

Before we dive into the integration, ensure you have the following in place:

  • Notion Account: An active Notion account with administrative access to the database you intend to sync. This database should include properties like “Name” (title), “Due Date” (date), and ideally a “Status” (select or multi-select) property.
  • Notion Integration Token: A Notion integration token (API key) with read/write access to your chosen database.
  • Google Account: A Google account with access to Google Calendar.
  • Google Cloud Project: A Google Cloud Project with the Google Calendar API enabled and a Service Account created. You will need the JSON key file for this Service Account.
  • Python 3.x: Python installed on your local machine or server. We recommend Python 3.8 or newer.
  • pip: Python’s package installer, usually bundled with Python 3.x.
  • Required Python Libraries: You’ll need requests for Notion API interactions, and google-auth-oauthlib, google-api-python-client for Google Calendar API interactions.

Step-by-Step Guide

Step 1: Set Up Notion Integration and Database Access

First, we need to create an internal integration in Notion and grant it access to your project database.

  1. Go to your Notion workspace and navigate to Settings & Members > Integrations.
  2. Click “Develop your own integrations” > “New integration”.
  3. Give your integration a name (e.g., “GCal Sync Integration”) and select your workspace. Click “Submit”.
  4. Copy the “Internal Integration Token”. Keep this token secure.
  5. Now, open the Notion database you wish to sync. Click the “…” (more options) menu at the top right of the database.
  6. Scroll down to “Add connections” and search for your newly created integration by name. Select it to grant access.
  7. To find your Database ID, open your database as a full page. The Database ID is the 32-character string found in the URL between your workspace name and the page name, e.g., https://www.notion.so/{workspace_name}/{database_id}?v=.... Copy this ID.

Step 2: Configure Google Calendar API Access

Next, we’ll set up access to the Google Calendar API through a Service Account. This is the recommended approach for server-to-server interactions.

  1. Go to the Google Cloud Console (https://console.cloud.google.com/).
  2. Create a new project or select an existing one.
  3. Navigate to “APIs & Services” > “Library”. Search for “Google Calendar API” and enable it.
  4. Go to “APIs & Services” > “Credentials”.
  5. Click “Create Credentials” > “Service Account”.
  6. Provide a Service Account name (e.g., “notion-gcal-sync”) and description. Click “Done”.
  7. Back on the “Credentials” page, click on the newly created Service Account. Go to the “Keys” tab > “Add Key” > “Create new key”. Select “JSON” and click “Create”. This will download a JSON key file to your computer. Rename it to something like service_account_key.json and keep it secure.
  8. Copy the Service Account’s email address from the “Details” tab of your Service Account (it typically ends with @project-id.iam.gserviceaccount.com).
  9. Open the Google Calendar you wish to sync with. In the “My Calendars” section on the left, hover over your target calendar, click the three dots, and select “Settings and sharing”.
  10. Under “Share with specific people or groups”, click “Add people”. Paste the Service Account’s email address and set permissions to “Make changes to events”. Click “Send”.
  11. To get your Google Calendar ID, scroll up to “Integrate calendar” on the “Settings and sharing” page. The Calendar ID is usually your primary email address for your main calendar, or a longer string for custom calendars.

Step 3: Develop the Python Sync Script

Now for the core logic. We’ll write a Python script that pulls tasks from Notion and pushes them to Google Calendar. We’ll assume a Notion database with at least a “Name” (Title) property and a “Due Date” (Date) property.

Create a file named notion_gcal_sync.py and add the following code. Remember to replace placeholder values with your actual Notion token, database ID, Google Calendar ID, and the path to your service account key file.

import os
import requests
from google.oauth2 import service_account
from googleapiclient.discovery import build
import datetime

# --- Configuration ---
NOTION_TOKEN = "YOUR_NOTION_INTEGRATION_TOKEN"
NOTION_DATABASE_ID = "YOUR_NOTION_DATABASE_ID"
GOOGLE_CALENDAR_ID = "YOUR_GOOGLE_CALENDAR_ID" # e.g., 'your-email@example.com' or custom calendar ID
SERVICE_ACCOUNT_FILE = "path/to/your/service_account_key.json" # Relative or absolute path

SCOPES = ['https://www.googleapis.com/auth/calendar']
NOTION_API_VERSION = "2022-06-28" # Use the latest API version Notion provides

# Notion API headers
NOTION_HEADERS = {
    "Authorization": f"Bearer {NOTION_TOKEN}",
    "Notion-Version": NOTION_API_VERSION,
    "Content-Type": "application/json"
}

def get_notion_tasks():
    """Fetches tasks from Notion database that have a 'Due Date'."""
    url = f"https://api.notion.com/v1/databases/{NOTION_DATABASE_ID}/query"

    # Filter for pages that have a 'Due Date' property and are not 'Done' (if a 'Status' property exists)
    filter_payload = {
        "filter": {
            "and": [
                {
                    "property": "Due Date",
                    "date": {
                        "is_not_empty": True
                    }
                }
                # Optional: Add a status filter
                # {
                #     "property": "Status",
                #     "select": {
                #         "does_not_equal": "Done"
                #     }
                # }
            ]
        }
    }

    try:
        response = requests.post(url, headers=NOTION_HEADERS, json=filter_payload)
        response.raise_for_status() # Raise an exception for HTTP errors
        data = response.json()

        tasks = []
        for page in data.get('results', []):
            page_id = page['id']
            properties = page['properties']

            # Extract Name (title)
            name_prop = properties.get('Name', {}).get('title', [])
            task_name = name_prop[0]['plain_text'] if name_prop else 'Untitled Task'

            # Extract Due Date
            due_date_prop = properties.get('Due Date', {}).get('date')
            if not due_date_prop:
                continue # Skip tasks without a due date

            start_date_str = due_date_prop.get('start')
            end_date_str = due_date_prop.get('end') or start_date_str # If no end date, use start date

            # Construct Notion page URL
            notion_page_url = page.get('url')

            tasks.append({
                'id': page_id,
                'name': task_name,
                'start_date': start_date_str,
                'end_date': end_date_str,
                'url': notion_page_url
            })
        return tasks
    except requests.exceptions.RequestException as e:
        print(f"Error fetching Notion tasks: {e}")
        return []

def get_google_calendar_service():
    """Authenticates and returns the Google Calendar service object."""
    try:
        credentials = service_account.Credentials.from_service_account_file(
            SERVICE_ACCOUNT_FILE, scopes=SCOPES
        )
        service = build('calendar', 'v3', credentials=credentials)
        return service
    except Exception as e:
        print(f"Error authenticating Google Calendar: {e}")
        return None

def sync_notion_to_gcal():
    """Main function to sync Notion tasks to Google Calendar."""
    notion_tasks = get_notion_tasks()
    if not notion_tasks:
        print("No Notion tasks with due dates found or an error occurred.")
        return

    service = get_google_calendar_service()
    if not service:
        print("Could not initialize Google Calendar service.")
        return

    print(f"Found {len(notion_tasks)} Notion tasks to process.")

    # Get existing events from GCal that were previously synced by this script
    # We use extendedProperties to store the Notion page ID for mapping
    # Fetch events for a reasonable future range, e.g., 6 months
    time_min = datetime.datetime.utcnow().isoformat() + 'Z' # now
    time_max = (datetime.datetime.utcnow() + datetime.timedelta(days=180)).isoformat() + 'Z' # 6 months from now

    existing_events_map = {} # Map Notion ID to GCal event
    try:
        events_result = service.events().list(
            calendarId=GOOGLE_CALENDAR_ID,
            timeMin=time_min,
            timeMax=time_max,
            singleEvents=True,
            orderBy='startTime',
            privateExtendedProperty=[f'notion_page_id'] # Filter for events with our custom property
        ).execute()

        for event in events_result.get('items', []):
            notion_id_in_event = event.get('extendedProperties', {}).get('private', {}).get('notion_page_id')
            if notion_id_in_event:
                existing_events_map[notion_id_in_event] = event
    except Exception as e:
        print(f"Error fetching existing Google Calendar events: {e}")
        # Continue without existing map, might create duplicates if not handled carefully

    for task in notion_tasks:
        notion_id = task['id']
        task_name = task['name']
        start_date_str = task['start_date']
        end_date_str = task['end_date']
        task_url = task['url']

        # Determine if it's an all-day event or specific time
        if 'T' in start_date_str: # Contains time component
            start = {'dateTime': start_date_str, 'timeZone': 'UTC'} # Assuming UTC from Notion
            end = {'dateTime': end_date_str, 'timeZone': 'UTC'}
        else: # All-day event
            start = {'date': start_date_str}
            end = {'date': (datetime.datetime.strptime(end_date_str, '%Y-%m-%d') + datetime.timedelta(days=1)).strftime('%Y-%m-%d')}


        event_body = {
            'summary': f"[Notion] {task_name}",
            'description': f"Task from Notion: {task_url}",
            'start': start,
            'end': end,
            'extendedProperties': {
                'private': {
                    'notion_page_id': notion_id
                }
            }
        }

        existing_event = existing_events_map.get(notion_id)

        if existing_event:
            # Check if event needs update (simple check for title/date change)
            current_summary = existing_event.get('summary')
            current_start_date = existing_event.get('start', {}).get('dateTime') or existing_event.get('start', {}).get('date')
            current_end_date = existing_event.get('end', {}).get('dateTime') or existing_event.get('end', {}).get('date')

            # Notion provides ISO format, GCal might return simplified. Compare effectively.
            # Simplified comparison: if summary or start date has changed
            if current_summary != event_body['summary'] or current_start_date != start_date_str or current_end_date != end_date_str:
                try:
                    updated_event = service.events().update(
                        calendarId=GOOGLE_CALENDAR_ID,
                        eventId=existing_event['id'],
                        body=event_body
                    ).execute()
                    print(f"Updated event for Notion task: {task_name} (ID: {notion_id})")
                except Exception as e:
                    print(f"Error updating event for Notion task {notion_id}: {e}")
            else:
                # print(f"No update needed for Notion task: {task_name}")
                pass
        else:
            # Create new event
            try:
                created_event = service.events().insert(
                    calendarId=GOOGLE_CALENDAR_ID,
                    body=event_body
                ).execute()
                print(f"Created new event for Notion task: {task_name} (ID: {notion_id})")
            except Exception as e:
                print(f"Error creating event for Notion task {notion_id}: {e}")

    # Optional: Delete GCal events that no longer exist in Notion or are marked as 'Done'
    # This would require fetching all relevant GCal events first and comparing their notion_page_id
    # against the current list of active notion_tasks.
    # For simplicity, this part is omitted in the initial script but is a crucial next step for robust sync.

if __name__ == "__main__":
    # Install dependencies:
    # pip install requests google-auth-oauthlib google-api-python-client
    sync_notion_to_gcal()
Enter fullscreen mode Exit fullscreen mode

Code Logic Explanation:

  • Configuration: We define essential variables like API tokens, database IDs, and file paths.
  • get_notion_tasks(): This function uses the Notion API’s /databases/{database_id}/query endpoint to retrieve pages (tasks) from your specified database. It filters for tasks that have a “Due Date” property. It extracts the task name, Notion page ID, start/end dates, and the Notion page URL.
  • get_google_calendar_service(): This function handles authentication with the Google Calendar API using the provided Service Account key. It returns a service object which we use to interact with the calendar.
  • sync_notion_to_gcal(): This is the main orchestration function.
    1. It first calls get_notion_tasks() to get all relevant tasks from Notion.
    2. It then fetches existing events from your Google Calendar that were previously created by this script. It uses extendedProperties to store the Notion page ID, allowing us to map Notion tasks to their corresponding Google Calendar events.
    3. For each Notion task:
      • It constructs a Google Calendar event payload. It intelligently handles all-day events versus timed events based on the Notion date property.
      • It checks if an event for this Notion task already exists in Google Calendar using the notion_page_id stored in extendedProperties.
      • If the event exists, it compares key properties (summary, start/end dates) and updates the Google Calendar event if changes are detected.
      • If the event does not exist, it creates a new event in Google Calendar.
  • Idempotency: By storing the Notion page ID in the Google Calendar event’s extendedProperties, the script can identify if an event has already been created. This prevents duplicate events and enables updates to existing ones.

Step 4: Schedule the Sync Script

For continuous synchronization, you’ll want to run this script periodically. Here are a few common approaches:

Option A: Cron Job (Linux/macOS)

A cron job is a simple and effective way to schedule tasks on Unix-like systems.

  1. Open your crontab for editing:
   crontab -e
Enter fullscreen mode Exit fullscreen mode
  1. Add a line to run your script every 15 minutes (adjust frequency as needed):
   */15 * * * * /usr/bin/python3 /path/to/your/notion_gcal_sync.py >> /var/log/notion_gcal_sync.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Ensure you use the correct Python executable path (e.g., /usr/bin/python3 or /usr/local/bin/python3) and the full path to your script. The >> /var/log/notion_gcal_sync.log 2>&1 part redirects both standard output and error to a log file, which is crucial for debugging.

Option B: Task Scheduler (Windows)

On Windows, you can use the Task Scheduler to run the Python script.

  1. Open Task Scheduler, then “Create Basic Task…”.
  2. Follow the wizard, setting a trigger (e.g., “Daily” or “Weekly” and then adjust frequency) and for the action, choose “Start a program”.
  3. For “Program/script”, enter the path to your Python executable (e.g., C:\Python39\python.exe).
  4. For “Add arguments (optional)”, enter the full path to your script (e.g., C:\Users\YourUser\scripts\notion_gcal_sync.py).
  5. Complete the wizard.

Option C: Docker Container and Orchestrator

For more robust and portable deployment, you can containerize your script and run it via Kubernetes cron jobs, AWS Fargate with scheduled tasks, or a simple Docker cron.

Example Dockerfile:

FROM python:3.9-slim-buster
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "notion_gcal_sync.py"]
Enter fullscreen mode Exit fullscreen mode

Example requirements.txt:

requests
google-auth-oauthlib
google-api-python-client
Enter fullscreen mode Exit fullscreen mode

Then, build and run:

docker build -t notion-gcal-sync .
docker run --name notion-gcal-sync-instance -d notion-gcal-sync
Enter fullscreen mode Exit fullscreen mode

You would typically use a cron inside the container or an external orchestrator to schedule runs.

Common Pitfalls

  • Authentication and Authorization Errors:
    • Notion: Double-check your NOTION_TOKEN. Ensure your integration has been explicitly granted access to the specific database you are querying (this is a common oversight).
    • Google: Verify your SERVICE_ACCOUNT_FILE path is correct and the JSON file is valid. Confirm that the Service Account’s email address has “Make changes to events” access to the target Google Calendar.
  • API Rate Limits:

Both Notion and Google Calendar APIs have rate limits. For typical project management tasks, running the script every 15-30 minutes should be fine. If you have a very large database or are syncing frequently, you might hit limits. Implement exponential backoff in your API calls for retries.

  • Data Mismatches and Property Names:

Ensure that the property names in your Notion database (e.g., “Name”, “Due Date”) exactly match what the script expects. If you rename a property in Notion, you must update the script accordingly. Also, be mindful of date formats and timezones, ensuring consistency between Notion and Google Calendar events. Notion typically provides ISO 8601 formatted dates, which Python and Google Calendar APIs handle well.

Conclusion

You’ve successfully implemented an automated synchronization solution between your Notion database and Google Calendar! By investing a little upfront effort in this setup, you’ve eliminated a significant amount of manual, repetitive work, leading to improved project oversight, fewer missed deadlines, and a more cohesive view of your schedule. This integration transforms your Notion tasks into actionable calendar events, placing all critical information exactly where you need it.

This foundation opens doors for further enhancements. Consider extending the script for:

  • Two-Way Sync: Allowing changes in Google Calendar to reflect back in Notion.
  • Event Deletion: Automatically deleting Google Calendar events when a Notion task is marked “Done” or deleted.
  • More Sophisticated Filtering: Syncing tasks based on specific tags, team members, or project states.
  • Error Handling and Logging: Implementing robust error logging and notification mechanisms (e.g., via Slack or email).
  • Webhooks: Instead of polling, investigate Notion webhooks (when they become more generally available) for real-time updates to trigger your sync script.

At TechResolve, we believe in empowering engineers with the tools and knowledge to build efficient, automated systems. We hope this guide helps you reclaim your time and focus on what truly matters: delivering exceptional projects.


Darian Vance

👉 Read the original article on TechResolve.blog


☕ Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)