Escalating Missed Notice Windows to Manual Review

A tiered reminder ladder does its job right up until the moment it fails: the owner is on leave, the manager’s inbox rule swallowed the escalation, and the notice window for a renewal option quietly crosses zero with nobody having acknowledged it. The Critical Date Alerting layer is built to make that rare, but “rare” is not “never,” and when it happens the machine must not simply go quiet. This page covers the one narrow thing that happens after the last reminder tier has fired and gone unanswered: converting a breached or unacknowledged critical date into a concrete, owned, time-boxed review task and handing it to a human triage queue with enough context to act. It is the terminal rung — not another reminder, but the point where automation admits it cannot resolve the date and routes it to a person.

The distinction from the alerting ladder matters. The ladder decides who to tell and how loudly while there is still runway; this page begins where that runway is gone. A missed option deadline may already carry legal exposure, so the handoff cannot be a bare “check on lease L-4471” line in a spreadsheet. It has to arrive as a structured work item that says exactly which canonical field lapsed, which amendment set the deadline, how that deadline was computed, why the ladder failed to get an acknowledgement, and how long the reviewer has before the situation gets worse.

Architectural context

This escalation is deliberately the same seam the platform already uses for machine-unresolvable work. When a low-confidence extraction cannot be trusted, it diverts through Fallback Routing Logic into a manual-review queue; a breached critical date lands in that same queue by the same contract, so operations triages parsing failures and missed deadlines in one place rather than two. The review task itself reads exclusively from already-validated data: the due date, owner, and manager come from the watched CriticalDate, whose values trace back to the canonical lease data models, and any derived deadline (a rent-review date on a CPI cycle, a notice window measured back from an option date) carries a pointer to the escalation formula mapping rule that produced it. This layer never recomputes those values; it packages them, stamps provenance, assigns an SLA, and enqueues — idempotently, so one missed date is exactly one review item no matter how many times the daily sweep re-runs.

From breached critical date to an idempotent review task in the manual-review queue A critical date that is either breached, meaning the days-remaining count has crossed below zero, or has its final reminder tier left unacknowledged past the acknowledgement SLA, enters an escalation decision. The decision first computes a deterministic dedup key from the lease id, date type, and a review discriminator; if a task with that key already exists the run is a no-op, guaranteeing one missed date becomes one review item. Otherwise it assembles a review task carrying provenance: which canonical fields lapsed, the source amendment that set the deadline, the computed deadline and the escalation-formula rule behind it, plus a severity and an SLA due time. That task is enqueued into the shared manual-review queue, the same seam fallback routing uses for low-confidence extractions. A reviewer resolves the task, which closes the loop by either re-arming the alert schedule against a corrected deadline or marking the date genuinely lapsed for the audit trail. Breached or unacked date T-minus < 0, or top tier · no ack dedup key exists? yes · no-op (one date = one task) Build review task · provenance · canonical fields that lapsed · source amendment id · computed deadline + formula rule · breach reason (why ack failed) · severity + SLA due time · deterministic idempotency key Manual-review queue shared fallback-routing seam Reviewer resolves re-arm schedule or mark lapsed no triage · within SLA
The terminal handoff: a breached or unacknowledged date is deduplicated to one task, packaged with provenance and an SLA, enqueued into the shared review queue, then resolved by a human who re-arms or closes it.

Trigger conditions, severity, and SLA

Not every escalation is equally urgent, and the triage queue is useless if a lapsed insurance certificate and a soft near-breach on a distant rent review arrive with identical priority. The mapping below is the contract the builder encodes: each trigger produces a severity, an assignee, and a service-level target that the queue enforces. Assignees are roles, not people, so the schedule survives staff changes.

Trigger condition Severity Assignee & SLA Action taken
Option / renewal deadline passed unacknowledged Critical Senior asset manager · 4h File task, flag legal-watch, freeze auto-actions on the lease
Insurance or estoppel deadline passed Critical Compliance owner · 4h File task, attach certificate provenance, notify risk
Final tier (T-7) unacknowledged past ack SLA, deadline imminent High Portfolio manager · 1 business day File task, re-ping owner in parallel
Notice window closed with no acknowledgement High Asset manager · 1 business day File task, record whether option was preserved
Near-breach: within final tier, ack SLA elapsed, deadline still future Medium Owner + manager · 2 business days File task, keep ladder live as backstop
Suspected false positive (stale sweep / timezone) Low Ops triage · 3 business days File task, verify computed deadline before acting

Severity keys off how much runway remains and what is at stake if it is gone: a passed option deadline is Critical because the tenant’s right may already be forfeited, while a near-breach with the deadline still a fortnight out is Medium because a human can still catch it. The SLA is not decoration — it is the second clock. The first clock (the deadline) may have already expired; the SLA governs how fast a person must now look at the wreckage.

The builder consumes the exact CriticalDate and AlertState the alerting evaluator already maintains — this layer adds no new schema for the date itself. It produces a ReviewTask whose primary property is a deterministic idempotency key, so enqueuing the same breach twice is a no-op. Provenance is a first-class nested model, not a free-text note, because a reviewer acting under a 4-hour SLA cannot afford to go spelunking through source documents to reconstruct why the alert exists.

