> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# On-call and escalation policy model: rotation, timeline, and incident state

> How rotations resolve who is on call, how a policy flattens into an ordered page timeline, how incidents advance open to resolved, and why every answer is pure compute over state you own.

# 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](/api-reference/oncall) 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](/guides/oncall-alerting) 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`.

```text theme={null}
anchor ─┬─ shift 0: members[0] ─┬─ shift 1: members[1] ─┬─ shift 2: members[2] ─┬─ ...
        │                       │                       │                       │
        └── shift_start ── shift_end = next_handoff_at ── ... wrap with modulo
```

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

```mermaid theme={null}
graph LR
  O[open] -->|ack| A[acknowledged]
  O -->|resolve| R[resolved]
  A -->|resolve| R
  O -->|reassign: swap policy| O
  A -->|reassign: swap policy| O
  O -->|timeline fully fired| E[exhausted]
```

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](/api-reference/endpoints/messaging) 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](/webhooks/overview) 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](/guides/oncall-alerting) — the step-by-step walk from rotation definition to a resolved incident and the actual page send.
* [On-Call API reference](/api-reference/oncall) — full endpoint, scope, and field-level reference.
* [Messages API](/api-reference/endpoints/messaging) — the send surface that reaches a resolved target.
* [Webhooks](/webhooks/overview) — delivery status for the pages you sent.
