Deadline Tracking & Escalation for Public Records Requests
Within Intake & Routing Workflows, deadline tracking and escalation is the layer that turns a receipt timestamp into an enforceable, defensible obligation. Once a request is admitted and the statutory clock has started, the agency owns a countdown that the law will not pause on its behalf: the federal Freedom of Information Act mandates a determination within 20 business days of receipt by the proper component under 5 U.S.C. § 552(a)(6)(A)(i), and every state open-records act imposes its own window on its own basis. Missing that window is not a latency metric — it is a statutory failure that a requester can litigate, an inspector general can cite, and a court can compel. This reference is the engineering blueprint for the countdown: computing each deadline correctly per jurisdiction, moving it deterministically when a tolling event pauses the clock, warning the right people while there is still time to act, and recording every one of those changes in an append-only log that proves the agency knew and responded.
The failure this system is built to prevent is the silent breach — a request that quietly ages past its window because no clock was watching it, no alert fired, and no record exists of who should have acted. A deadline tracker earns its keep by being deterministic (the same receipt date and jurisdiction always yield the same deadline), transparent (every day-remaining figure is reproducible from logged facts), and pre-emptive (it escalates before T-0, not after). The sections below build each of those properties in turn, then wire them into a runnable Python engine.
The Deadline Clock as a First-Class Domain Object
The most common defect in home-grown tracking systems is treating the deadline as a derived display value — computed on the fly whenever a dashboard renders, never stored, never audited. That design cannot answer the only question that matters under review: what did the agency believe the deadline was, and when did it know? A defensible system models the deadline as a first-class domain object with its own identity, its own state, and its own event history. It carries the receipt date, the governing jurisdiction, the computed due date, an accumulator of tolled days, and a flag for whether the unusual-circumstances extension has been applied. Every mutation to that object — the initial computation, a tolling pause, a resume, an extension — is an explicit, logged event, never an in-place recalculation that erases what came before.
Storing the deadline rather than recomputing it also decouples the clock from wall-clock drift and code changes. If next quarter someone corrects a holiday table or fixes a rounding bug, a stored-and-logged deadline preserves the figure the agency actually acted on, while a purely derived value silently rewrites history. The clock object is the anchor the rest of the pipeline hangs from: priority scoring reads its days-remaining, escalation reads its tier, and retention reads its disposition date.
Why the clock must start at ingestion
The statutory clock starts on receipt by the proper component, not when an analyst opens the request. If deadline computation is deferred to the first time a human touches the file, the recorded due date will be systematically late — understating the true obligation by exactly the queue latency. That is why the clock is created in the same transition that admits the request upstream, and why the State Law Compliance Frameworks reference is consulted at that moment to resolve the correct window and basis. A deadline computed a day late is worse than no deadline at all, because it manufactures false confidence.
Per-Jurisdiction Clocks and the Business-Day Problem
No single constant models public records deadlines. Statutes diverge on two independent axes: the length of the window and the basis on which it is counted. Federal FOIA runs 20 business days; the California Public Records Act expects a determination within 10 calendar days under Gov. Code § 7922.535; the Texas Public Information Act runs 10 business days with an Attorney General referral track; New York FOIL requires acknowledgment within 5 business days. A system that hard-codes “20 days” as a timedelta will silently breach every calendar-day jurisdiction it touches and every business-day jurisdiction whose window differs.
The correct model is a lookup keyed by jurisdiction that returns both the basis (business or calendar) and the count, and a computation path for each basis. Calendar-day math is trivial addition. Business-day math is where the subtlety lives: the window excludes Saturdays, Sundays, and legal public holidays, so advancing twenty business days from a receipt in mid-November — with Veterans Day, Thanksgiving, and the surrounding weekends in the path — lands weeks later on the calendar than a naive +20 days would suggest. Getting this arithmetic right, with a maintained federal holiday calendar and correct weekend roll-over, is involved enough to warrant its own treatment in Computing Business-Day FOIA Deadlines with Federal Holiday Calendars. For agencies operating under a specific state act, the worked example in Calculating California Public Records Act Deadlines in Python shows how a calendar-day statute with its own holiday roll-over rules diverges from the federal business-day pattern.
Tolling: moving time explicitly
Statutes permit the clock to pause under defined conditions — a request awaiting payment of assessed fees, a request needing clarification from the requester, or a consultation with another agency. Tolling is the single most error-prone part of deadline tracking because it is tempting to implement as a silent recalculation. It must instead be modeled as an explicit pair of logged events: a TOLL_PAUSE records the date the clock stopped and why, and a TOLL_RESUME records the date it restarted and shifts the due date forward by exactly the elapsed span. Between those events the days-remaining figure is undefined rather than counting down, and escalation is suspended. Encoding tolling as movable, attributable events — never as a quiet subtraction — is what lets a records officer defend the final due date line by line.
Escalation Before the Breach
Tracking a deadline is worthless if the countdown reaches zero unwatched. Escalation is the active half of the system: as the days-remaining figure crosses defined thresholds, the tracker notifies progressively more senior owners so the request cannot age out through simple inattention. A workable ladder fires at T-7 to the assigned records owner, at T-3 to the unit supervisor, at T-1 to the agency records officer, and at T-0 records the breach and assembles the evidence a review will demand. The thresholds are policy, not physics — an agency under a 5-business-day acknowledgment rule will compress them — but the shape is constant: earlier, quieter nudges give way to louder, higher escalations as the margin evaporates.
Two properties make escalation trustworthy. First, alerts must be idempotent: a tier fires exactly once, so a monitor that runs every fifteen minutes does not page the records officer ninety-six times a day. Second, the alert history must be audit-logged, because the record that a T-3 warning was delivered to a named supervisor on a specific date is precisely what distinguishes a defensible near-miss from negligence. The design of that threshold ladder, the idempotent scheduling that prevents alert storms, and the escalation to records officers are developed in full in Automated Escalation Alerts Before a FOIA Deadline Breach.
The audit lane underneath everything
Every deadline change — the initial computation, each tolling event, the extension, each escalation, and the final disposition — is appended to an immutable log keyed by request. This is not incidental telemetry; it is the primary evidentiary artifact the whole system exists to produce. The append-only, hash-chained store and the retention rules that govern it are specified in Audit Logging Architecture, and the deadline tracker writes into it rather than inventing its own. When a requester alleges the agency ignored their request, the audit lane is what reconstructs, to the day, that the clock started on receipt, paused only for a logged fee tolling, resumed on payment, escalated on schedule, and closed within the window.
Reference Implementation
The engine below models the deadline as a first-class object with per-jurisdiction computation, explicit tolling, the unusual-circumstances extension, and structured JSON audit logging. It uses only the standard library so it runs anywhere, and it keeps a small holiday set inline; a production deployment swaps that set for a maintained calendar and writes the audit stream to the append-only sink rather than stdout. The state guarantees — deadlines never silently recomputed, tolling always paired and logged, escalation derived deterministically from days-remaining — are identical to the production system.
"""
deadline_tracker.py
Statutory deadline tracking and tiered escalation for public records requests.
Deterministic, per-jurisdiction, tolling-aware, and audit-ready.
"""
import json
import logging
import sys
from dataclasses import dataclass, field
from datetime import date, timedelta
from enum import Enum
from typing import Optional
# ---------------------------------------------------------------------------
# Structured audit logging.
# AU-2 / AU-12: every deadline change emits one machine-parseable audit event.
# ---------------------------------------------------------------------------
class JSONFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
entry = {
"timestamp": self.formatTime(record, self.datefmt),
"level": record.levelname,
"message": record.getMessage(),
"request_id": getattr(record, "request_id", None),
"event": getattr(record, "event", None),
"days_remaining": getattr(record, "days_remaining", None),
"deadline": getattr(record, "deadline", None),
}
return json.dumps(entry)
audit = logging.getLogger("foia_deadline_audit")
audit.setLevel(logging.INFO)
_handler = logging.StreamHandler(sys.stdout)
_handler.setFormatter(JSONFormatter())
audit.addHandler(_handler)
# ---------------------------------------------------------------------------
# Federal legal public holidays. 5 U.S.C. § 552 excludes Saturdays, Sundays,
# and legal public holidays from the business-day response window; a real
# deployment loads a maintained multi-year calendar here.
# ---------------------------------------------------------------------------
FEDERAL_HOLIDAYS = frozenset({
date(2026, 1, 1), date(2026, 1, 19), date(2026, 2, 16),
date(2026, 5, 25), date(2026, 6, 19), date(2026, 7, 3),
date(2026, 9, 7), date(2026, 10, 12), date(2026, 11, 11),
date(2026, 11, 26), date(2026, 12, 25),
})
def is_business_day(day: date, holidays=FEDERAL_HOLIDAYS) -> bool:
# Monday=0 .. Sunday=6; weekends and legal public holidays never count.
return day.weekday() < 5 and day not in holidays
def add_business_days(start: date, count: int, holidays=FEDERAL_HOLIDAYS) -> date:
"""Advance `count` business days from a receipt date, skipping non-days."""
cursor, remaining = start, count
while remaining > 0:
cursor += timedelta(days=1)
if is_business_day(cursor, holidays):
remaining -= 1
return cursor
def business_days_between(start: date, end: date, holidays=FEDERAL_HOLIDAYS) -> int:
"""Count business days strictly after `start` up to and including `end`."""
if end <= start:
return 0
days, cursor = 0, start
while cursor < end:
cursor += timedelta(days=1)
if is_business_day(cursor, holidays):
days += 1
return days
class Jurisdiction(str, Enum):
# Window length and basis differ per governing statute.
FEDERAL = "FEDERAL" # 20 business days, 5 U.S.C. § 552(a)(6)(A)(i)
CALIFORNIA = "CALIFORNIA" # 10 calendar days, Gov. Code § 7922.535
TEXAS = "TEXAS" # 10 business days, Tex. Gov't Code § 552.221
# (basis, count) per jurisdiction — never a single shared constant.
WINDOWS = {
Jurisdiction.FEDERAL: ("business", 20),
Jurisdiction.CALIFORNIA: ("calendar", 10),
Jurisdiction.TEXAS: ("business", 10),
}
# Escalation thresholds keyed to business days remaining.
ESCALATION_TIERS = (
(0, "T0_BREACH"),
(1, "T1_RECORDS_OFFICER"),
(3, "T3_SUPERVISOR"),
(7, "T7_OWNER"),
)
def escalation_tier(days_remaining: Optional[int]) -> str:
"""Map days-remaining onto the escalation ladder; None means the clock is paused."""
if days_remaining is None:
return "PAUSED"
for threshold, tier in ESCALATION_TIERS:
if days_remaining <= threshold:
return tier
return "ON_TRACK"
@dataclass
class DeadlineClock:
request_id: str
received_on: date
jurisdiction: Jurisdiction
deadline: date = field(init=False)
tolled_days: int = 0
tolling_started: Optional[date] = None
extended: bool = False
def __post_init__(self) -> None:
# Compute at receipt so the recorded clock can never start late.
self.deadline = self._compute(self.received_on)
self._log("CLOCK_START", self.received_on)
def _compute(self, anchor: date) -> date:
basis, count = WINDOWS[self.jurisdiction]
if basis == "business":
return add_business_days(anchor, count)
return anchor + timedelta(days=count)
def toll_pause(self, on: date) -> None:
# Tolling pauses the clock (fee request, clarification, consultation).
if self.tolling_started is not None:
raise ValueError("clock is already tolled")
self.tolling_started = on
self._log("TOLL_PAUSE", on)
def toll_resume(self, on: date) -> None:
if self.tolling_started is None:
raise ValueError("clock is not tolled")
paused = max((on - self.tolling_started).days, 0)
self.tolled_days += paused
self.deadline += timedelta(days=paused) # move time explicitly
self.tolling_started = None
self._log("TOLL_RESUME", on)
def apply_unusual_circumstances(self, on: date) -> None:
# § 552(a)(6)(B): up to 10 extra working days for unusual circumstances.
if self.extended:
raise ValueError("extension already applied")
self.deadline = add_business_days(self.deadline, 10)
self.extended = True
self._log("EXTENSION", on)
def days_remaining(self, as_of: date) -> Optional[int]:
if self.tolling_started is not None:
return None # clock paused; days-remaining is undefined
basis, _ = WINDOWS[self.jurisdiction]
if basis == "business":
return business_days_between(as_of, self.deadline)
return max((self.deadline - as_of).days, 0)
def _log(self, event: str, on: date) -> None:
audit.info(
f"deadline {event.lower()} for {self.request_id}",
extra={
"request_id": self.request_id,
"event": event,
"days_remaining": self.days_remaining(on)
if event != "CLOCK_START" else None,
"deadline": self.deadline.isoformat(),
},
)
def evaluate(clock: DeadlineClock, as_of: date) -> dict:
"""Return the current tier and escalate deterministically from days-remaining."""
try:
remaining = clock.days_remaining(as_of)
tier = escalation_tier(remaining)
audit.info(
f"evaluated {clock.request_id}: {tier}",
extra={"request_id": clock.request_id, "event": f"ESCALATE_{tier}",
"days_remaining": remaining, "deadline": clock.deadline.isoformat()},
)
return {"request_id": clock.request_id, "tier": tier,
"days_remaining": remaining, "deadline": clock.deadline.isoformat()}
except Exception as exc: # never let a monitor crash swallow a countdown
audit.error(
f"evaluation failed for {clock.request_id}: {exc}",
extra={"request_id": clock.request_id, "event": "EVAL_FAILURE"},
)
raise
if __name__ == "__main__":
clock = DeadlineClock("REQ-2026-0412", date(2026, 6, 22), Jurisdiction.FEDERAL)
clock.toll_pause(date(2026, 6, 29)) # requester asked to clarify scope
clock.toll_resume(date(2026, 7, 6)) # clarification received a week later
print(json.dumps(evaluate(clock, date(2026, 7, 15)), indent=2))
Running the module emits one JSON audit line for the clock start, one for each tolling event, and one for the evaluation, then prints the current tier and due date. Because tolling shifts the deadline by an explicit, logged span and the tier is derived purely from days-remaining, the entire countdown is reproducible from the log — the property that makes it defensible when a requester or a court asks the agency to show its work.
Operational Resilience
A deadline tracker is only as reliable as the monitor that drives it, and that monitor runs in a hostile operational reality: it can crash mid-sweep, run twice from overlapping cron invocations, or fall behind during an outage. Each of those must fail toward preservation. Idempotent escalation means a double-run re-sends nothing already sent. A monitor that misses a window because the host was down must, on recovery, evaluate every open clock against the current date and fire whatever tiers are now due rather than assuming yesterday’s sweep covered them — a request that crossed two thresholds during the outage should escalate to the higher tier immediately. Feeding these evaluations through the durable brokering described in Async Queue Management keeps the escalation workload decoupled from the monitor’s own uptime, so a slow notification channel never delays the next clock’s evaluation.
The final guarantee is that the clock never advances a state whose audit write has not committed. A tolling resume that shifts the deadline but fails to log the shift is a silent history rewrite — exactly the failure mode the whole design exists to prevent. Commit the audit event first, then expose the new due date; on a partition, buffer locally and reconcile, but never let the visible deadline diverge from the logged one.
Compliance Verification Checklist
Confirm these controls before a deadline tracker governs live statutory obligations.
FAQ
When exactly does the deadline clock start, and why store it instead of recomputing?
The statutory clock starts on receipt by the proper agency component, not when an analyst first opens the request. Computing the deadline at ingestion and storing it prevents the systematic lateness that deferred computation introduces, and it preserves the figure the agency actually acted on even if a holiday table or a rounding rule is later corrected. A recomputed-on-render deadline silently rewrites history; a stored-and-logged one survives review because it is anchored to the receipt date and the jurisdiction resolved at that moment.
How should tolling events be represented so the final due date is defensible?
As explicit, attributable, paired events — never a silent subtraction. A TOLL_PAUSE logs the date and reason the clock stopped (a fee request, a clarification, an inter-agency consultation); a TOLL_RESUME logs the restart and shifts the due date forward by exactly the elapsed span. While the clock is paused the days-remaining figure is undefined and escalation is suspended. Because each movement of time is logged, a records officer can reconstruct the due date line by line and show that every pause was authorized.
Why model each jurisdiction separately instead of using one deadline constant?
Statutes diverge on both the window length and the counting basis. Federal FOIA runs 20 business days, California expects a determination in 10 calendar days, Texas runs 10 business days, and New York FOIL requires a 5-business-day acknowledgment. A single constant silently breaches at least one of these. The State Law Compliance Frameworks reference encodes each window and its basis so the clock resolves the correct computation at receipt rather than approximating.
What keeps an escalation monitor from paging staff dozens of times a day?
Idempotent tiers. Each threshold — T-7, T-3, T-1, T-0 — fires exactly once per request, guarded by a durable record that the tier has already been sent. A monitor that sweeps every fifteen minutes therefore evaluates the countdown continuously but notifies only on the transition into a tier, not on every pass. The delivery of each tier is itself logged, so the audit trail proves the right person was warned on the right day.
How does the deadline tracker fit the rest of the intake pipeline?
It sits directly downstream of admission: the clock is created in the same transition that admits the request through Intake & Routing Workflows, reads its window from the state-law reference, writes every change into the shared append-only store from Audit Logging Architecture, and exposes days-remaining to priority scoring and escalation. It owns the countdown; the surrounding pipeline owns capture, routing, and disposition.
Related
- Intake & Routing Workflows — the parent control plane that admits a request and creates its deadline clock in the same transition.
- Computing Business-Day FOIA Deadlines with Federal Holiday Calendars — the weekend and holiday roll-over math behind the 20-business-day window.
- Automated Escalation Alerts Before a FOIA Deadline Breach — the threshold ladder and idempotent notification scheduling that warn before T-0.
- State Law Compliance Frameworks — per-jurisdiction windows and bases the clock resolves at receipt.
- Audit Logging Architecture — the append-only, hash-chained store every deadline change is written to.