from __future__ import annotations

from datetime import date, datetime, timedelta, timezone
from enum import Enum
from typing import Optional

from pydantic import BaseModel, Field, field_validator


class BreachType(str, Enum):
    DEADLINE_PASSED = "deadline_passed"       # days_left < 0, unacknowledged
    FINAL_TIER_UNACKED = "final_tier_unacked"  # nearest tier fired, ack SLA elapsed


class Severity(str, Enum):
    CRITICAL = "critical"
    HIGH = "high"
    MEDIUM = "medium"
    LOW = "low"


# (date_type, breach_type) -> (severity, assignee_role, sla_hours)
SEVERITY_MATRIX: dict[tuple[str, BreachType], tuple[Severity, str, int]] = {
    ("option_exercise", BreachType.DEADLINE_PASSED): (Severity.CRITICAL, "senior_asset_manager", 4),
    ("notice_window", BreachType.DEADLINE_PASSED): (Severity.HIGH, "asset_manager", 24),
    ("insurance_expiry", BreachType.DEADLINE_PASSED): (Severity.CRITICAL, "compliance_owner", 4),
    ("estoppel_deadline", BreachType.DEADLINE_PASSED): (Severity.CRITICAL, "compliance_owner", 4),
    ("rent_review", BreachType.DEADLINE_PASSED): (Severity.HIGH, "portfolio_manager", 24),
}
_DEFAULT_RULE = (Severity.MEDIUM, "portfolio_manager", 48)  # near-breach / final-tier unacked


class Provenance(BaseModel):
    """Everything a reviewer needs to act without opening the source lease."""
    canonical_fields: list[str] = Field(..., min_length=1)  # which fields lapsed
    source_amendment_id: Optional[str] = None               # amendment that set the deadline
    computed_deadline: date                                 # the deadline as the platform saw it
    formula_rule_id: Optional[str] = None                   # escalation-formula rule, if derived
    last_tier_fired: Optional[int] = None                   # nearest reminder that went out
    last_notified_on: Optional[date] = None
    breach_reason: str = Field(..., min_length=3)           # why the ladder failed to resolve it


class ReviewTask(BaseModel):
    idempotency_key: str = Field(..., min_length=8)
    lease_id: str
    date_type: str
    breach_type: BreachType
    severity: Severity
    assignee_role: str
    sla_due_at: datetime
    provenance: Provenance
    status: str = "open"

    @field_validator("sla_due_at")
    @classmethod
    def _timezone_aware(cls, v: datetime) -> datetime:
        if v.tzinfo is None:
            raise ValueError("sla_due_at must be timezone-aware (store UTC)")
        return v


def build_review_task(cd, state, today: date, now: Optional[datetime] = None) -> ReviewTask:
    """Convert a breached/unacknowledged CriticalDate into a review-queue item.

    Reads only already-validated fields off the CriticalDate and AlertState the
    alerting evaluator maintains; classifies the breach; stamps provenance; and
    derives a deterministic key so re-running the daily sweep never duplicates it.
    """
    now = now or datetime.now(timezone.utc)
    days_left = (cd.due_date - today).days
    breach_type = (
        BreachType.DEADLINE_PASSED if days_left < 0 else BreachType.FINAL_TIER_UNACKED
    )

    severity, assignee, sla_hours = SEVERITY_MATRIX.get(
        (cd.date_type.value, breach_type), _DEFAULT_RULE
    )

    reason = (
        f"deadline {cd.due_date.isoformat()} passed unacknowledged"
        if breach_type is BreachType.DEADLINE_PASSED
        else f"final tier T-{state.last_tier_fired} unacknowledged past {cd.ack_sla_days}d SLA"
    )

    provenance = Provenance(
        canonical_fields=[f"{cd.date_type.value}.due_date"],
        source_amendment_id=getattr(cd, "source_amendment_id", None),
        computed_deadline=cd.due_date,
        formula_rule_id=getattr(cd, "formula_rule_id", None),
        last_tier_fired=state.last_tier_fired,
        last_notified_on=state.last_fired_on,
        breach_reason=reason,
    )

    # One missed date == one review item. The key excludes `today` on purpose:
    # every sweep after the breach resolves to the same key, so the queue dedupes.
    idempotency_key = f"review:{cd.lease_id}:{cd.date_type.value}:{breach_type.value}"

    return ReviewTask(
        idempotency_key=idempotency_key,
        lease_id=cd.lease_id,
        date_type=cd.date_type.value,
        breach_type=breach_type,
        severity=severity,
        assignee_role=assignee,
        sla_due_at=now + timedelta(hours=sla_hours),
        provenance=provenance,
    )


def enqueue_review_task(task: ReviewTask, queue) -> bool:
    """Insert-if-absent on the idempotency key. Returns True only on first write."""
    if queue.exists(task.idempotency_key):     # unique constraint in production
        return False
    queue.insert(task.idempotency_key, task.model_dump(mode="json"))
    return True

