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.

Worked expansion of a tiered reminder ladder to concrete dates A USPTO office-action response is due Monday 14 September 2026. Seven reminder tiers, each a lead time in calendar days, map to concrete fire dates. The 90-day tier fires Tuesday 16 June, the 60-day tier Thursday 16 July, the 30-day tier would fall on Saturday 15 August and is shifted earlier to Friday 14 August, the 14-day tier fires Monday 31 August, the 7-day tier would fall on Labor Day Monday 7 September and is shifted earlier to Friday 4 September, the 3-day tier fires Friday 11 September, and the 1-day tier would fall on Sunday 13 September and is shifted to Friday 11 September where it collides with the 3-day tier and is deduplicated. Shifted rows are marked with a gold border. USPTO office-action response — due Mon 14 Sep 2026 procedural, extendable · 37 CFR 1.134 · roll: 37 CFR 1.7(a) T-90d Tue 16 Jun 2026 email T-60d Thu 16 Jul 2026 email T-30d Fri 14 Aug 2026 email + in-app shifted earlier Sat 15 Aug → Fri 14 Aug T-14d Mon 31 Aug 2026 in-app T-7d Fri 4 Sep 2026 email + SMS holiday shift Labor Day 7 Sep → Fri 4 Sep T-3d Fri 11 Sep 2026 SMS T-1d Fri 11 Sep 2026 SMS + page shift + dedup Sun 13 Sep → Fri 11 Sep, merged

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?
Backward, to the previous working day. Rolling forward would push the reminder closer to the deadline than the policy intends and shrink the recipient's working runway — the opposite of a reminder's purpose. In the worked example the 30-day tier's Saturday becomes the preceding Friday and the 7-day tier's Labor Day Monday becomes the preceding Friday, so the recipient still gets the intended notice on a day the office is open.
What happens when two tiers resolve to the same date after rolling?
They are deduplicated into a single reminder that keeps the higher-urgency tier's channels. In the example the 1-day tier's Sunday and the 3-day tier's Friday both land on 11 September, so one reminder goes out as SMS-plus-page rather than two separate messages to the same person on the same day. The collapsed tier is still recorded in the ledger as suppressed-by-merge, keeping the schedule history complete while avoiding the noise that trains people to skim alerts.
Why keep the reminder ladder in YAML instead of hard-coding it?
Because the ladder is a policy decision a firm and its malpractice carrier care about, not an implementation detail. Configuration makes a change a reviewable diff patent counsel can approve, lets the ladder carry an effective_from date so a policy change never retroactively rewrites already-computed schedules, and lets every reminder date be traced to the exact configuration version that produced it. Editing a constant in a function body gives none of those audit properties.

↑ Back to Deadline Reminder Cadence Engines