Building a Docket Escalation Ladder in Python
A docket escalation ladder is a small, deterministic state machine: it holds one unacknowledged deadline alert at a tier, starts an SLA timer, and on each evaluation either resolves the alert (a human acknowledged it), waits (the timer is still running), or promotes it to the next tier and restarts the timer. This page implements that machine in idiomatic Python 3.11+, with every transition sealed into a hash-chained audit ledger.
It is the runnable counterpart to the Escalation Routing Workflows design and consumes the unacknowledged reminders produced by the Deadline Reminder Cadence Engines upstream. The goal is a self-contained core you can drop into a scheduler tick and test without a database or a message bus.
What the Ladder Must Guarantee
Before any code, four properties fix the design. Promotion is forward-only — an alert climbs and never descends, so a deadline cannot ping-pong while its window closes. Only an attributable acknowledgment stops the ladder — not a delivery receipt, not elapsed time alone. Timers are absolute expiry instants, not in-memory countdowns, so a process restart resumes every in-flight escalation. And every transition is immutably logged to an append-only, hash-chained ledger, because the escalation record is the primary evidence in a missed-deadline inquiry. The implementation below encodes each property directly rather than leaving it to convention.
Modelling Tiers and Alerts
Start with immutable, typed definitions. The ladder is a tuple of frozen tiers so it cannot be mutated at runtime, and each tier carries its role, its SLA as a timedelta, and the channels it may use. Modelling the role as a string that is later resolved against an assignment table — rather than a person — keeps the ladder stable across staffing changes.
from __future__ import annotations
import hashlib
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
from zoneinfo import ZoneInfo
UTC = ZoneInfo("UTC")
class AlertStatus(str, Enum):
ACTIVE = "ACTIVE" # timer running at the current tier
ACKNOWLEDGED = "ACKNOWLEDGED" # terminal: a human accepted ownership
BREACHED_FINAL = "BREACHED_FINAL" # terminal: top tier lapsed → manual backstop
@dataclass(frozen=True)
class EscalationTier:
role: str
sla: timedelta
channels: tuple[str, ...]
# Ordered ladder: paralegal → manager → attorney → group lead.
LADDER: tuple[EscalationTier, ...] = (
EscalationTier("paralegal", timedelta(hours=8), ("email", "in_app")),
EscalationTier("docketing_manager", timedelta(hours=4), ("email", "in_app", "sms")),
EscalationTier("responsible_attorney", timedelta(hours=2), ("sms", "phone")),
EscalationTier("practice_group_lead", timedelta(hours=1), ("sms", "page", "phone")),
)
@dataclass
class EscalatingAlert:
"""One deadline's live escalation state. `sla_expiry` is an absolute instant."""
deadline_id: str
tier_index: int
sla_expiry: datetime
status: AlertStatus = AlertStatus.ACTIVE
@property
def tier(self) -> EscalationTier:
return LADDER[self.tier_index]
@classmethod
def open(cls, deadline_id: str, started: datetime) -> "EscalatingAlert":
if started.tzinfo is None:
raise ValueError("started must be timezone-aware") # reject naive datetimes
return cls(deadline_id, 0, started.astimezone(UTC) + LADDER[0].sla)
The Append-Only Ledger
The ledger is a hash-chained log. Each entry binds its fields and the previous entry’s hash into a SHA-256 digest, so reordering or back-dating any entry breaks the chain — the tamper-evidence a reviewer relies on. In production this backs onto write-once storage; here it is an in-memory list with the same contract, matching the immutable-ledger discipline of the Core Docketing Architecture & Deadline Types reference.
@dataclass
class LedgerEntry:
deadline_id: str
tier_role: str
status: str
actor: str
occurred_utc: str
prev_hash: str
record_hash: str
class AppendOnlyLedger:
def __init__(self) -> None:
self._entries: list[LedgerEntry] = []
self._tip = "0" * 64 # genesis hash
def append(self, deadline_id: str, tier_role: str, status: str,
actor: str, now: datetime) -> LedgerEntry:
occurred = now.astimezone(UTC).isoformat()
material = "|".join([deadline_id, tier_role, status, actor, occurred, self._tip])
record_hash = hashlib.sha256(material.encode("utf-8")).hexdigest()
entry = LedgerEntry(deadline_id, tier_role, status, actor,
occurred, self._tip, record_hash)
self._entries.append(entry)
self._tip = record_hash # chain forward
return entry
@property
def entries(self) -> list[LedgerEntry]:
return list(self._entries)
The Evaluation Loop
evaluate is the whole state machine and maps one-to-one onto the flowchart above. It is pure with respect to time — it takes now as an argument rather than reading the clock — which is what makes it testable. A running timer is a no-op; a breach either promotes and restarts the timer or, at the top rung, fails closed.
def evaluate(alert: EscalatingAlert, now: datetime,
ledger: AppendOnlyLedger) -> AlertStatus:
"""Advance one alert by a single tick. Forward-only; never descends."""
if alert.status is not AlertStatus.ACTIVE:
return alert.status # terminal — nothing to do
if now < alert.sla_expiry:
return alert.status # WAIT: timer still running
# SLA breached without acknowledgment.
if alert.tier_index + 1 < len(LADDER):
breached_role = alert.tier.role
alert.tier_index += 1 # promote forward
alert.sla_expiry = now + alert.tier.sla # restart timer for the new tier
ledger.append(alert.deadline_id, breached_role,
AlertStatus.ACTIVE.value + "->ESCALATED", "system", now)
return alert.status # stays ACTIVE at the new tier
# Top tier lapsed: fail closed into the manual backstop.
alert.status = AlertStatus.BREACHED_FINAL
ledger.append(alert.deadline_id, alert.tier.role,
AlertStatus.BREACHED_FINAL.value, "system", now)
return alert.status
def acknowledge(alert: EscalatingAlert, who: str, now: datetime,
ledger: AppendOnlyLedger) -> AlertStatus:
"""A human accepts ownership. Only an ACTIVE alert can be acknowledged."""
if alert.status is not AlertStatus.ACTIVE:
return alert.status
alert.status = AlertStatus.ACKNOWLEDGED
ledger.append(alert.deadline_id, alert.tier.role,
AlertStatus.ACKNOWLEDGED.value, who, now)
return alert.status
Notice that acknowledge records who accepted and at which tier’s role, while an escalation is authored by "system". That distinction is what lets a reviewer see the difference between a human taking ownership and the machine promoting an alert nobody answered.
Running the Ladder
A short driver shows the intended flow: a paralegal never acknowledges, the alert escalates to the docketing manager after the 8-hour SLA, and the manager acknowledges within their window.
ledger = AppendOnlyLedger()
start = datetime(2026, 7, 16, 9, 0, tzinfo=UTC)
alert = EscalatingAlert.open("US-OA-4488", start)
# 9 hours later: the paralegal's 8h SLA lapsed → escalate to the manager.
evaluate(alert, start + timedelta(hours=9), ledger)
assert alert.tier.role == "docketing_manager"
assert alert.status is AlertStatus.ACTIVE
# The manager acknowledges an hour after that, inside their 4h window.
acknowledge(alert, "j.okafor", start + timedelta(hours=10), ledger)
assert alert.status is AlertStatus.ACKNOWLEDGED
for e in ledger.entries:
print(e.tier_role, e.status, e.actor, e.occurred_utc)
The demo uses plain wall-clock hours for clarity. In production the SLA windows are measured in business hours against the shared office calendar, so an alert firing late on a Friday does not breach over a weekend — the same calendar the cadence engine and rule engine use, which keeps escalation timing defensible.
Operational Action: Take now as an argument everywhere and store sla_expiry as an absolute UTC instant. A durable scheduler then re-evaluates persisted alerts on restart, so a redeploy never silently forgets an in-flight escalation.
Testing
Because the machine is time-driven and forward-only, tests assert three things: a breach promotes, an acknowledgment stops the ladder, and the ledger stays chained.
import pytest
from datetime import datetime, timedelta
def test_breach_promotes_and_restarts_timer() -> None:
led = AppendOnlyLedger()
start = datetime(2026, 7, 16, 9, 0, tzinfo=UTC)
a = EscalatingAlert.open("D-9", start)
evaluate(a, start + timedelta(hours=9), led) # 8h SLA lapsed
assert a.tier_index == 1 # promoted forward
assert a.sla_expiry == start + timedelta(hours=9) + timedelta(hours=4)
def test_acknowledgment_is_terminal() -> None:
led = AppendOnlyLedger()
start = datetime(2026, 7, 16, 9, 0, tzinfo=UTC)
a = EscalatingAlert.open("D-9", start)
acknowledge(a, "m.ruiz", start + timedelta(hours=1), led)
# A later tick must not resurrect or escalate a resolved alert.
evaluate(a, start + timedelta(hours=99), led)
assert a.status is AlertStatus.ACKNOWLEDGED
assert a.tier_index == 0
def test_ledger_chain_is_linked() -> None:
led = AppendOnlyLedger()
start = datetime(2026, 7, 16, 9, 0, tzinfo=UTC)
a = EscalatingAlert.open("D-9", start)
evaluate(a, start + timedelta(hours=9), led) # ESCALATED entry
acknowledge(a, "m.ruiz", start + timedelta(hours=10), led)
entries = led.entries
assert len(entries) == 2
assert entries[1].prev_hash == entries[0].record_hash
def test_naive_start_is_rejected() -> None:
with pytest.raises(ValueError):
EscalatingAlert.open("D-9", datetime(2026, 7, 16, 9, 0)) # no tzinfo
Frequently Asked Questions
Why does the evaluate function take the current time as an argument?
now in makes evaluate pure with respect to time, so a test can advance the clock instantly and assert the exact transition. It also lets a durable scheduler replay persisted alerts against a controlled instant on restart, which is what makes in-flight escalations survive a redeploy.
What stops a resolved alert from escalating again on a later tick?
evaluate and acknowledge early-return unless the alert is ACTIVE. Once an alert reaches a terminal status — ACKNOWLEDGED or BREACHED_FINAL — every subsequent tick is a no-op, so a resolved deadline can never be resurrected or promoted, even if the scheduler keeps ticking for days. The test that runs evaluate 99 hours after acknowledgment asserts exactly this.
Why record escalations as "system" but acknowledgments with a person's name?
Related
- Escalation Routing Workflows — the design this code implements, including the responsibility ladder and SLA model.
- Deadline Reminder Cadence Engines — the reminders whose unacknowledged tiers open an escalation.
- Building a Fallback Routing System for Patent Dockets — the fail-closed backstop pattern the top rung reuses.
- Core Docketing Architecture & Deadline Types — the append-only, hash-chained ledger contract.
↑ Back to Escalation Routing Workflows