On-Call Schedule Optimizer
Build on-call schedules that don't burn people out. Analyze current rotation fairness, balance load across timezones, respect PTO and holidays, minimize after-hours pages per person, and generate optimized schedules — for PagerDuty, OpsGenie, or spreadsheets.
Use when: "optimize on-call schedule", "on-call rotation", "fair on-call distribution", "reduce on-call burnout", "timezone coverage", "who's on call too much", or when designing on-call for a new team.
Commands
1. analyze — Audit Current On-Call Schedule
Step 1: Extract Current Schedule
# PagerDuty API
curl -s "https://api.pagerduty.com/schedules/$SCHEDULE_ID" \
-H "Authorization: Token token=$PD_TOKEN" \
-H "Content-Type: application/json" | python3 -c "
import json, sys
schedule = json.load(sys.stdin)['schedule']
print(f'Schedule: {schedule[\"name\"]}')
print(f'Timezone: {schedule[\"time_zone\"]}')
for layer in schedule.get('schedule_layers', []):
print(f'\\nLayer: {layer[\"name\"]}')
for user in layer.get('users', []):
print(f' - {user[\"user\"][\"summary\"]}')
"
# OpsGenie API
curl -s "https://api.opsgenie.com/v2/schedules/$SCHEDULE_ID" \
-H "Authorization: GenieKey $OG_API_KEY" | python3 -c "
import json, sys
data = json.load(sys.stdin)['data']
print(f'Schedule: {data[\"name\"]}')
print(f'Timezone: {data[\"timezone\"]}')
"
Step 2: Calculate Fairness Metrics
from collections import defaultdict
from datetime import datetime, timedelta
def analyze_on_call_fairness(shifts):
"""Analyze fairness of on-call distribution"""
person_stats = defaultdict(lambda: {
'total_hours': 0,
'weekend_hours': 0,
'holiday_hours': 0,
'night_hours': 0, # 22:00-08:00
'incidents': 0,
'consecutive_days': 0,
'max_consecutive': 0,
})
for shift in shifts:
person = shift['person']
start = shift['start']
end = shift['end']
hours = (end - start).total_seconds() / 3600
person_stats[person]['total_hours'] += hours
# Weekend check
if start.weekday() >= 5:
person_stats[person]['weekend_hours'] += hours
# Night check (22:00-08:00)
if start.hour >= 22 or start.hour < 8:
person_stats[person]['night_hours'] += hours
# Print fairness report
avg_hours = sum(s['total_hours'] for s in person_stats.values()) / len(person_stats)
for person, stats in sorted(person_stats.items(), key=lambda x: -x[1]['total_hours']):
deviation = ((stats['total_hours'] - avg_hours) / avg_hours) * 100
fairness = '🟢' if abs(deviation) < 10 else '🟡' if abs(deviation) < 25 else '🔴'
print(f'{fairness} {person}: {stats["total_hours"]:.0f}h total ({deviation:+.0f}%), '
f'{stats["weekend_hours"]:.0f}h weekends, {stats["night_hours"]:.0f}h nights')
Step 3: Generate Report
# On-Call Schedule Analysis
## Fairness Score: 65/100 (⚠️ Unbalanced)
## Load Distribution (last 90 days)
| Person | Total Hours | Weekends | Nights | Incidents | Deviation |
|--------|------------|----------|--------|-----------|-----------|
| Alice | 720h | 180h | 240h | 23 | +15% 🟡 |
| Bob | 480h | 120h | 160h | 12 | -23% 🔴 |
| Carol | 600h | 200h | 200h | 18 | -4% 🟢 |
| Dave | 720h | 100h | 240h | 28 | +15% 🟡 |
## Issues Found
1. 🔴 Alice and Dave carry 30% more load than Bob
2. 🟡 Carol has disproportionate weekend hours (33% vs team avg 25%)
3. 🟡 No timezone diversity — all US-East, gap 02:00-08:00 UTC
4. 🔴 Dave had 14 consecutive on-call days last month (burnout risk)
## Recommendations
1. Equalize rotation: Bob needs more shifts to balance
2. Add weekend weight: count weekend hours as 1.5× for fairness
3. Cap consecutive days at 7
4. Consider follow-the-sun with EU team (if available)
2. generate — Create Optimized Schedule
Given team members, timezones, PTO calendar, and constraints:
- Generate rotation that minimizes max deviation from fair share
- Respect PTO and holidays (no on-call during approved time off)
- Balance weekend and night hours separately
- Ensure handoff times align with business hours for each timezone
- Cap consecutive on-call days (default: 7)
3. coverage — Analyze Timezone Coverage
Map on-call coverage across 24 hours:
00 02 04 06 08 10 12 14 16 18 20 22 24
|---US-West---|
|---US-East---|
|---EU----|
|---India----|
|---APAC---|
Gap: 03:00-05:00 UTC (no primary on-call)
Recommend schedule layers to fill gaps.
4. burnout — Calculate Burnout Risk
Score each team member's burnout risk based on:
- Hours on-call in last 30/90 days
- Incidents handled (especially 2AM+ pages)
- Consecutive on-call days
- PTO taken vs owed
- After-hours page frequency
Flag anyone above threshold and recommend remediation (extra PTO, reduced rotation, hire).