Configuring Tiered Reminder Schedules for Patent Deadlines
A tiered reminder schedule is a declarative ladder of lead times — for example 90, 60, 30, 14, 7, 3, and 1 day before a deadline — stored as configuration and expanded at runtime into concrete reminder dates that never fall on a day the office is closed. Keeping the ladder in configuration rather than code lets a docketing team change a policy with a reviewed diff instead of a redeploy.
This is the concrete recipe behind the Deadline Reminder Cadence Engines framework: a real YAML ladder, cited to its governing rules, and a single, focused Python function that turns one effective due date into a dated set of reminders. The reminders it produces are what channel dispatch delivers and what the Escalation Routing Workflows model escalates when they go unacknowledged.
The Configuration-First Principle
A reminder ladder is a policy decision, not an implementation detail. How far ahead a firm starts warning about a non-extendable statutory bar, and how aggressively it presses in the final week, reflects that firm’s risk tolerance and its malpractice carrier’s expectations — not something that should be buried in a function body. Expressing the ladder as configuration gives three concrete benefits: a change is a reviewable diff that patent counsel can sign off on, the ladder can carry an effective_from date so a policy change never retroactively rewrites schedules already computed, and every reminder date can be traced back to the exact configuration version that produced it.
The convention below expresses lead times in calendar days, the way most practitioners describe a reminder (“remind me 30 days out”), and then shifts any resulting date that lands on a non-working day backward to the previous working day. Shifting earlier rather than later is deliberate: a reminder must never be pushed closer to the bar than the policy intends. This differs from a business-day-count ladder — both are valid, and the choice is itself configuration.
A Worked Reminder Schedule
The ladder is keyed to a deadline category so a routine window and an irreversible bar do not share a cadence. The snippet below configures the schedule for a USPTO office-action response — a procedural, extendable deadline under 37 CFR § 1.134, extendable to six months under 37 CFR § 1.136(a) — and pins the non-working-day roll to the federal calendar cited in 37 CFR § 1.7(a).
# config/reminders/uspto_oa_response.yaml
# Tiered reminder schedule for a USPTO office-action response.
# Deadline source: 37 CFR 1.134 https://www.ecfr.gov/current/title-37/section-1.134
# Extension source: 37 CFR 1.136(a) https://www.ecfr.gov/current/title-37/section-1.136
# Roll calendar: 37 CFR 1.7(a) https://www.ecfr.gov/current/title-37/section-1.7
category: procedural_extendable
effective_from: "2026-01-01"
roll_calendar: US_DC_FEDERAL # weekends + DC federal holidays
roll_direction: previous_working_day # shift closed-day reminders EARLIER, never later
tiers:
- lead_calendar_days: 90
urgency: low
channels: [email]
ack_window_hours: 72
- lead_calendar_days: 60
urgency: low
channels: [email]
ack_window_hours: 72
- lead_calendar_days: 30
urgency: normal
channels: [email, in_app]
ack_window_hours: 48
- lead_calendar_days: 14
urgency: normal
channels: [in_app]
ack_window_hours: 24
- lead_calendar_days: 7
urgency: high
channels: [email, sms]
ack_window_hours: 12
- lead_calendar_days: 3
urgency: high
channels: [sms]
ack_window_hours: 8
- lead_calendar_days: 1
urgency: critical
channels: [sms, page]
ack_window_hours: 4
Applied to a response due Monday 14 September 2026, this ladder expands to the schedule below. Two tiers land on closed days and shift earlier, and the final two both resolve to Friday 11 September — a collision the engine deduplicates into a single reminder.
Expanding the Schedule in Python
The expansion is one focused, side-effect-free function: it loads the validated ladder, subtracts each tier’s calendar-day lead from the due date, and rolls any closed-day result backward to the previous working day. Timezone-aware handling uses standard-library zoneinfo, and the office calendar is injected so a holiday-table update never edits this logic.
from __future__ import annotations
from datetime import date, timedelta
from typing import Protocol
from pydantic import BaseModel, Field
class Tier(BaseModel):
lead_calendar_days: int = Field(gt=0)
urgency: str = Field(pattern=r"^(low|normal|high|critical)$")
channels: list[str]
ack_window_hours: int = Field(gt=0)
class ReminderSchedule(BaseModel):
category: str
roll_direction: str = Field(pattern=r"^previous_working_day$")
tiers: list[Tier]
class WorkingCalendar(Protocol):
def is_working_day(self, d: date) -> bool: ...
def version(self) -> str: ...
def expand_reminders(deadline_id: str, effective_due: date,
schedule: ReminderSchedule,
calendar: WorkingCalendar) -> list[dict[str, object]]:
"""Expand one due date into concrete, business-day-aware reminder rows.
Subtracts each tier's calendar-day lead, then rolls any closed-day result
EARLIER to the previous working day. Tiers colliding on one date after the
roll are deduplicated, keeping the more urgent tier's channels.
"""
by_date: dict[date, dict[str, object]] = {}
for tier in schedule.tiers:
fire = effective_due - timedelta(days=tier.lead_calendar_days)
if fire >= effective_due: # guard: non-positive lead
continue
while not calendar.is_working_day(fire): # roll backward off closed days
fire -= timedelta(days=1)
row = {
"deadline_id": deadline_id,
"lead_calendar_days": tier.lead_calendar_days,
"urgency": tier.urgency,
"channels": tier.channels,
"fire_date": fire.isoformat(),
"ack_window_hours": tier.ack_window_hours,
"calendar_version": calendar.version(),
}
# Collision: keep the highest-urgency tier on a shared date.
rank = {"low": 0, "normal": 1, "high": 2, "critical": 3}
existing = by_date.get(fire)
if existing is None or rank[tier.urgency] > rank[str(existing["urgency"])]:
by_date[fire] = row
return [by_date[d] for d in sorted(by_date)]
The function is pure — given the same due date, schedule, and calendar it always returns the same rows — which is exactly what makes the output testable and auditable. The scheduler that consumes these rows applies the deterministic idempotency key from the parent Deadline Reminder Cadence Engines framework so a re-expansion never duplicates a reminder.
Operational Action: Store the ladder YAML in version control beside your deadline rules, pin its effective_from, and record the resolved calendar_version on every reminder row so any historical fire date can be reproduced from the exact configuration that produced it.
Handling the Shift and Dedup Edge Cases
Two situations recur and both are visible in the worked example. When a tier lands on a weekend or an office holiday, rolling backward keeps the recipient’s working runway intact — the 30-day tier’s Saturday becomes the preceding Friday, and the 7-day tier’s Labor Day Monday becomes the preceding Friday. Rolling forward instead would silently compress the runway toward the bar, which is the opposite of what a reminder is for.
The dedup case is where two tiers resolve to the same working day after the roll. In the worked example the 1-day tier’s Sunday and the 3-day tier’s Friday both land on 11 September. Sending two separate reminders to the same person on the same day trains them to skim, so the function keeps a single reminder and preserves the higher-urgency tier’s channels — the merged reminder goes out as SMS-plus-page, not just SMS. The collapsed tier is still recorded in the audit ledger as suppressed-by-merge, so the schedule history remains complete.
Verification
Because a one-day error in the roll silently moves reminders, pin the expansion against a fixed calendar with hand-verified dates, including the weekend shift, the holiday shift, and the collision.
import pytest
from datetime import date
class _USFixture:
_holidays = {date(2026, 9, 7)} # Labor Day 2026
def is_working_day(self, d: date) -> bool:
return d.weekday() < 5 and d not in self._holidays
def version(self) -> str:
return "us-fed-2026.1"
def _schedule() -> ReminderSchedule:
return ReminderSchedule(category="procedural_extendable",
roll_direction="previous_working_day", tiers=[
Tier(lead_calendar_days=30, urgency="normal", channels=["email"], ack_window_hours=48),
Tier(lead_calendar_days=7, urgency="high", channels=["sms"], ack_window_hours=12),
Tier(lead_calendar_days=3, urgency="high", channels=["sms"], ack_window_hours=8),
Tier(lead_calendar_days=1, urgency="critical", channels=["sms", "page"], ack_window_hours=4),
])
def test_weekend_and_holiday_rolls() -> None:
rows = expand_reminders("D-1", date(2026, 9, 14), _schedule(), _USFixture())
fired = {r["lead_calendar_days"]: r["fire_date"] for r in rows}
assert fired[30] == "2026-08-14" # Sat 15 Aug rolled to Fri 14 Aug
assert fired[7] == "2026-09-04" # Labor Day rolled to Fri 4 Sep
def test_collision_is_deduplicated_to_highest_urgency() -> None:
rows = expand_reminders("D-1", date(2026, 9, 14), _schedule(), _USFixture())
on_11 = [r for r in rows if r["fire_date"] == "2026-09-11"]
assert len(on_11) == 1 # T-3 and T-1 merged
assert on_11[0]["urgency"] == "critical" # kept the more urgent tier
assert "page" in on_11[0]["channels"]
Frequently Asked Questions
Should a reminder that lands on a weekend roll forward or backward?
What happens when two tiers resolve to the same date after rolling?
Why keep the reminder ladder in YAML instead of hard-coding it?
Related
- Deadline Reminder Cadence Engines — the framework this schedule implements, including idempotent scheduling.
- Escalation Routing Workflows — where an unacknowledged reminder from this ladder is escalated.
- Core Docketing Architecture & Deadline Types — the deadline categories each ladder is keyed to.
- Docket Alerting, Escalation & Compliance Reporting — the pipeline these reminders feed.
↑ Back to Deadline Reminder Cadence Engines