đ Executive Summary
TL;DR: This guide provides a solution to automatically synchronize PagerDuty on-call schedules with Google Calendar, eliminating manual checks and scheduling conflicts. It leverages PagerDuty and Google Calendar APIs via a Python script to ensure on-call shifts are always visible in your preferred calendar view.
đŻ Key Takeaways
- Synchronization requires a PagerDuty Read-only API Token and a Google Cloud Platform Service Account with Google Calendar API enabled and âMake changes to eventsâ permission on the target calendar.
- A Python script fetches PagerDuty on-call shifts, compares them with existing Google Calendar events, and performs create, update, or delete operations to maintain synchronization.
- Environment variables are crucial for securely managing sensitive credentials like PagerDuty API tokens, Google Calendar ID, and the path to the Google Service Account JSON key file.
- The script uses a rolling window (e.g., 14 days) to fetch on-call data and filters existing Google Calendar events by a specific summary pattern (âPagerDuty On-Callâ) for efficient management.
- Cron jobs are recommended for scheduling the Python script to run periodically (e.g., every 4 hours) to keep the Google Calendar consistently updated with the latest PagerDuty schedule.
Syncing PagerDuty On-Call Schedules to Google Calendar
Introduction
In the fast-paced world of DevOps and system administration, managing on-call responsibilities is critical. PagerDuty excels at incident management and on-call scheduling, providing clarity on who is responsible when. However, relying solely on PagerDuty for schedule visibility can sometimes lead to missed shifts or scheduling conflicts, especially when your personal and professional commitments are scattered across various calendars.
Imagine a world where your PagerDuty on-call shifts automatically appear in your Google Calendar, alongside your meetings, appointments, and personal events. No more manually checking PagerDuty, no more accidental overlaps. This tutorial will guide you through setting up an automated synchronization process, leveraging the PagerDuty and Google Calendar APIs, to bring your on-call schedule directly into your preferred calendar view. By the end of this guide, youâll have a robust, self-updating system that ensures youâre always aware of your on-call duties, reducing stress and improving operational efficiency.
Prerequisites
Before we dive into the setup, ensure you have the following:
- A PagerDuty Account: With sufficient permissions to generate an API Read-only Access Key.
- A Google Cloud Platform (GCP) Project: Youâll need this to enable the Google Calendar API and create a Service Account.
- A Google Calendar: The specific calendar where you want your PagerDuty shifts to appear. You might create a new one dedicated to on-call schedules.
- Python 3: Installed on your synchronization host (e.g., your workstation, a dedicated server, or a CI/CD runner).
-
pip: Pythonâs package installer, usually bundled with Python 3. - Basic understanding of APIs: Familiarity with API concepts, JSON, and environment variables will be beneficial.
Step-by-Step Guide
Step 1: Obtain Your PagerDuty Read-Only API Token
First, we need to generate an API token from PagerDuty that our script will use to fetch your on-call schedule. For security, we recommend a Read-only token.
- Log in to your PagerDuty account.
- Navigate to Integrations > API Access Keys.
- Click + Create New API Key.
- Provide a descriptive name for the key (e.g., âGoogle Calendar Syncâ).
- Ensure the Read-only checkbox is selected.
- Click Create Key.
- Copy the generated API Key immediately. This key will only be shown once.
You will need this key later, so keep it secure. Weâll refer to it as PAGERDUTY_API_TOKEN.
Step 2: Configure Google Cloud Project and Calendar API
Next, weâll set up a Google Cloud Project, enable the Calendar API, and create a Service Account that can interact with your Google Calendar.
-
Create a Google Cloud Project:
- Go to the Google Cloud Console.
- In the project selector at the top, click Select a project > New Project.
- Give your project a name (e.g., âPagerDuty Calendar Syncâ) and click Create.
-
Enable Google Calendar API:
- With your new project selected, navigate to APIs & Services > Library.
- Search for âGoogle Calendar APIâ and click on it.
- Click Enable.
-
Create a Service Account:
- Go to APIs & Services > Credentials.
- Click Create Credentials > Service Account.
- Provide a Service account name (e.g., âpagerduty-sync-saâ) and an optional description. Click Done.
- Click on the newly created Service Account email address.
- Go to the Keys tab, click Add Key > Create new key.
- Select JSON as the key type and click Create.
- Your browser will download a JSON file. Rename it to something like
gcal_service_account.jsonand save it in a secure location on your synchronization host (e.g., in/home/user/.config/pagerduty_sync/). This file contains the credentials for your service account. Weâll refer to its path asGOOGLE_APPLICATION_CREDENTIALS.
-
Share your Google Calendar with the Service Account:
- Open your Google Calendar (calendar.google.com).
- On the left sidebar, hover over the calendar you want to sync to, click the three dots, and select Settings and sharing.
- Scroll down to Share with specific people.
- Click Add people.
- Paste the Service Account email address (from step 3, usually found in the downloaded JSON file under
client_email) into the âAdd peopleâ field. - Set Permissions to Make changes to events or Make changes and manage sharing.
- Click Send.
Note your Google Calendar ID. You can find this in the calendarâs settings under âIntegrate calendarâ (itâs often your email address for your primary calendar, or a long string for secondary calendars).
Step 3: Develop the Python Synchronization Script
Now, letâs write the Python script that orchestrates the data flow between PagerDuty and Google Calendar. Weâll fetch on-call entries and create/update/delete corresponding events in your Google Calendar.
First, create a directory for your script and install the necessary Python packages:
mkdir /home/user/pagerduty_sync
cd /home/user/pagerduty_sync
python3 -m venv venv
source venv/bin/activate
pip install requests google-api-python-client google-auth-httplib2 google-auth-oauthlib
Next, create a file named pagerduty_sync.py:
import os
import requests
from datetime import datetime, timedelta, timezone
import json
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.http import Http
# --- Configuration ---
# PagerDuty API Token (Read-only)
PAGERDUTY_API_TOKEN = os.environ.get('PAGERDUTY_API_TOKEN')
# PagerDuty Schedule ID (You can find this in the URL when viewing a schedule in PagerDuty)
PAGERDUTY_SCHEDULE_ID = os.environ.get('PAGERDUTY_SCHEDULE_ID')
# Google Calendar ID (e.g., your_email@example.com or a long string for secondary calendars)
GOOGLE_CALENDAR_ID = os.environ.get('GOOGLE_CALENDAR_ID')
# Path to your Google Service Account JSON key file
GOOGLE_CREDENTIALS_PATH = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS', '/home/user/.config/pagerduty_sync/gcal_service_account.json')
# --- Constants ---
PAGERDUTY_API_URL = "https://api.pagerduty.com"
GCAL_SCOPES = ['https://www.googleapis.com/auth/calendar']
CALENDAR_SERVICE_NAME = 'calendar'
CALENDAR_SERVICE_VERSION = 'v3'
def get_pagerduty_oncalls(schedule_id, days_forward=14):
"""Fetches upcoming on-call shifts from PagerDuty."""
headers = {
"Accept": "application/vnd.pagerduty+json;version=2",
"Authorization": f"Token token={PAGERDUTY_API_TOKEN}"
}
# Fetch for a date range
now = datetime.now(timezone.utc)
since = now - timedelta(days=1) # Include ongoing shifts
until = now + timedelta(days=days_forward)
params = {
"since": since.isoformat(),
"until": until.isoformat(),
"schedule_ids[]": schedule_id,
"time_zone": "UTC" # Ensure consistent timezones
}
try:
response = requests.get(
f"{PAGERDUTY_API_URL}/oncalls",
headers=headers,
params=params
)
response.raise_for_status() # Raise an exception for HTTP errors
return response.json().get('oncalls', [])
except requests.exceptions.RequestException as e:
print(f"Error fetching PagerDuty on-calls: {e}")
return []
def initialize_google_calendar_service():
"""Initializes and returns a Google Calendar API service."""
try:
credentials = service_account.Credentials.from_service_account_file(
GOOGLE_CREDENTIALS_PATH, scopes=GCAL_SCOPES)
service = build(CALENDAR_SERVICE_NAME, CALENDAR_SERVICE_VERSION, credentials=credentials, http=Http())
return service
except Exception as e:
print(f"Error initializing Google Calendar service: {e}")
return None
def get_existing_calendar_events(service, calendar_id, days_forward=14):
"""Fetches existing events from the Google Calendar."""
now = datetime.now(timezone.utc)
time_min = (now - timedelta(days=1)).isoformat() + 'Z' # Events from yesterday onwards
time_max = (now + timedelta(days=days_forward)).isoformat() + 'Z' # Events for next N days
events_result = service.events().list(
calendarId=calendar_id,
timeMin=time_min,
timeMax=time_max,
singleEvents=True,
orderBy='startTime',
q="PagerDuty On-Call" # Filter for events likely created by us
).execute()
return events_result.get('items', [])
def sync_events():
"""Main synchronization logic."""
if not PAGERDUTY_API_TOKEN or not PAGERDUTY_SCHEDULE_ID or not GOOGLE_CALENDAR_ID:
print("Error: PAGERDUTY_API_TOKEN, PAGERDUTY_SCHEDULE_ID, or GOOGLE_CALENDAR_ID not set.")
print("Please set these as environment variables.")
return
pd_oncalls = get_pagerduty_oncalls(PAGERDUTY_SCHEDULE_ID)
if not pd_oncalls:
print("No PagerDuty on-calls found or error occurred.")
return
gcal_service = initialize_google_calendar_service()
if not gcal_service:
return
existing_events = get_existing_calendar_events(gcal_service, GOOGLE_CALENDAR_ID)
# Map existing events for easy lookup
existing_event_map = {}
for event in existing_events:
# We use a unique identifier (e.g., a hash of start/end/summary) or description field
# For simplicity, let's assume events with "PagerDuty On-Call: [Schedule Name]" in summary
# and matching start/end times are candidates for updates.
# A more robust solution might use extended properties.
# For this tutorial, we'll use a basic summary/time match.
event_summary = event.get('summary', '')
if 'PagerDuty On-Call:' in event_summary:
key = (event.get('start', {}).get('dateTime'), event.get('end', {}).get('dateTime'), event_summary)
existing_event_map[key] = event
processed_pd_events = set() # To track which PD events have been handled
for oncall in pd_oncalls:
schedule_name = oncall['schedule']['summary']
start_time_pd = datetime.fromisoformat(oncall['start'])
end_time_pd = datetime.fromisoformat(oncall['end'])
# Adjusting end_time for Google Calendar (exclusive end time)
# If PD event is 9:00 AM to 5:00 PM, GCAL end should be 5:00 PM (exclusive)
# No adjustment needed if using exact datetimes.
summary = f"PagerDuty On-Call: {schedule_name}"
description = (f"On-call for {schedule_name}.\n"
f"User: {oncall['user']['summary']} ({oncall['user']['html_url']})\n"
f"Schedule: {oncall['schedule']['html_url']}")
event_body = {
'summary': summary,
'description': description,
'start': {'dateTime': start_time_pd.isoformat(), 'timeZone': 'UTC'},
'end': {'dateTime': end_time_pd.isoformat(), 'timeZone': 'UTC'},
'reminders': {'useDefault': True},
# Consider adding an extended property to store the PagerDuty event ID for robust lookup
# 'extendedProperties': {'private': {'pagerDutyOncallId': oncall['id']}}
}
# Create a key for lookup in existing events
pd_event_key = (event_body['start']['dateTime'], event_body['end']['dateTime'], event_body['summary'])
processed_pd_events.add(pd_event_key)
if pd_event_key in existing_event_map:
# Event exists, check if it needs update (e.g., description might change)
existing_gcal_event = existing_event_map[pd_event_key]
# Simple check: if descriptions differ, update
if existing_gcal_event.get('description') != event_body['description']:
print(f"Updating event: {summary} from {start_time_pd} to {end_time_pd}")
try:
gcal_service.events().update(
calendarId=GOOGLE_CALENDAR_ID,
eventId=existing_gcal_event['id'],
body=event_body
).execute()
except Exception as e:
print(f"Error updating Google Calendar event: {e}")
else:
# print(f"Event already up-to-date: {summary} from {start_time_pd} to {end_time_pd}")
pass # Event is identical, no action needed
del existing_event_map[pd_event_key] # Remove from map so it's not deleted
else:
# Event does not exist, create it
print(f"Creating event: {summary} from {start_time_pd} to {end_time_pd}")
try:
gcal_service.events().insert(
calendarId=GOOGLE_CALENDAR_ID,
body=event_body
).execute()
except Exception as e:
print(f"Error creating Google Calendar event: {e}")
# Any remaining events in existing_event_map were not found in PagerDuty and should be deleted
for key, event_to_delete in existing_event_map.items():
print(f"Deleting stale event: {event_to_delete.get('summary')} from {event_to_delete.get('start', {}).get('dateTime')} to {event_to_delete.get('end', {}).get('dateTime')}")
try:
gcal_service.events().delete(
calendarId=GOOGLE_CALENDAR_ID,
eventId=event_to_delete['id']
).execute()
except Exception as e:
print(f"Error deleting Google Calendar event: {e}")
if __name__ == '__main__':
sync_events()
Logic Behind the Code:
-
Configuration: The script fetches sensitive API keys and IDs from environment variables (
PAGERDUTY_API_TOKEN,PAGERDUTY_SCHEDULE_ID,GOOGLE_CALENDAR_ID,GOOGLE_APPLICATION_CREDENTIALS). This is a security best practice, preventing credentials from being hardcoded. -
get_pagerduty_oncalls: This function makes an HTTP GET request to the PagerDuty APIâs/oncallsendpoint. It fetches on-call entries for a specified schedule ID within a rolling window (e.g., next 14 days), ensuring we cover ongoing and upcoming shifts. It uses UTC for consistent time handling. -
initialize_google_calendar_service: This sets up the Google Calendar API client using the service account credentials downloaded earlier. It specifies the necessary OAuth 2.0 scopes for calendar access. -
get_existing_calendar_events: Before adding new events, the script retrieves existing events from your Google Calendar that match our expected summary pattern (âPagerDuty On-Callâ). This allows us to compare and avoid duplicates or update outdated entries. -
sync_events(Main Logic):- It first retrieves all relevant PagerDuty on-call shifts.
- Then, it fetches existing PagerDuty-related events from Google Calendar.
- It iterates through the PagerDuty shifts:
- If a shift already exists in Google Calendar with the same start/end time and summary, it checks for updates (e.g., if the description changed). If an update is needed, it performs an
events().update(). - If a PagerDuty shift does not exist in Google Calendar, it creates a new event using
events().insert(). - Finally, any events that were in Google Calendar but not found in the latest PagerDuty fetch are considered stale and are deleted using
events().delete(), keeping your calendar clean.
Step 4: Schedule the Synchronization Script
To keep your Google Calendar up-to-date, youâll want to run this script periodically. Cron is an excellent tool for this on Linux/Unix systems.
- Set Environment Variables:
Before running the script, you need to set the environment variables that it expects. You can do this directly in your terminal or, for cron jobs, within the cron definition or a wrapper script.
export PAGERDUTY_API_TOKEN='[YOUR_PAGERDUTY_API_TOKEN]'
export PAGERDUTY_SCHEDULE_ID='[YOUR_PAGERDUTY_SCHEDULE_ID]'
export GOOGLE_CALENDAR_ID='[YOUR_GOOGLE_CALENDAR_ID]'
export GOOGLE_APPLICATION_CREDENTIALS='/home/user/.config/pagerduty_sync/gcal_service_account.json'
# Test run
python3 /home/user/pagerduty_sync/pagerduty_sync.py
Replace the placeholders with your actual values. Remember the path to your Google Service Account JSON file.
- Create a Cron Job:
Open your cron editor. Add a line to schedule the script to run, for example, every 4 hours.
0 */4 * * * PAGERDUTY_API_TOKEN='[YOUR_PAGERDUTY_API_TOKEN]' PAGERDUTY_SCHEDULE_ID='[YOUR_PAGERDUTY_SCHEDULE_ID]' GOOGLE_CALENDAR_ID='[YOUR_GOOGLE_CALENDAR_ID]' GOOGLE_APPLICATION_CREDENTIALS='/home/user/.config/pagerduty_sync/gcal_service_account.json' python3 /home/user/pagerduty_sync/pagerduty_sync.py
Make sure the path to your pagerduty_sync.py script is correct and that python3 is accessible in the cron environmentâs PATH. Including the environment variables directly in the cron line ensures they are set for that specific job. For better manageability, you might put these variables into a wrapper script that then calls the Python script.
Common Pitfalls
-
Incorrect API Permissions:
- PagerDuty: Ensure your PagerDuty API token has at least âRead-onlyâ access. If the script canât fetch schedules, this is often the culprit.
- Google Calendar: Verify that the Service Account email address has âMake changes to eventsâ permissions on the target Google Calendar. Without this, the script wonât be able to create, update, or delete events.
-
Timezone Discrepancies:
- PagerDuty often operates in UTC internally. The script fetches in UTC and creates events in UTC. Google Calendar will then display these events according to your personal Google Calendar timezone settings. If event times appear incorrect, double-check the
timeZoneparameter in your Google Calendar event body (we used âUTCâ) and your Google Calendarâs display settings. Inconsistent handling of timezones (e.g., mixing naive datetimes with timezone-aware ones) can lead to offset errors.
- PagerDuty often operates in UTC internally. The script fetches in UTC and creates events in UTC. Google Calendar will then display these events according to your personal Google Calendar timezone settings. If event times appear incorrect, double-check the
-
Service Account Key Path or Calendar ID:
- Ensure the
GOOGLE_APPLICATION_CREDENTIALSenvironment variable (or hardcoded path if you deviated from the guide) points to the correct.jsonfile, and that theGOOGLE_CALENDAR_IDis accurate for the calendar you intend to sync. A common mistake is using the wrong calendar ID for a secondary calendar.
- Ensure the
-
Environment Variables Not Loaded in Cron:
- Cron environments are often minimal and do not inherit your userâs shell environment variables. Always explicitly set needed environment variables within the cron job definition itself, or source a file that sets them, as shown in the example.
Conclusion
Youâve successfully set up an automated system to synchronize your PagerDuty on-call schedules directly into your Google Calendar! This integration provides a centralized, up-to-date view of your responsibilities, reducing mental overhead and the risk of missing critical shifts. By automating this process, you empower your team to stay informed and better integrate on-call duties into their daily planning, ultimately contributing to a more resilient and efficient operational workflow.
Feel free to extend this script further â perhaps by syncing multiple PagerDuty schedules to different calendars, adding more detailed event descriptions, or integrating with other notification systems. At TechResolve, weâre committed to building practical solutions for complex DevOps challenges. Stay tuned for more guides and tutorials designed to streamline your operations!
đ Read the original article on TechResolve.blog
â Support my work
If this article helped you, you can buy me a coffee:

Top comments (0)