The load-bearing choice is that idempotency_key deliberately omits today. The breach is a property of the date, not of the run that noticed it, so the twentieth sweep after a deadline passes produces the same key as the first — and enqueue_review_task discards it. That is the whole guarantee: one missed date is one review item, forever, regardless of how long it sits open. Closing the loop is the reviewer’s action: they either correct and re-arm the underlying schedule (reset the alert state’s tier markers so the ladder tracks a new deadline) or mark the date genuinely lapsed, and either resolution flips the task to resolved so it leaves the active queue while staying in the audit trail.

Edge cases specific to commercial leases

  • The sweep never ran. A breach detected on a Monday might actually have occurred the previous Wednesday because the scheduled job was down. The deadline is real, but the sla_due_at computed from now is generous relative to how long the date has actually been lapsed. Enrich provenance with days_since_deadline and let the queue escalate severity when that gap is large, rather than trusting the detection timestamp as the breach timestamp.
  • An amendment retroactively moved the deadline. A late-arriving amendment shifts a notice window such that a date the platform thought was breached was never actually due, or vice versa. Because the deadline is re-derived through escalation formula mapping, a review task built against the stale deadline must be reconciled: on re-derivation, close the old task with reason superseded_by_amendment and let the ladder re-arm against the corrected date rather than leaving a task that references a deadline that no longer exists.
  • Duplicate review items for one date. Two workers run the sweep concurrently and both classify the same breach. The deterministic key plus an insert-if-absent write means only the first commits; the second is a silent no-op. Never key the task on a UUID or the run timestamp, or the queue accumulates one task per sweep for the same missed date.
  • False positive from a timezone boundary. A notice window that expires at end-of-day in the lease’s jurisdiction can look breached when today is computed in UTC just after midnight. Classify these as Low severity into an ops-triage lane and require the reviewer to confirm the computed deadline against the jurisdiction date before any irreversible action — never auto-forfeit an option on a UTC off-by-one.
  • Acknowledged after breach. An owner acknowledges the date after it has already been escalated to a review task. Acknowledgement stops the reminder ladder, but it does not silently delete the review task, because a lapsed option may need a human to confirm whether the right was actually preserved. Instead, annotate the open task with the late acknowledgement so the reviewer closes it with full context rather than the system erasing evidence of the miss.

When to escalate further

The review queue is the end of the automation path, but some missed dates carry exposure beyond an operations reviewer’s remit. When a passed option or notice window plausibly forfeits a tenant’s right, or an estoppel deadline lapses in a way that could bind the landlord to an inaccurate statement, the task should route onward to legal with the audit trail intact — and that trail is precisely why provenance is structured, not free-text. Escalating into legal or estoppel-exposure territory means the record is about to be read by people who were not in the pipeline, so the who-saw-what history matters: enforce the access rules in security and access boundaries so the review task, its provenance, and the immutable log of every notification the ladder sent are visible to the right roles and tamper-evident. A reviewer resolving a Critical option miss should be able to prove, from that trail alone, that the platform fired every tier on schedule and that the failure was a human non-response — not a silent system fault.

For the mechanics of re-arming a schedule after resolution, and how recurring option deadlines are seeded in the first place, see the calendar guide on mapping option deadlines to recurring reminders; for how the shared queue itself is configured and drained, see implementing fallback routing for missing lease metadata fields.

Frequently asked questions

How is this different from the last reminder tier in the alerting ladder? The final reminder tier still assumes a human will respond in time; this layer begins once that assumption has failed. A reminder is a nudge toward an owner who can still act before the deadline. A review task is an admission that the deadline is gone or effectively unreachable, so it is packaged with provenance, a severity, and an SLA and handed to a triage queue — it is a work item to be worked, not another notification to be ignored.

How do I guarantee one missed date produces exactly one review item? Derive the task’s idempotency key from stable properties of the date — lease id, date type, and breach type — and deliberately exclude the run timestamp and today. Enqueue with an insert-if-absent write backed by a unique constraint. Every subsequent sweep recomputes the identical key and the write is discarded, so retries, overlapping workers, and weeks of the task sitting open never create a duplicate.

What context does a reviewer actually need in the task? Enough to act without opening the source lease under time pressure: which canonical field lapsed, the amendment that set the deadline, the computed deadline plus the escalation-formula rule that derived it, which reminder tiers actually fired and when, and a plain-language breach reason. That structured provenance is what turns a 4-hour SLA into a solvable problem rather than a research project.

What happens if an amendment changes the deadline after a review task was filed? Treat the amendment as an event that re-derives the date. If the corrected deadline invalidates the breach, close the existing task with a superseded_by_amendment reason and re-arm the alert schedule against the new deadline. Never leave a task pointing at a deadline that no longer exists, and never edit the deadline in place while keeping the old task open.

Should an acknowledgement that arrives after the breach auto-close the review task? No. A late acknowledgement stops the reminder ladder but should only annotate the open review task, not delete it. A lapsed option or notice window may still need a human to confirm whether anything was actually lost, so the task stays open with the late acknowledgement recorded, and a reviewer closes it deliberately.

← Back to Critical Date Alerting

← Back to Critical Date Alerting