🚀 Executive Summary
TL;DR: This guide solves the problem of manually reviewing PagerDuty on-call history for shift handoff notes by automating the process. It details building a Python script to fetch previous week’s notes from the PagerDuty API, format them into an HTML email, and send them to a team inbox via a scheduled cron job.
🎯 Key Takeaways
- Securely manage credentials by utilizing environment variables (e.g., `config.env` with `python-dotenv`) and App Passwords for email services, preventing hard-coding of sensitive PagerDuty API keys and email credentials.
- Interact with the PagerDuty v2 Read-Only API to fetch on-call data, specifically requesting `oncalls` for a defined `schedule_id` and date range, ensuring `include[]: “users”` to retrieve engineer names and filtering for `handoff_notes`.
- Automate script execution with a cron job (e.g., `0 2 * * 1 python3 script.py`) to run weekly, calculating the previous week’s date range using `datetime.utcnow()` to correctly handle UTC timestamps from the PagerDuty API.
Automate PagerDuty Shift Handoff Notes to Email
Hey team, Darian Vance here. Let’s talk about a small but powerful automation I set up that’s given me back a couple of hours every single week. On Monday mornings, I used to manually sift through PagerDuty’s on-call history to piece together the weekend’s events. I was looking for those crucial handoff notes to get context for the week ahead. It was tedious, and I’d sometimes miss important details.
I realized this was a perfect task to automate. Why not have a clean, formatted summary of the previous week’s handoff notes sitting in my inbox every Monday morning? This guide will walk you through exactly how to build that. It’s a simple script, but the value it delivers in saved time and improved context is huge.
Prerequisites
Before we dive in, make sure you have the following ready:
- A PagerDuty account with admin or manager permissions to generate an API key.
- The Schedule ID for the on-call schedule you want to track.
- A dedicated email account for sending notifications. I highly recommend using a service account and an App Password if you’re using a provider like Gmail, as it’s more secure.
- Python 3 installed on a server or service where you can run a scheduled script.
- Familiarity with environment variables for securely storing credentials.
The Guide: Step-by-Step
Step 1: Get Your PagerDuty Credentials
First, we need to allow our script to talk to PagerDuty. This requires an API key and the ID of the specific schedule we’re interested in.
- Generate an API Key: In PagerDuty, navigate to Integrations > API Access Keys and create a new key. Give it a descriptive name like “Shift Handoff Automation” and make sure it’s a v2 Read-Only Key. Copy this key immediately; you won’t be able to see it again.
- Find Your Schedule ID: Go to People > On-Call Schedules and click on the schedule you want to monitor. Look at the URL in your browser. It will look something like
https://your-company.pagerduty.com/schedules#/[SCHEDULE_ID]. That alphanumeric string at the end is your Schedule ID.
Pro Tip: For security, always create a specific, read-only API key for each integration. If a key is ever compromised, you can revoke it without affecting other services.
Step 2: Set Up Your Environment
We need a place to store our sensitive credentials. I always use environment variables for this, never hard-coding them into the script. I’ll skip the standard virtualenv setup since you likely have your own workflow for that. Let’s jump straight to the configuration.
Create a file named config.env in your project directory. This is where we’ll store our secrets.
# PagerDuty Details
PD_API_KEY="your_pagerduty_api_key_here"
PD_SCHEDULE_ID="your_schedule_id_here"
# Email Details
SENDER_EMAIL="your-sender-email@example.com"
SENDER_PASSWORD="your_email_app_password"
RECIPIENT_EMAIL="your-team-inbox@example.com"
SMTP_SERVER="smtp.gmail.com"
SMTP_PORT=587
Next, you’ll need to install the necessary Python libraries. The only external one we need is `requests`. You can install it using pip: `pip install requests python-dotenv`. The `python-dotenv` library makes it easy to load our `config.env` file.
Step 3: The Python Script
This is where the magic happens. The script will perform four main actions: load our config, calculate the correct date range, fetch on-call data from PagerDuty, and email the formatted notes.
Here’s the complete script. I’ve added comments to explain each part.
import os
import requests
import smtplib
from datetime import datetime, timedelta
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from dotenv import load_dotenv
# Load environment variables from config.env
load_dotenv('config.env')
# --- Configuration ---
PD_API_KEY = os.environ.get("PD_API_KEY")
PD_SCHEDULE_ID = os.environ.get("PD_SCHEDULE_ID")
SENDER_EMAIL = os.environ.get("SENDER_EMAIL")
SENDER_PASSWORD = os.environ.get("SENDER_PASSWORD")
RECIPIENT_EMAIL = os.environ.get("RECIPIENT_EMAIL")
SMTP_SERVER = os.environ.get("SMTP_SERVER")
SMTP_PORT = int(os.environ.get("SMTP_PORT", 587))
def get_previous_week_oncall_notes():
"""Fetches PagerDuty on-call handoff notes from the previous week."""
# Calculate the start and end of the previous week (Monday to Sunday)
today = datetime.utcnow().date()
start_of_last_week = today - timedelta(days=today.weekday() + 7)
end_of_last_week = start_of_last_week + timedelta(days=6)
since = start_of_last_week.isoformat()
until = (end_of_last_week + timedelta(days=1)).isoformat() # PagerDuty 'until' is exclusive
print(f"Fetching data from {since} to {until}")
headers = {
"Authorization": f"Token token={PD_API_KEY}",
"Accept": "application/vnd.pagerduty+json;version=2",
"Content-Type": "application/json"
}
params = {
"schedule_ids[]": PD_SCHEDULE_ID,
"since": since,
"until": until,
"include[]": "users" # Important to get user names
}
try:
response = requests.get(
f"https://api.pagerduty.com/oncalls",
headers=headers,
params=params
)
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
oncalls = response.json().get('oncalls', [])
# We only care about entries that actually have a handoff note
notes = [oc for oc in oncalls if oc.get('handoff_notes')]
return notes, start_of_last_week.strftime('%B %d, %Y')
except requests.exceptions.RequestException as e:
print(f"Error fetching data from PagerDuty: {e}")
return None, None
def format_notes_for_email(notes, week_start_date):
"""Formats the collected notes into an HTML email body."""
if not notes:
return f"<h2>No Handoff Notes Found</h2><p>No on-call handoff notes were logged for the week of {week_start_date}.</p>"
email_body = f"<h1>PagerDuty Handoff Notes Summary</h1><h3>Week of {week_start_date}</h3><hr>"
# Sort notes by end time to keep them in chronological order
notes.sort(key=lambda x: x['end'])
for note in notes:
user_name = note['user']['summary']
start_time = datetime.fromisoformat(note['start'].replace('Z', '+00:00')).strftime('%a, %b %d at %I:%M %p UTC')
end_time = datetime.fromisoformat(note['end'].replace('Z', '+00:00')).strftime('%a, %b %d at %I:%M %p UTC')
handoff_note = note['handoff_notes'].replace('\n', '<br>') # Preserve line breaks
email_body += f"""
<div style="border-left: 3px solid #00aaff; padding-left: 15px; margin-bottom: 20px;">
<p><strong>Engineer:</strong> {user_name}</p>
<p><strong>Shift End:</strong> {end_time}</p>
<p><strong>Notes:</strong></p>
<blockquote style="margin: 0 0 0 20px; border-left: 2px solid #ccc; padding-left: 10px; color: #555;">
{handoff_note}
</blockquote>
</div>
"""
return email_body
def send_email(html_content, week_start_date):
"""Sends the email using SMTP."""
msg = MIMEMultipart('alternative')
msg['Subject'] = f"Weekly PagerDuty Handoff Summary: Week of {week_start_date}"
msg['From'] = SENDER_EMAIL
msg['To'] = RECIPIENT_EMAIL
part = MIMEText(html_content, 'html')
msg.attach(part)
try:
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
server.starttls()
server.login(SENDER_EMAIL, SENDER_PASSWORD)
server.sendmail(SENDER_EMAIL, RECIPIENT_EMAIL, msg.as_string())
print("Email sent successfully!")
except Exception as e:
print(f"Failed to send email: {e}")
if __name__ == "__main__":
oncall_notes, week_start = get_previous_week_oncall_notes()
if oncall_notes is not None:
email_html = format_notes_for_email(oncall_notes, week_start)
send_email(email_html, week_start)
else:
print("Could not generate email content due to an earlier error.")
Pro Tip: Notice the `response.raise_for_status()` line. This is a clean way to handle HTTP errors. If PagerDuty returns anything other than a 2xx success code, the script will raise an exception, preventing it from proceeding with bad data. In my production setups, I’d wrap this in more robust logging and alerting.
Step 4: Schedule the Script
The final step is to run this script automatically. A simple cron job is perfect for this. We want it to run early every Monday morning to collect the previous week’s notes. I usually set mine for 2 AM on Monday.
To set this up, you would typically edit your crontab and add a line like this. Remember to run it from within your project directory so it can find the `config.env` file.
0 2 * * 1 python3 script.py
This tells the system to execute our Python script at 2:00 AM every Monday.
Common Pitfalls
Here are a few spots where I’ve seen this kind of integration stumble:
- Timezone Mismatches: The PagerDuty API operates in UTC. My script explicitly handles this by using `datetime.utcnow()` and parsing the UTC timestamps from the API. If your server is in a different timezone, relying on `datetime.now()` can pull the wrong date range.
- Email Authentication: Many modern email providers (like Gmail) will block login attempts from less secure apps. Using an App Password is almost always required instead of your regular account password.
- API Permissions: If you use a general-purpose API key that gets revoked, your script will fail. Using a dedicated, read-only key prevents this and is a better security practice.
- Empty Notes: The script handles the case where no notes are found for a given week, but it’s a good reminder to encourage your team to consistently fill out their handoff notes!
Conclusion
And that’s it. With a relatively simple Python script and a cron job, you’ve created a valuable pipeline that pushes crucial context directly to your team’s inbox. It removes a manual, repetitive task, ensures consistency, and helps everyone start the week on the same page.
This is the kind of small, high-impact automation that we in DevOps live for. It frees up our most valuable resource—time—to focus on bigger challenges. Hope this helps your team as much as it has helped mine.
All the best,
Darian Vance
🤖 Frequently Asked Questions
âť“ How can I automate PagerDuty shift handoff notes to email?
Automate PagerDuty shift handoff notes by creating a Python script that uses the PagerDuty v2 API to fetch on-call data and handoff notes for the previous week, formats them into an HTML email, and sends it via SMTP, scheduled by a cron job.
âť“ What are the benefits of automating PagerDuty handoff notes compared to manual review?
Automating PagerDuty handoff notes saves significant time by eliminating manual sifting through history, ensures consistent delivery of crucial context, reduces the risk of missing important details, and helps teams start the week on the same page.
âť“ What are common challenges when setting up PagerDuty email automation?
Common challenges include timezone mismatches (PagerDuty API uses UTC), email authentication issues (often requiring an App Password), incorrect PagerDuty API permissions (a v2 Read-Only key is recommended), and ensuring consistent handoff note logging by the on-call team.
Leave a Reply