Skip to main content

On-call and the escalation-policy model

Incident alerting comes down to three primitives, the same ones PagerDuty-style tools are built on:
  • Rotation — an ordered list of responders and a shift cadence. Answers “who is on call right now.”
  • Escalation policy — an ordered list of steps saying who to page, on which channels, and how long to wait for an acknowledgement before moving on. Answers “who gets paged next if nobody responds.”
  • Incident — one occurrence of something that needs a human, traveling open → acknowledged → resolved, or to exhausted when nobody ever responds.
The On-Call API implements all three as a thin HTTP surface over a pure compute engine. You send the definition, you receive the answer. This page is the mental model behind that design; the on-call alerting guide walks the same ground as a step-by-step build.

Rotation — who holds the pager

A rotation is a fixed-length shift schedule anchored to an absolute instant (anchor). On-call rotates through members in their list order, one member per shift, wrapping with modulo. The anchor is the single design decision that keeps the math honest. A rotation defined as “every Monday at 09:00 local” has to reason about timezones and daylight-saving gaps and repeats; a rotation anchored to an absolute ISO instant never does. Each hand-off is exactly shift_length_seconds after the previous one, and timezones enter the picture exactly once — when you pick the anchor for the first hand-off. From then on the arithmetic is plain seconds, so it stays correct across DST changes no matter which timezone the anchor encodes. cadence is daily (24-hour shifts), weekly (7-day shifts), or custom, where you carry your own shift_length_seconds — 43,200 for 12-hour halves, 9,000 for a school-nights shift, whatever matches your staffing. resolve answers three facts: the member currently on call, the exact shift_start / shift_end window they hold, and next_on_call with next_handoff_at.
Two edge cases are resolved deliberately:
  • Before the anchor, the rotation has not started, but a pager-less query still needs a human — member one is treated as on call for the notional shift that ends at the anchor, and next_handoff_at equals the anchor itself.
  • A backdated at returns the historically correct shift. “Who was on call when the alert fired” is a legitimate question, and the window math is deterministic enough to answer it.
Members are opaque strings — E.164 numbers, email addresses, push tokens, or your own user ids. The engine picks which member; it never interprets the value.

Escalation policy — the flattened page timeline

An escalation policy is an ordered list of steps. Each step names:
  • a target — either a live rotation or a fixed member list (users),
  • the channels to page (push, SMS, voice, email, …), and
  • escalate_after_seconds — how long to wait for an acknowledgement before the next step fires.
planEscalation flattens this tree into an absolute-offset timeline: step 0 fires at offset 0, step 1 after step 0’s wait window, and so on, with each offset the cumulative sum of the windows before it. repeat loops the whole policy that many additional times when nobody acknowledges — the PagerDuty “repeat the policy N times” knob. The result is the schedule an incident driver walks: fire page P at fire_at, and if no acknowledgement lands before the next fire_at, fire the next page. Rotation targets are resolved at each page’s fire instant, not once at incident start. A hand-off landing mid-escalation pages whoever holds the pager at that moment, which is the behavior you actually want when an incident outlives a rotation boundary. The common shape mixes the two target kinds: rotation first (whoever currently holds the pager), a fixed fallback human second. POST /api/v1/oncall/escalation/plan returns exactly this timeline — every page with its round, step, offset_seconds, fire_at, targets, and channels, plus total_pages across all rounds.

Incident lifecycle — open, acknowledged, resolved, exhausted

An incident is the snapshot you keep: its policy, the started_at that anchors the timeline, the current status, and a pages_fired high-water mark. Two operations drive it:
  • tick — given the snapshot and a “now”, returns which pages became due, the new high-water mark, and when to wake up next (next_tick_at).
  • transition — applies an operator action: ack, resolve, or reassign.
The state machine rejects the edges it does not allow: acknowledging a resolved incident, or re-acking an acknowledged one, returns 409 incident_transition_invalid instead of silently corrupting your state. A halted (acknowledged or resolved) incident ticks back due_pages: [] and next_tick_at: null, so your scheduler stands down. reassign is the only path back to paging — it swaps the policy, resets the mark to zero, and restarts the timeline; that is how you bump a Sev-2 incident onto a broader Sev-1 policy. When every page across every round has fired with no acknowledgement, tick reports status: "exhausted" and hands the incident back to you — treat that as its own alert (page a fallback channel, open a ticket) rather than keep a scheduler awake on a null next-tick.

Pure compute, caller-owned state

Every On-Call endpoint is pure compute over the request body. It never stores your rotation, never stores your incident, and never sends anything. The caller owns persistence end to end: you pass the full definition in, you persist the returned snapshot, and the next call carries it back. That posture buys three concrete properties:
  • Idempotency by construction — the flattened timeline is strictly increasing in fire time, so the only progress you must remember is a single monotonic integer, pages_fired. No set of “already fired” pages to reconcile, and replaying a tick with a stale snapshot returns the same due-pages, never a double page.
  • Determinism — the same snapshot and the same at always return the same answer. You can test a rotation before it goes live, and you can reconstruct “who was on call then” from the definition alone.
  • Zero lock-in on the state layer — keep incidents in your own database, or in memory for a quick script; the API imposes no schema of its own on your side.
What the engine never does is the send. When a tick returns due pages, your driver fans each one out through the Messages API on the channels the step named. Because every page exits through the same Messages pipeline as the rest of your traffic, your sender ids, quiet-hours policy, and delivery webhooks apply to pages exactly as they apply to any other send — the engine decides who and when, and the existing send surface carries the delivery gates. Subscribe to delivery-status webhooks so an unacknowledged page is distinguishable from a delivered one before its window elapses.

Where to go next

  • On-call alerting guide — the step-by-step walk from rotation definition to a resolved incident and the actual page send.
  • On-Call API reference — full endpoint, scope, and field-level reference.
  • Messages API — the send surface that reaches a resolved target.
  • Webhooks — delivery status for the pages you sent.