Downstream Automation & Sync
A validated lease record is not the finish line — it is the starting gun. Once a commercial lease has been abstracted into trustworthy structured data, the terms locked inside it have to do something: a renewal option has to surface on someone’s calendar before the notice window closes, a rent escalation has to appear as the correct dollar figure on the next accounts-receivable run, a right-of-use asset has to roll forward on the ASC 842 schedule, and a missed deadline has to escalate to a human before it becomes a lost right or a lawsuit. Downstream automation is the discipline of turning canonical lease terms into those actions reliably, at portfolio scale, without ever touching the source document again.
This domain sits strictly after the ingestion boundary defined in Core Architecture & Lease Taxonomy. Where that domain decides how lease data is shaped and governed, and the companion Parsing & Extraction Workflows domain decides how raw documents become candidate records, this domain consumes the finished product. It never parses a PDF, never runs OCR, and never guesses at a clause. It subscribes to the canonical events those upstream systems emit — lease.canonicalized, rent.schedule.changed, amendment.applied — and it fans each one out into calendars, billing ledgers, accounting exports, and alerting queues. Every artifact it produces is idempotent and carries a provenance trail back to the event and record version that caused it.
Why Downstream Sync Is an Architecture Problem, Not a Cron Job
The naive version of this work looks trivial: read the lease, write the dates and the rent into the systems that need them. Teams reach for a nightly script, a spreadsheet export, or a hand-maintained reminder list. That approach survives a dozen leases and collapses at a thousand. The reason is that the inputs never stop moving. An amendment shifts a commencement date; a CPI revision changes every future rent row; a lease is assigned and its notice window recalculated; a renewal is exercised and an option deadline that was on three people’s calendars must now vanish. Each of those changes has to propagate to every system that already acted on the old value — and it has to do so without creating duplicates, without dropping the change, and without silently overwriting a manual correction an accountant made downstream.
The cost of getting this wrong is not cosmetic. A missed option-exercise deadline can forfeit a below-market renewal worth years of rent differential. A rent escalation that syncs a month late, or at the wrong figure, produces an under-billed tenant and a revenue-leakage finding at audit. A right-of-use asset that rolls forward on a stale schedule misstates the balance sheet under FASB ASC 842 and IFRS 16. At portfolio scale these are not edge cases; they are a steady drip of small, expensive errors that no one notices until reconciliation season. Treating downstream sync as an event-driven, idempotent architecture — rather than a batch job — is what converts that drip into a system a controller will actually trust.
Three principles run through everything below, and they are the same three that govern the ingestion seam upstream. First, derive, never store-and-drift: downstream artifacts are a pure function of the canonical record plus its rules, so they can always be recomputed and reconciled against the source of truth. Second, idempotency is mandatory, not optional: every emitted calendar event, payment row, and journal line carries a stable key so that redelivery is a no-op. Third, everything leaves a trail: each artifact records which event and which record version produced it, because an auditor will eventually ask why a number is what it is, and “the script ran” is not an answer.
Calendar & Critical-Date Generation
The temporal terms of a lease are a minefield of deadlines that are invisible until they are missed. Renewal options, termination rights, notice windows, rent-review dates, insurance-certificate expiries, and holdover triggers all carry a deadline and, crucially, a notice period that runs backward from that deadline. A lease that expires 31 December 2027 with a 12-month notice requirement effectively has an action deadline of 31 December 2026 — and the calendar has to show the earlier date, not the later one, or the reminder is worthless.
The Calendar Event Generation service owns this translation. It consumes the temporal fields of the canonical record, applies the notice-window arithmetic to derive the actionable date, and emits durable calendar artifacts — ICS invitations, recurring reminders, or API calls into whatever calendar system the property team lives in. The mechanics of building portable calendar files are covered in generating ICS calendar events for lease renewal deadlines, and the pattern for turning a single hard deadline into a graduated series of nudges is treated in mapping option deadlines to recurring reminders.
The subtlety that makes this an engineering problem rather than a formatting problem is idempotency across change. A lease record can be canonicalized, then amended, then re-canonicalized several times before its option window ever opens. Each pass must produce the same calendar entry when nothing relevant changed, and must supersede the old entry when the date moved — never leave two live invitations for the same option pointing at different dates. That requires a deterministic identity for each calendar artifact, independent of when it was generated, which we build below with a stable idempotency key.
Payment-Schedule & AR Sync
The financial terms of a lease resolve into a stream of dated money movements: base rent every month, stepped or indexed escalations at defined anniversaries, CAM estimates and true-ups, percentage rent, and abatement periods. The Payment Schedule Sync service turns those terms into concrete payment rows and pushes them into the accounts-receivable or billing system that actually invoices the tenant. The guide on syncing rent schedules to accounts-receivable systems covers the AR-side write path; generating payment schedules from escalation rules covers the derivation in depth.
This service leans hard on work done upstream in the core domain. It does not interpret rent language — it consumes an already-parameterized rule produced by escalation formula mapping and the canonical financial fields defined by the lease data models. Its job is purely deterministic: given a base rent, a term window, and an escalation rule, produce the exact ordered list of dated amounts, in Decimal, that the tenant owes. Because the derivation is pure, a schedule can be recomputed at any time and reconciled row-for-row against what was actually billed, which is how revenue leakage gets caught before an auditor finds it.
The following code is the deterministic core of this service. It models the canonical rent schedule with pydantic v2, derives an escalating payment schedule with cent-exact Decimal arithmetic and ISO-8601 dates, and produces a stable idempotency key so that redelivery of the same schedule never double-writes a row.
from __future__ import annotations
import calendar
import hashlib
import json
from datetime import date
from decimal import Decimal, ROUND_HALF_UP
from typing import Literal
from pydantic import BaseModel, Field, field_validator, model_validator
CENTS = Decimal("0.01")
class EscalationRule(BaseModel):
# Consumed as-is from escalation formula mapping — never re-interpreted here.
rule_type: Literal["none", "fixed_pct", "fixed_step"] = "none"
parameter: Decimal = Decimal("0") # percent, or currency step
frequency_months: int = Field(default=12, ge=1)
class CanonicalRentSchedule(BaseModel):
lease_id: str
currency: str = "USD"
base_rent_monthly: Decimal
schedule_start: date
schedule_end: date
escalation: EscalationRule = Field(default_factory=EscalationRule)
version: int = Field(default=1, ge=1) # bumps on every amendment
@field_validator("base_rent_monthly")
@classmethod
def rent_must_be_positive(cls, v: Decimal) -> Decimal:
if v <= 0:
raise ValueError("base rent must be positive")
return v
@model_validator(mode="after")
def term_in_order(self) -> "CanonicalRentSchedule":
if self.schedule_end <= self.schedule_start:
raise ValueError("schedule_end must strictly follow schedule_start")
return self
class PaymentRow(BaseModel):
lease_id: str
period_index: int
due_date: date
amount: Decimal
currency: str
def _add_months(anchor: date, months: int) -> date:
total = anchor.month - 1 + months
year = anchor.year + total // 12
month = total % 12 + 1
day = min(anchor.day, calendar.monthrange(year, month)[1]) # clamp Jan 31 -> Feb 28
return date(year, month, day)
def _escalate(rent: Decimal, rule: EscalationRule) -> Decimal:
if rule.rule_type == "fixed_pct":
return rent * (Decimal(1) + rule.parameter / Decimal(100))
if rule.rule_type == "fixed_step":
return rent + rule.parameter
return rent
def derive_payment_schedule(sched: CanonicalRentSchedule) -> list[PaymentRow]:
"""Pure function: canonical rent schedule -> ordered, cent-exact payment rows."""
rows: list[PaymentRow] = []
running_rent = sched.base_rent_monthly
idx = 0
due = sched.schedule_start
while due < sched.schedule_end:
if (idx > 0
and sched.escalation.rule_type != "none"
and idx % sched.escalation.frequency_months == 0):
running_rent = _escalate(running_rent, sched.escalation) # compounds
rows.append(PaymentRow(
lease_id=sched.lease_id,
period_index=idx,
due_date=due,
amount=running_rent.quantize(CENTS, rounding=ROUND_HALF_UP),
currency=sched.currency,
))
idx += 1
due = _add_months(sched.schedule_start, idx)
return rows
def calendar_idempotency_key(lease_id: str, event_type: str, effective_date: date) -> str:
"""Stable identity for a downstream artifact.
Independent of *when* it was generated: duplicate deliveries collapse to the
same key, while a moved date yields a naturally different key.
"""
basis = f"{lease_id}|{event_type}|{effective_date.isoformat()}"
return hashlib.sha256(basis.encode()).hexdigest()[:32]
def content_hash(payload: dict) -> str:
"""Detects a *changed* artifact under a stable key, to trigger supersede."""
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), default=str)
return hashlib.sha256(canonical.encode()).hexdigest()[:16]
Two properties are worth calling out. The escalation compounds because running_rent carries forward across anniversary boundaries — a 3% annual step applied to year three is 3% on top of the already-escalated year-two figure, which is what commercial leases almost always mean. And every monetary value is quantized to cents at the point it enters a row, so no sub-cent residue ever reaches the AR system to break reconciliation.
Accounting & Compliance Export (ASC 842 / IFRS 16)
Since the 2019 effective dates of FASB ASC 842 and IFRS 16, nearly every operating lease has to appear on the balance sheet as a right-of-use asset and a corresponding lease liability, each amortized on its own schedule. Generating those schedules by hand, per lease, per period, is exactly the kind of deterministic drudgery that belongs in an automated export. The Accounting System Integration service consumes the same canonical financial terms the payment sync uses, but produces a different artifact: period-by-period journal entries and disclosure schedules that a general ledger or a dedicated compliance system can ingest. The concrete export mechanics — present-value discounting, ROU asset roll-forward, and the file shapes compliance systems expect — are detailed in exporting lease schedules to ASC 842 compliance systems.
The defining constraint here is auditability. A compliance export is not fire-and-forget; it is evidence. Every journal line has to trace back to the lease version and the event that produced it, so that when a number is questioned six quarters later, the export can be regenerated deterministically and shown to match. That is why the canonical record carries a version and why downstream artifacts pin themselves to it. When an amendment changes the discount rate or the term, the export does not mutate old periods in place — it produces a new schedule tied to the new version, and the append-only ledger upstream preserves the prior one for the periods already reported. This mirrors the amendment-versioning discipline the core domain enforces, and it is what keeps a restatement honest.
Because the export is derived rather than stored, reconciliation is symmetric with the payment path: the accounting schedule and the AR schedule are two projections of the same canonical rent terms, so a divergence between them is a signal that one consumer processed a stale event. Detecting that divergence is far cheaper than explaining it to an auditor after the fact.
Critical-Date Alerting & Escalation
Calendar events are passive — they assume someone is looking. Critical-date alerting is active: it watches for deadlines that are approaching without evidence of action and escalates them before they lapse. The Critical Date Alerting service is the safety net under the whole domain. It subscribes to the same temporal events the calendar service does, but it layers on time-based evaluation — as a notice window enters its final stretch with no recorded response, the alert level rises from an informational nudge, to a direct notification to the responsible manager, to a hard escalation into a manual-review queue. The escalation ladder and its hand-off to human review are covered in escalating missed notice windows to manual review.
This service is where the downstream domain reconnects to the graceful-degradation philosophy of the core architecture. When a record is incomplete — a renewal option with no stated notice window, say — the alerting service cannot compute a reliable deadline. Rather than silently emitting nothing, it treats the gap the same way the upstream fallback routing logic treats a low-confidence field: it raises the record to human review explicitly. A silent non-alert is the single most dangerous outcome in this entire domain, because it is indistinguishable from “nothing is due” right up until the right is forfeited. Alerting therefore fails loud, never quiet.
Escalation is also idempotent and stateful. An approaching deadline generates a series of alerts over weeks, but a duplicate event delivery must not reset the ladder or spam the manager. The service keys each alert on the lease, the deadline, and the escalation stage, so redelivery is absorbed and only genuine stage transitions produce a new notification.
Production Integration: The Event-Consumer Seam
Everything above meets at one function: the consumer that subscribes to the canonical event stream and idempotently emits downstream artifacts. This is the seam where production incidents concentrate, because it is where at-least-once event delivery collides with systems that must not double-act. A well-behaved consumer treats every event as possibly a duplicate and every artifact as possibly already emitted, and reconciles the two with a durable dedupe store keyed on the stable identity built earlier.
The function below consumes a lease.canonicalized or rent.schedule.changed event, derives the calendar and payment artifacts, and emits only those that are new or genuinely changed. A prior artifact whose content hash matches is a duplicate and is skipped; one whose hash differs is a supersede and is re-emitted. Everything else — an event on an unrelated topic — is ignored, not errored, because a shared bus carries traffic this consumer has no business acting on.
from typing import Callable, Protocol
class DedupeStore(Protocol):
def seen(self, key: str) -> str | None: ... # returns prior content hash or None
def remember(self, key: str, content_hash: str) -> None: ...
class EmittedArtifact(BaseModel):
idempotency_key: str
kind: Literal["calendar_event", "payment_row", "alert"]
content_hash: str
source_event: str
source_version: int
payload: dict
def consume_domain_event(
event: dict,
sched: CanonicalRentSchedule,
critical_dates: list[tuple[str, date]], # (event_type, actionable_date)
store: DedupeStore,
emit: Callable[[EmittedArtifact], None],
) -> list[EmittedArtifact]:
"""Subscribe to a canonical event; idempotently emit downstream artifacts.
Re-delivery of the same event is a no-op. An amendment that shifts a date or
a rent produces a superseding artifact under a new-or-changed key.
"""
topic = event.get("topic")
if topic not in {"lease.canonicalized", "rent.schedule.changed"}:
return [] # not ours — ignore, don't fail the shared bus
produced: list[EmittedArtifact] = []
def _emit_if_new(key: str, kind, payload: dict) -> None:
chash = content_hash(payload)
if store.seen(key) == chash:
return # exact duplicate already emitted
artifact = EmittedArtifact(
idempotency_key=key,
kind=kind,
content_hash=chash,
source_event=event["event_id"],
source_version=sched.version,
payload=payload,
)
store.remember(key, chash) # supersede or first-write
emit(artifact)
produced.append(artifact)
# 1. Calendar / critical-date artifacts
for event_type, actionable in critical_dates:
key = calendar_idempotency_key(sched.lease_id, event_type, actionable)
_emit_if_new(key, "calendar_event", {
"lease_id": sched.lease_id,
"type": event_type,
"date": actionable.isoformat(),
})
# 2. Payment rows — one stable key per period index
for row in derive_payment_schedule(sched):
key = f"pay:{sched.lease_id}:{row.period_index}"
_emit_if_new(key, "payment_row", row.model_dump(mode="json"))
return produced
The seam embodies the domain’s three rules concretely: artifacts are derived from the canonical record inside the consumer; the dedupe store makes redelivery a no-op; and every EmittedArtifact pins its source_event and source_version for the audit trail. A new consumer — a facilities calendar, a second billing system — attaches to the same bus and follows the same contract without any change to the events being published.
Failure Modes & Edge Cases
Each capability above has a way to fail in production, and the failures concentrate around the collision of moving inputs with already-emitted outputs. Designing for these explicitly is the difference between a sync that a controller trusts and one they quietly shadow with a spreadsheet.
- Duplicate event delivery. At-least-once buses redeliver, and a consumer that acts on every delivery double-books calendars and double-writes rent rows. The dedupe store keyed on stable identity absorbs redelivery: an identical content hash under an existing key is a no-op, so replaying the entire event log converges to the same downstream state rather than doubling it.
- Amendment that shifts a window after events were emitted. A renewal option was calendared for 31 December 2026; an amendment moves expiry, and the actionable date becomes 30 June 2026. The stable key is derived from the date, so the new date yields a new key — but the old invitation is now orphaned and must be explicitly cancelled, not merely superseded. Reconciliation has to diff the current derived artifact set against what was previously emitted and retract the stragglers, or two live deadlines coexist.
- Timezone and notice-window arithmetic. Notice periods run backward from a deadline, and an off-by-one in month arithmetic or a naive UTC assumption can move an actionable date across a calendar day — enough to blow a same-day filing deadline. Anchor date math to the lease’s governing timezone, clamp month rollover (31 January plus one month is not 31 February), and treat the notice window as a hard interval, never an approximate offset.
- Escalation revisions changing future payment rows. A CPI series is revised, or a fixed-step schedule is corrected. Every future payment row changes while already-billed past rows must not. Because the schedule is derived, the consumer recomputes the full series and supersedes only the rows whose content hash differs; past rows already invoiced are pinned to the version that produced them and are left untouched.
- Silent non-alert on incomplete records. A record missing its notice window yields no computable deadline. Emitting nothing is indistinguishable from “nothing is due” and forfeits the right in silence. The alerting service must instead escalate the incomplete record to human review — failing loud, exactly as fallback routing does upstream.
- Consumer divergence. The AR schedule and the ASC 842 export are two projections of one rent term. If one consumer processed a stale event, they diverge. Periodic reconciliation of the two derived artifact sets against the canonical record surfaces the drift long before an auditor does.
Implementation Checklist
Engineers building this domain can sequence the work as follows. Each milestone is independently testable and should ship behind its own validation gate before the next begins.
- Subscribe, don’t poll. Attach consumers to the canonical event bus for
lease.canonicalizedandrent.schedule.changed; ignore unrelated topics rather than erroring on them. - Make every artifact derivable. Treat calendar events, payment rows, and journal lines as pure functions of the canonical record plus its rules, so any of them can be recomputed and reconciled.
- Give every artifact a stable idempotency key. Derive identity from lease, artifact type, and effective date — never from wall-clock time — so redelivery collapses to a no-op.
- Add a content hash for supersede. Under a stable key, a changed hash marks a genuine revision; emit it and retract any orphaned prior artifact.
- Keep money in Decimal end to end. Quantize to cents at the point each payment row is created, so no sub-cent residue reaches AR or the general ledger.
- Compute notice windows in the governing timezone. Run notice periods backward from the deadline as hard intervals, with clamped month arithmetic.
- Pin every artifact to its source event and record version. Persist provenance so a compliance export can be regenerated and defended at audit.
- Fail loud on incomplete records. Escalate a record that cannot yield a reliable deadline to human review instead of silently emitting nothing.
Frequently Asked Questions
How do I keep duplicate event delivery from double-billing or double-booking? Make every downstream artifact idempotent under a stable key derived from the lease, the artifact type, and the effective date — never from the time it was generated. Persist a dedupe store that records the content hash written under each key. When an event is redelivered, the consumer recomputes the artifact, finds an identical hash already stored, and skips the write. Replaying the entire event log then converges to the same state rather than doubling it.
What happens to calendar events and payment rows when an amendment moves a date? Because artifacts are derived, the consumer recomputes them from the amended canonical record. A shifted date produces a new idempotency key, so the new artifact is emitted as a supersede, and the reconciliation step diffs the current derived set against what was previously emitted and explicitly cancels the orphaned old entry. Never mutate already-billed past rows; pin them to the record version that produced them and change only future ones.
Does this domain ever parse the lease document? No. Downstream automation is strictly after the ingestion boundary. It subscribes to canonical events such as lease.canonicalized and rent.schedule.changed and consumes already-validated, already-parameterized terms. Document parsing, OCR, and clause extraction all belong to the upstream parsing and core-architecture domains; mixing them into the sync layer reintroduces the extraction variance this layer exists to be insulated from.
How do I generate ASC 842 and IFRS 16 schedules without drift? Derive the right-of-use asset and liability roll-forwards from the same canonical financial terms the payment sync uses, and pin every journal line to the lease version and the event that produced it. Because the export is a pure function of the record, it can be regenerated deterministically and reconciled against the AR schedule; a divergence between the two projections is a signal that one consumer processed a stale event, caught long before an auditor would find it.
How should I handle a critical date the record can’t compute — like a missing notice window? Fail loud. A record missing the data needed to compute a reliable deadline must be escalated to a manual-review queue, not silently skipped, because an absent alert is indistinguishable from “nothing is due” until the right is forfeited. This mirrors the upstream fallback approach to low-confidence fields: raise the gap to a human explicitly rather than degrading into silence.
Conclusion
Downstream automation is where lease abstraction finally pays for itself. A canonical record that never leaves the database prevents no missed option and bills no rent; the value is realized only when its terms reach the calendar, the AR ledger, the accounting export, and the alerting queue — correctly, and without the duplication and drift that batch scripts breed. Build these consumers as idempotent subscribers to a canonical event stream, derive every artifact rather than storing and mutating it, and pin each one to the event and version that produced it. Do that, and the four systems property teams actually operate inherit the correctness of the canonical layer for free, every time an event fires.
Related Pages
- Calendar Event Generation — deriving actionable renewal, option, and notice deadlines and emitting durable, idempotent calendar artifacts.
- Payment Schedule Sync — turning canonical rent and escalation terms into cent-exact, dated payment rows for accounts-receivable systems.
- Accounting System Integration — generating auditable ASC 842 and IFRS 16 roll-forward schedules and journal exports from the same terms.
- Critical Date Alerting — time-based escalation of approaching deadlines, failing loud into manual review when a record cannot be computed.
- Escalation Formula Mapping — the upstream parameterization of fixed, CPI-indexed, and step-up rent rules this domain consumes.
- Lease Data Models — the canonical schema whose validated financial and temporal fields feed every downstream consumer.
← Back to Core Architecture & Lease Taxonomy