Back to skills
extension
Category: OtherAPI key required

On Call Schedule Optimizer

Analyze and optimize on-call schedules for engineering teams. Balance load distribution, respect timezone coverage, minimize burnout, handle holidays and PTO...

personAuthor: charlie-morrisonhubclawhub

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).