On-Call & Schedules
Multi-layer rotations with restrictions, fixed shifts, overrides, and escalation policies — the resolution stack for "who is on call right now".
On-Call & Schedules
A schedule is a stack of layers. A layer is a rotation of users with optional time restrictions. At any moment, the schedule resolves to a primary (the first layer whose restriction is active right now) plus any secondary, tertiary, etc. that also happen to be active.
This mirrors PagerDuty's layered schedules. Add BetterStack's idea of pinning a specific user to a specific time range — a fixed shift — and you have full coverage for any rotation pattern people actually use.
The resolution stack
For each layer at time T, Happy Uptime walks this priority list:
Override (highest)
Active if now ∈ [start_at, end_at). Scope: schedule-wide (covers every layer) or layer-specific. Used when someone needs to cover for someone else for a defined window — vacation, conference, sick day.
Fixed shift
A specific user pinned to a specific (layer_id, start_at, end_at) tuple. Wins over rotation but loses to overrides. Used for one-off coverage that doesn't fit the rotation pattern (weekend swap, holiday).
Rotation
The default. shiftIdx = floor((now − layerEpoch) / rotationLength), then members[shiftIdx % members.length]. The epoch is the most recent handoff day + hour (in the layer's timezone) at-or-before the layer's created_at, so handoffs always land on Monday 9am (or whenever you configured).
Restriction gate
If the layer's restriction (none / weekday-business-hours / weekends-only / custom) does NOT include now, the layer is off-duty for this moment. Resolution falls through to the next layer.
The schedule's primary is the first layer whose user is non-null AND whose restriction is active at the current moment.
Common patterns
One layer. Weekly rotation. No restriction. 5 members. Each gets every 5th week.
Two layers:
- Primary — Mon–Fri 9am–5pm local. Senior engineers.
- Secondary — always on. Junior engineers (or external paging service).
During business hours: primary is active, secondary is also active but ignored. Nights/weekends: primary is off-duty, secondary takes over.
Three layers, each with weekday-hours restriction in a different timezone:
- Asia primary — Mon–Fri 9am–6pm Asia/Tokyo
- Europe primary — Mon–Fri 9am–6pm Europe/London
- Americas primary — Mon–Fri 9am–6pm America/New_York
At any given moment, exactly one layer is active. Coverage rolls around the globe.
One rotation layer + an escalation policy:
- Layer 0: Engineering team rotates weekly.
- Escalation level 1 (after 5 min unack): page CTO directly.
- Escalation level 2 (after 15 min unack): post to #incident channel.
The escalation policy is independent of layers — it fires only on unacked incidents.
What each thing controls
| Concept | Controls | Storage |
|---|---|---|
| Schedule | Name, organization, default Slack routing, ICS token | oncall_schedules |
| Layer | Rotation cadence, handoff time, timezone, restriction, ordered members | oncall_layers + oncall_members |
| Override | One person covering for the rotation in a time window | oncall_overrides |
| Fixed shift | One person pinned to a specific layer for a specific window | oncall_fixed_shifts |
| Escalation policy | What happens N minutes after an incident is unacked | oncall_escalation_policies |
Restrictions in detail
A layer's restriction_type is one of:
| Type | Active when |
|---|---|
none | Always (24/7) |
weekday_hours | Mon–Fri, hours [start, end) in layer's timezone |
weekends | Sat + Sun, all hours |
custom | A subset of weekdays you pick + optional hour window. Hour windows can wrap midnight (e.g. 22:00 → 06:00 covers nights). |
Restrictions are evaluated in the layer's timezone, so DST transitions resolve cleanly.
Quick links
API
Full CRUD lives under /oncall/schedules/:id — see schedules, layers, overrides, shifts, escalation, timeline, and iCalendar export.