Deadline Reminder Cadence Engines
A reminder cadence engine is the component that expands a single computed due date into a graduated sequence of reminders — for example a 90/60/30/14/7/3/1-day ladder — each fired on a business day, tuned to the deadline’s category, and cancelled the moment a responsible human acknowledges it. It decides when to warn, leaving whom to warn next to escalation and whether it worked to reporting.
The engine is the second stage of the Docket Alerting, Escalation & Compliance Reporting pipeline: it reads effective deadlines the rule engine has already rolled off non-working days and produces the timed reminders that channel dispatch delivers. Its output is the input to the Escalation Routing Workflows model — every reminder that fires and goes unacknowledged past its window is what triggers a climb up the responsibility ladder. This page specifies how to structure the ladder, expand it against a business-day calendar, schedule it idempotently, and keep it from degenerating into a wall of noise that trains people to ignore it.
Scope & Boundaries
A cadence engine has one job — turning a due date into a schedule of reminder instants — and it must not quietly acquire others. It does not compute the deadline (that is the rule engine’s job), it does not decide who receives the reminder (that is the responsibility ladder), and it does not deliver anything (that is channel dispatch). Keeping these separate is what makes the cadence auditable: a reviewer can look at the schedule the engine produced and confirm it matches policy without untangling delivery failures or escalation decisions.
The engine reads a single input contract — an effective due date, a deadline category, and an identifier — and emits a set of scheduled reminders. It never mutates the deadline. If the underlying due date changes because the rule engine recomputed it (a corrected office-action mail date, a restored priority claim), the engine regenerates the cadence from scratch and supersedes the old schedule with a recorded reason, rather than editing individual reminders in place. This regeneration-not-mutation rule is what lets the audit trail show the complete history of every schedule a deadline ever had.
Operational Action: Give the cadence engine exactly one input contract and one output contract. If you find it reading assignment tables or calling a mail server, the responsibilities have leaked — pull them back into the escalation and dispatch stages.
Anatomy of a Tiered Cadence
A tier is a single rung of the reminder ladder, defined by three properties: a lead time (how many business days before the due date it fires), an urgency level that governs which channels carry it, and an acknowledgment window (how long the recipient has to confirm before the tier is considered unanswered and eligible for escalation). A cadence is an ordered set of tiers, densest near the deadline. The 90/60/30/14/7/3/1-business-day ladder is a sensible default for high-value statutory deadlines because it front-loads a long, low-urgency runway and then compresses into daily, high-urgency touches in the final week when the cost of inaction is highest.
Lead times are expressed in business days, not calendar days, for a specific reason: a 3-calendar-day reminder that lands on a Friday for a Monday deadline gives the recipient effectively no working time, whereas a 3-business-day reminder guarantees three actual working days of runway. The engine therefore walks the same office calendar the rule engine used for its non-working-day roll under 37 CFR § 1.7(a) and PCT Rule 80.5, so that reminder math and deadline math share one definition of a working day. Two subtleties follow. First, a tier whose lead time exceeds the remaining runway (a deadline computed only 20 days out cannot have a 90-day reminder) is simply dropped, not fired retroactively. Second, a tier instant that itself lands on a weekend or holiday is shifted to the previous working day — earlier, never later — so a reminder is never scheduled for a day the office is closed and never pushed closer to the bar.
Designing the Tier Ladder by Deadline Category
Not every deadline deserves the same ladder. A non-extendable statutory bar and a maintenance-fee window with a six-month grace period carry very different consequences for a missed reminder, so the cadence is keyed to the deadline category defined in the Core Docketing Architecture & Deadline Types taxonomy. Tuning the ladder per category is what stops routine annuities from generating the same alert volume as an irreversible bar, which is the root cause of most reminder fatigue.
| Deadline category | Example | Suggested tier ladder (business days) | Rationale |
|---|---|---|---|
| Statutory, non-extendable | PCT 30-month national phase entry | 120 / 90 / 60 / 30 / 14 / 7 / 3 / 1 | Miss is generally unrecoverable; long runway plus daily final week |
| Procedural, extendable | USPTO office-action response, 37 CFR § 1.136(a) | 60 / 30 / 14 / 7 / 3 / 1 | Extensions exist but cost fees; still needs a firm final push |
| Fee, with grace period | USPTO maintenance fee (3.5/7.5/11.5 yr) | 180 / 90 / 30 / 7 | Six-month grace absorbs slips; early runway matters more than daily nags |
| Discretionary / internal | Client-reporting or docket-review target | 14 / 3 | Low external consequence; keep the ladder short to preserve signal |
The entity-tunable dimension matters as much as the category. A firm docketing for a risk-averse corporate client may widen a statutory ladder; a solo practitioner may narrow the fee ladder to reduce noise. The ladder is therefore configuration, not code — declared in a versioned table, citable to policy, and changeable without a redeploy. The concrete YAML form and the expansion function are worked through in Configuring Tiered Reminder Schedules for Patent Deadlines.
Operational Action: Key every cadence to a deadline category and store the ladders in a version-controlled configuration table with an effective_from date. A change to a reminder schedule is a policy change and should be reviewable as a diff, never a hard-coded constant edit.
Step-by-Step Implementation
Each step is independently testable — you can exercise the tier model, the business-day expansion, and the idempotency logic against a fixed calendar before wiring in a live scheduler.
Step 1 — Model tiers as validated configuration
Represent a tier and a cadence policy as Pydantic models so a malformed ladder is rejected at load time rather than producing a broken schedule at runtime. Validation at the boundary is the same discipline the ingestion layer applies to office data.
from __future__ import annotations
from pydantic import BaseModel, Field, field_validator
class ReminderTier(BaseModel):
"""One rung of a reminder ladder."""
lead_business_days: int = Field(gt=0)
urgency: str = Field(pattern=r"^(low|normal|high|critical)$")
ack_window_hours: int = Field(gt=0) # runway to acknowledge before escalation
class CadencePolicy(BaseModel):
"""A category-keyed ladder. Tiers are normalized to descending lead time."""
category: str
tiers: list[ReminderTier]
@field_validator("tiers")
@classmethod
def sort_and_dedupe(cls, tiers: list[ReminderTier]) -> list[ReminderTier]:
seen: set[int] = set()
for t in tiers:
if t.lead_business_days in seen:
raise ValueError(f"duplicate tier at {t.lead_business_days} days")
seen.add(t.lead_business_days)
# Densest-near-deadline order is produced by sorting descending.
return sorted(tiers, key=lambda t: t.lead_business_days, reverse=True)
Step 2 — Expand a deadline against a business-day calendar
The core operation walks the injected office calendar backward from the due date, counting only working days, then shifts any tier instant that still lands on a closed day to the previous working day. The calendar is injected, not imported, so a holiday-table update never touches this function.
from datetime import date, timedelta
from typing import Protocol
class BusinessCalendar(Protocol):
def is_working_day(self, d: date) -> bool: ...
def version(self) -> str: ...
def _minus_business_days(due: date, n: int, cal: BusinessCalendar) -> date:
cursor, counted = due, 0
while counted < n:
cursor -= timedelta(days=1)
if cal.is_working_day(cursor):
counted += 1
return cursor
def expand_cadence(deadline_id: str, due: date, policy: CadencePolicy,
cal: BusinessCalendar) -> list[dict[str, str]]:
"""Turn one deadline into concrete, business-day-aware reminder dates."""
schedule: list[dict[str, str]] = []
for tier in policy.tiers:
fire_on = _minus_business_days(due, tier.lead_business_days, cal)
if fire_on >= due: # runway shorter than this tier
continue
# A tier instant on a closed day shifts EARLIER, never toward the bar.
while not cal.is_working_day(fire_on):
fire_on -= timedelta(days=1)
schedule.append({
"deadline_id": deadline_id,
"lead_business_days": str(tier.lead_business_days),
"urgency": tier.urgency,
"fire_date": fire_on.isoformat(),
"ack_window_hours": str(tier.ack_window_hours),
"calendar_version": cal.version(),
})
return schedule
Step 3 — Schedule idempotently
Re-running the expansion — nightly reconciliation, a service restart, a manual re-sync — must never create a second copy of the same reminder. Derive a deterministic idempotency key from the deadline, the tier, and the fire date so a duplicate collapses onto the existing row instead of inserting a new one.
import hashlib
def reminder_key(deadline_id: str, lead_business_days: int, fire_date: str) -> str:
"""Stable across re-expansions of the same logical reminder."""
material = f"{deadline_id}|{lead_business_days}|{fire_date}"
return hashlib.sha256(material.encode("utf-8")).hexdigest()
The scheduler upserts each reminder keyed on reminder_key. If the due date is later recomputed upstream, the fire dates change, the keys change, and the old reminders are marked SUPPRESSED with the reason superseded_by_recompute — a regeneration, not an in-place edit, so the audit trail preserves every schedule the deadline ever carried.
Step 4 — Track acknowledgment and stop the cadence
Acknowledgment is the only signal that stops the ladder. When a responsible human confirms a reminder, every remaining SCHEDULED tier for that deadline is cancelled. Until then, each fired tier whose acknowledgment window lapses becomes eligible for escalation — the handoff into the Escalation Routing Workflows model.
from datetime import datetime, timezone
def on_acknowledged(deadline_id: str, reminders: list[dict[str, str]]) -> list[dict[str, str]]:
"""Cancel all still-pending tiers once the deadline is acknowledged."""
now = datetime.now(timezone.utc).isoformat()
for r in reminders:
if r.get("state") == "SCHEDULED":
r["state"] = "SUPPRESSED"
r["reason"] = "acknowledged"
r["suppressed_utc"] = now
return reminders
Preventing Reminder Storms
A cadence engine can fail in two opposite directions: too quiet (deadlines lapse) and too loud (people mute the system, and deadlines lapse anyway). The loud failure is subtler and just as dangerous, because a firm that has trained itself to ignore docket email has effectively disabled the alerting layer while still paying for it. Three controls keep volume proportionate.
First, deduplicate across deadlines that share an owner and a day. A paralegal with forty matters maturing in the same week should receive a consolidated digest per urgency level, not forty separate emails — the engine groups low- and normal-urgency reminders by recipient and day, while high- and critical-urgency reminders always send individually. Second, jitter the batch dispatch window. When hundreds of reminders come due at the same nightly run, releasing them in a randomized spread over the dispatch window prevents both a mail-server thundering herd and a recipient inbox flood, reusing the jitter discipline from the ingestion layer’s exponential backoff patterns. Third, suppress superseded tiers immediately. When a deadline is satisfied, withdrawn, or recomputed, its remaining reminders must be cancelled in the same transaction, so a paid maintenance fee never generates a “7 days remaining” alert the following week.
Operational Action: Track a per-recipient reminder-volume metric and alert on it. A sustained rise in low-urgency reminders per person per day is an early sign the ladders need tuning before staff start filtering docket mail to a folder they never open.
Edge Cases & Failure Modes
- Runway shorter than the ladder. A deadline computed only days before it is due (a late-ingested office action) cannot support a 90-day tier. Drop out-of-range tiers silently, but always fire at least the nearest in-range tier and flag the compressed runway for review — never leave a short-runway deadline with zero reminders.
- Recomputed due date mid-cadence. If the rule engine revises the effective date, regenerate the whole cadence and supersede the prior schedule with a recorded reason. Editing individual fire dates in place corrupts the audit trail and can leave orphaned reminders keyed to the old date.
- Tier instant on a long office closure. A tier landing inside a multi-day holiday (an EPO year-end closure, say) shifts backward past the entire block to the previous working day. Because the shift is earlier, it can collide with the adjacent tier; deduplicate so two tiers never fire the same reminder to the same recipient on the same day.
- Acknowledgment after escalation. A recipient may acknowledge a tier that has already escalated. The acknowledgment still cancels remaining tiers, but the escalation record stays in the ledger — the fact that it had to escalate is itself a diligence signal the reporting layer counts.
- Clock skew and DST. All fire instants are computed on dates in the office’s calendar and materialized to timezone-aware UTC only at dispatch, so a daylight-saving transition never shifts a reminder onto the wrong day. Never store a naive local datetime.
Verification & Regression Testing
Treat the ladder and the expansion function as code and pin their behavior against a fixed calendar, because a subtle off-by-one in the business-day walk silently moves every reminder.
import pytest
from datetime import date
class _FixedCalendar:
"""Weekdays are working days; a fixed holiday set for tests."""
_holidays = {date(2026, 12, 25), date(2026, 1, 1)}
def is_working_day(self, d: date) -> bool:
return d.weekday() < 5 and d not in self._holidays
def version(self) -> str:
return "test-cal-v1"
def test_business_day_walk_skips_weekend() -> None:
cal = _FixedCalendar()
# 3 business days before Wed 2026-07-15 is Fri 2026-07-10 (skips the weekend).
assert _minus_business_days(date(2026, 7, 15), 3, cal) == date(2026, 7, 10)
def test_out_of_range_tier_is_dropped() -> None:
cal = _FixedCalendar()
policy = CadencePolicy(category="statutory", tiers=[
ReminderTier(lead_business_days=90, urgency="low", ack_window_hours=48),
ReminderTier(lead_business_days=3, urgency="high", ack_window_hours=8),
])
# Only ~10 business days of runway: the 90-day tier must be dropped, 3-day kept.
schedule = expand_cadence("D-1", date(2026, 7, 15), policy, cal)
leads = {r["lead_business_days"] for r in schedule}
assert leads == {"3"}
def test_reminder_key_is_stable() -> None:
a = reminder_key("D-1", 30, "2026-06-03")
b = reminder_key("D-1", 30, "2026-06-03")
assert a == b # re-expansion collapses onto one row, no duplicate reminder
A corpus of known deadlines with hand-verified fire dates — including a month-boundary case and a holiday-block case — is the strongest guard against a change that quietly shifts the whole ladder by a day. Run it in CI before promoting any change to the expansion logic or the default ladders.
Operational Action Summary
Operational Action: Express every lead time in business days and share one office-calendar definition with the rule engine, so reminder math and deadline math never disagree about what a working day is.
Operational Action: Enforce a deterministic reminder key at the scheduler and regenerate-then-supersede on any upstream recompute, so re-runs and revised dates never duplicate or orphan a reminder.
Operational Action: Consolidate low-urgency reminders per recipient, jitter batch dispatch, and always send high- and critical-urgency reminders individually — proportionate volume is what keeps staff reading docket alerts at all.
Frequently Asked Questions
Why express reminder lead times in business days instead of calendar days?
What happens when a deadline is computed with less runway than the ladder needs?
How do you keep a large portfolio from drowning people in reminders?
Does acknowledging one reminder cancel the rest of the ladder?
Related
- Docket Alerting, Escalation & Compliance Reporting — the pipeline this cadence engine feeds.
- Escalation Routing Workflows — where unacknowledged reminders climb a responsibility ladder.
- Configuring Tiered Reminder Schedules for Patent Deadlines — the concrete YAML ladder and expansion function.
- Building a Docket Escalation Ladder in Python — the SLA-driven ladder an unacknowledged reminder triggers.
- Core Docketing Architecture & Deadline Types — the deadline categories that key each cadence.
↑ Back to Docket Alerting, Escalation & Compliance Reporting