A Python CLI tool for analyzing PagerDuty on-call schedules and identifying team members who exceed configurable on-call day limits per month.
- Fetch on-call schedules by PagerDuty team
- Timezone-aware per-user calculations - each user's on-call time is calculated in their local timezone
- After-hours only counting - only counts days where on-call coverage extends past standard workday hours (configurable, default: 5 PM)
- Calculate on-call days per user for a specified month
- Identify users exceeding configurable daily limits
- Beautiful color-coded terminal output using Rich
- JSON output support for automation and scripting
- Configurable via environment variables or CLI arguments
- Python 3.12 or higher
- uv (fast Python package installer)
- PagerDuty API key
# Clone the repository
git clone https://github.com/getsentry/on-call-scheduler.git
cd on-call-scheduler
# Install dependencies using uv
uv sync
# The tool is now ready to use
uv run oncall-scheduler --help# Install with development dependencies
uv sync --extra dev
# Run tests
uv run pytest- Log in to your PagerDuty account
- Navigate to Integrations → API Access Keys (General API token) or My Profile → User Settings (Personal API token).
- Create a new API key with read permissions
- Copy the API key for use in configuration
Create a .env file in the project root (see .env.example):
# Required
PAGERDUTY_API_KEY=your_api_key_here
# Optional (can also be specified via CLI)
PAGERDUTY_TEAM_IDS=TEAM1,TEAM2,TEAM3
ONCALL_MAX_DAYS=10
# Optional: Sentry error tracking
SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id
SENTRY_ENVIRONMENT=production- CLI arguments (highest priority)
- Environment variables
- .env file
- Default values (lowest priority)
The application includes built-in Sentry integration for error tracking and monitoring:
- Create a Sentry Project: Sign up at sentry.io and create a new project
- Get your DSN: Navigate to Settings → Projects → [Your Project] → Client Keys (DSN)
- Configure: Add the
SENTRY_DSNto your.envfile - Optional: Set
SENTRY_ENVIRONMENTto differentiate between environments (e.g., "production", "staging", "development")
When configured, the application will automatically:
- Capture and report unhandled exceptions
- Track performance metrics
- Provide detailed error context and stack traces
- Associate errors with specific releases
To disable Sentry, simply omit the SENTRY_DSN environment variable.
The scheduler uses timezone-aware calculations to only count days where users have on-call coverage extending past standard workday hours. This means:
-
User Timezones: Each user is assigned a timezone (from PagerDuty API, or configured via
USER_TIMEZONES, or defaults to schedule timezone) -
Workday End Hour: Configurable via
WORKDAY_END_HOUR(default: 17 for 5 PM) -
After-Hours Coverage: A day is only counted if the user's on-call shift extends past the workday end hour on that specific day in their local timezone
Scenario 1: Full Day Coverage
- User timezone: America/New_York (EST/EDT)
- Shift: Jan 1, 12:00 AM - Jan 2, 12:00 AM EST
- Result: Jan 1 is counted (coverage extends past 5 PM EST)
Scenario 2: Business Hours Only
- User timezone: America/New_York
- Shift: Jan 1, 9:00 AM - Jan 1, 5:00 PM EST
- Result: Jan 1 is NOT counted (coverage ends at 5 PM, doesn't extend past it)
Scenario 3: Evening Shift
- User timezone: America/New_York
- Shift: Jan 1, 5:00 PM - Jan 2, 9:00 AM EST
- Result: Both Jan 1 and Jan 2 are counted (coverage extends past 5 PM on Jan 1, and past 5 PM on Jan 2)
Scenario 4: Night Shift
- User timezone: Europe/London
- Shift: Jan 1, 10:00 PM - Jan 2, 6:00 AM GMT
- Result: Jan 1 is counted (shift extends past 5 PM), Jan 2 is NOT counted (shift ends at 6 AM, before 5 PM)
This approach ensures that only after-hours on-call burden is measured, not business-hours coverage.
Check on-call schedules for the current month:
uv run oncall-scheduler check --team PXXXXXXuv run oncall-scheduler check --team TEAM1 --team TEAM2 --team TEAM3uv run oncall-scheduler check --team PXXXXXX --month 2 --year 2026uv run oncall-scheduler check --team PXXXXXX --max-days 15Analyze the next 3 months starting from the current month:
uv run oncall-scheduler check --team PXXXXXX --months 3Analyze 6 months starting from a specific month:
uv run oncall-scheduler check --team PXXXXXX --month 1 --year 2026 --months 6Show all users including those under the limit:
uv run oncall-scheduler check --team PXXXXXX --verboseOutput results as JSON for scripting:
uv run oncall-scheduler check --team PXXXXXX --output json# Get names of users over the limit
uv run oncall-scheduler check --team PXXXXXX --output json | jq '.over_limit[].user.name'
# Count users over limit
uv run oncall-scheduler check --team PXXXXXX --output json | jq '.over_limit | length'PagerDuty On-Call Analysis - January 2026
Max Days Allowed: 10
┏━━━━━━━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓
┃ User ┃ Days ┃ Schedule ┃ Dates ┃ Status ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━┩
│ Alice Smith │ 14 │ Primary On-Call │ Jan 01-07, 15-21 │ Over │ (red)
│ Bob Jones │ 12 │ Secondary On-Call │ Jan 05-10, 20-25 │ Over │ (red)
│ Carol White │ 10 │ Primary On-Call │ Jan 08-12, 22-26 │ At │ (yellow)
└──────────────────┴──────┴───────────────────┴─────────────────────┴────────┘
Users under limit: 5 (use --verbose to see details)
Summary: 2 over limit, 1 at limit, 5 under limit
{
"month": 1,
"year": 2026,
"max_days": 10,
"over_limit": [
{
"user": {
"id": "PXXXXXX",
"name": "Alice Smith",
"email": "alice@example.com",
"html_url": "https://example.pagerduty.com/users/PXXXXXX"
},
"schedule_name": "Primary On-Call",
"total_days": 14,
"scheduled_dates": ["2026-01-01", "2026-01-02", "..."]
}
],
"at_limit": [...],
"under_limit": [...],
"summary": {
"over_limit_count": 2,
"at_limit_count": 1,
"under_limit_count": 5
}
}Run daily at 9 AM:
0 9 * * * cd /path/to/on-call-scheduler && /path/to/uv run oncall-scheduler check >> /var/log/oncall-check.log 2>&1Use the JSON output to integrate with notification systems:
#!/bin/bash
RESULT=$(uv run oncall-scheduler check --team PXXXXXX --output json)
OVER_COUNT=$(echo "$RESULT" | jq '.summary.over_limit_count')
if [ "$OVER_COUNT" -gt 0 ]; then
# Send alert to Slack, email, etc.
echo "$RESULT" | jq '.over_limit[]' | curl -X POST -H 'Content-Type: application/json' \
-d @- https://hooks.slack.com/services/YOUR/WEBHOOK/URL
fisrc/
└── oncall_scheduler/
├── __init__.py
├── __main__.py # Entry point
├── cli.py # Click CLI interface
├── config.py # Configuration management
├── models.py # Data models
├── api/
│ ├── client.py # PagerDuty API wrapper
│ └── exceptions.py # Custom exceptions
├── analysis/
│ ├── analyzer.py # Main analysis orchestrator
│ └── calculator.py # On-call days calculator
└── output/
└── formatter.py # Output formatting
# Run all tests
uv run pytest
# Run with coverage
uv run pytest --cov=src/oncall_scheduler --cov-report=term-missing
# Run specific test file
uv run pytest tests/test_calculator.py# Format code
uv run black src/oncall_scheduler tests
# Lint code
uv run ruff check src/oncall_scheduler tests
# Type checking
uv run mypy src/oncall_schedulerError: Authentication failed
Solution: Check that your PAGERDUTY_API_KEY is correct and has the necessary permissions.
Error: Resource not found: Team PXXXXXX not found
Solution: Verify the team ID is correct. You can find team IDs in the PagerDuty web UI under Configuration → Teams.
Warning: No schedules found for teams: [PXXXXXX]
Solution: The team might not have any associated schedules. Check that the team has schedules configured in PagerDuty.
Apache-2.0
Contributions are welcome! Please feel free to submit a Pull Request.
For issues and questions:
- GitHub Issues: Create an issue
- PagerDuty API Documentation: https://developer.pagerduty.com/