Ein n8n Workflow, der Ihr Vertragsrepository an jedem Werktag ausliest, für jeden Lieferantenvertrag das Datum berechnet, bis zu dem gekündigt oder nachverhandelt werden muss — die Kündigungsfrist, nicht das Vertragsende —, Lizenznutzung und angebotene Preisänderung aus Ihren eigenen Tabellen zieht und dem verantwortlichen Owner ein Briefing mit der Empfehlung verlängern / nachverhandeln / beenden zustellt, bevor dieses Datum verstreicht. Kostet rund 0,05 bis 0,07 USD pro Briefing an Claude Inferenz und etwa 22 n8n Executions pro Monat.
Der Workflow liegt in apps/web/public/artifacts/contract-renewal-radar-n8n/contract-renewal-radar-n8n.json (15 Nodes, ein Schedule Trigger). Die drei benötigten Postgres Tabellen stehen in der Nachbardatei schema.sql, das Einrichten der Credentials sowie eine siebenstufige Verifikationssequenz in der _README.md.
Wann Sie das einsetzen
Sie verwalten mehr als etwa 40 Lieferantenverträge, überwiegend mit automatischer Verlängerung, und diese liegen in einem Repository mit strukturierten Datumsfeldern — Ironclad, Docusign Agreement Manager oder ein Äquivalent, das Ablauf- und Verlängerungsmetadaten über eine API bereitstellt. Jemand verantwortet die Lieferantenausgaben und muss geradestehen, wenn ein Vertrag sich zu einem Preis verlängert, dem niemand zugestimmt hat. Der Gewinn ist nicht die Benachrichtigung; Ihr CLM benachrichtigt bereits. Der Gewinn ist, dass die Benachrichtigung an die Kündigungsfrist gekoppelt ankommt, die Nutzungs- und Preisänderungszahlen mitbringt, die der Owner sonst selbst zusammensuchen müsste, und eine Empfehlung ausspricht. Die Entscheidung fällt damit in einer Nachricht statt in drei Wochen Terminfindung.
Wann Sie das NICHT einsetzen
Lassen Sie es, wenn Ihre Kündigungsfristen nirgendwo maschinenlesbar erfasst sind. Der Workflow kann eine Frist aus Ablaufdatum und Kündigungsfrist berechnen; er kann die Kündigungsklausel nicht aus einem PDF lesen, und ein Repository, in dem dieses Feld bei den meisten Datensätzen leer ist, erzeugt ein Radar, das fast vollständig auf der Standardannahme von 90 Tagen beruht. Diese Klauseln zuerst zu extrahieren ist ein eigenes Projekt und genau das, was Sie tatsächlich brauchen. Lassen Sie es bei weniger als etwa 40 Verträgen, wo eine Tabelle mit vier Kalendererinnerungen pro Vertrag klar gewinnt. Lassen Sie es, wenn der Einkauf bereits einen Verlängerungskalender führt, an den sich die Leute halten: Dies ersetzt einen fehlenden Prozess, keinen funktionierenden. Und lassen Sie es, wenn Ihre Kündigungsklauseln in Werktagen statt Kalendertagen formuliert sind und Sie die Alarmstufen nicht entsprechend verkürzen wollen; der Workflow zählt Kalendertage, wodurch seine Frist später liegt als die tatsächliche.
Einrichtung
Führen Sie zuerst schema.sql aus. Es legt vendor_context an (die Ausgaben- und Nutzungsdaten, die Ihr CLM nicht hält), renewal_radar_log (Audit Trail und Deduplizierungsschlüssel) sowie renewal_decisions (von Menschen geschrieben und die einzige Tabelle, die Ihre Erfolgsmetrik ehrlich auslesen kann). Importieren Sie danach das JSON, verdrahten Sie die vier Platzhalter-Credentials gemäß README und prüfen Sie, dass die Zeitzone des Workflows der entspricht, nach der sich Ihre Kündigungsfristen richten.
Die Konfiguration, auf die es wirklich ankommt, sind einige Konstanten am Anfang zweier Code Nodes. In Normalize + Compute Notice Window bildet RENEWAL_FIELD_NAMES die Anzeigenamen Ihrer Ironclad Record Felder auf die interne Struktur des Workflows ab: Die Ironclad Feld-IDs für Record Metadaten sind mandantenspezifisch, deshalb ist dies die einzige Einstellung, für die niemand einen funktionierenden Standardwert ausliefern kann. DEFAULT_NOTICE_DAYS steht auf 90, und TIERS löst Hinweise 90, 60, 30 und 7 Tage vor der Frist aus. In Score + Route entscheiden die Nutzungsbänder (0,40 und 0,70), die Schwelle für Preiserhöhungen (10%) und die Eskalationsgrenze (50.000 USD pro Jahr) darüber, was empfohlen wird und wer davon erfährt.
Rechnen Sie damit, diese Schwellen zweimal nachzujustieren. Starten Sie mit den Standardwerten, beobachten Sie ein Quartal lang die Routing-Entscheidungen im Vergleich zu dem, was Ihr Team tatsächlich entschieden hat, und verschieben Sie die Bänder entsprechend.
Was der Workflow tut
Daily Radar Run — 07:00 Weekdays löst aus. Load Radar State liest, welche Verträge bereits gemeldet wurden und in welcher Stufe. Ironclad — Record Schema löst die Feld-IDs Ihres Mandanten gegen RENEWAL_FIELD_NAMES auf, danach durchläuft Ironclad — List Records das Repository abgeschlossener Verträge unter /public/api/v1/records mit dem Scope public.records.readRecords. Docusign — List Agreements tut dasselbe gegen die Agreement Manager API, deren Iris Extraktion expiration_date, renewal_type, renewal_notice_date und total_agreement_value als Provisions zurückgibt. Merge Contract Sources hängt beide Quellen aneinander; einen der beiden Nodes zu deaktivieren ist der Weg, mit nur einem Repository zu arbeiten.
Normalize + Compute Notice Window ist die Stelle, an der die eigentliche Arbeit passiert, und das ist bewusst schlichte Arithmetik statt eines Modellaufrufs. Der Node bildet beide Quellen auf eine Struktur ab und berechnet dann notice_deadline als Ablaufdatum minus Kündigungsfrist, auf UTC Kalendertagsgrenzen, damit die Sommerzeit ein juristisches Datum nicht verschieben kann. Verträge innerhalb des 90-Tage-Horizonts erhalten eine Stufe, alles andere entfällt. New Tier Only verwirft anschließend jeden Vertrag, der in seiner aktuellen Stufe bereits gemeldet wurde — das ist es, was verhindert, dass dieselben 40 Verträge jeden Werktag erneut gepostet werden.
Load Spend + Usage zieht lizenzierte und aktive Seats sowie die Werte der letzten Laufzeit und des angebotenen Verlängerungspreises aus vendor_context. Claude — Renewal Brief macht einen Aufruf der Messages API gegen claude-sonnet-5 mit adaptivem Thinking auf mittlerem Effort und liefert ein JSON Briefing mit einer Empfehlung, einer auf 60 Wörter begrenzten Begründung, bis zu drei Verhandlungspunkten und einer expliziten Liste der Felder, die es benötigt und nicht erhalten hat. Der System Prompt verbietet jede Zahl, die nicht im übergebenen JSON steht — das ist der Fehlermodus, der KI-verfasste kommerzielle Briefings gefährlich macht.
Score + Route berechnet die Empfehlung deterministisch aus Nutzung und Preisänderung und vergleicht dann die Antwort des Modells damit. Die Arithmetik entscheidet über den Weg, das Modell schreibt den Text. Uneinigkeit ist selbst ein Routing-Signal: Sie zwingt den Vertrag zur Rechtsabteilung, mit beiden Empfehlungen nebeneinander. Needs Legal Escalation verzweigt nach #legal-ops-renewals oder #vendor-renewals, und beide Zweige enden bei Upsert Radar Log, das auf contract_id upsertet, sodass ein erneuter Lauf nach einem Teilausfall gefahrlos ist.
Die Kostenrealität
Pro Briefing: ein Sonnet 5 Aufruf mit 5.000 bis 8.000 Input Tokens und 800 bis 1.500 Output Tokens, dazu adaptives Thinking, das als Output abgerechnet wird — also 0,05 bis 0,07 USD bei Listenpreisen von 3 USD je Million Input und 15 USD je Million Output. Ein Portfolio von 400 Verträgen schiebt typischerweise 30 bis 40 Verträge pro Monat über eine Stufengrenze, was 2 bis 3 USD Inferenz ergibt.
Die n8n Seite lohnt das Verständnis, weil sie die übliche Pro-Element-Ökonomie umdreht. Der Fan-out über die Verträge passiert innerhalb eines einzigen Workflow-Laufs, ein Werktags-Cron kostet also etwa 22 Executions pro Monat gegenüber den 2.500 im n8n Cloud Starter für 24 EUR/Monat — unter 1% des Einstiegstarifs, bei unbegrenzten Workflows und Nutzern in allen Cloud Tarifen seit 2026.
Dem gegenüber steht eine einzige ungewollte automatische Verlängerung bei einem mittelgroßen Vertrag, die mindestens vierstellig kostet. Die Ökonomie ist nicht der interessante Teil dieses Workflows, die operative Disziplin ist es.
Erfolgsmetrik
Messen Sie den Anteil der in renewal_decisions erfassten Verlängerungsentscheidungen, bei denen decided_on am oder vor notice_deadline liegt. Die Query steht am Ende von schema.sql. Ihre Ausgangsbasis ist der Anteil, der heute vor Fristablauf entschieden wird — für die meisten Teams, die hiermit starten, eine Zahl, die noch nie jemand gemessen hat. Erfassen Sie sie ein Quartal lang, bevor Sie eine Verbesserung behaupten.
Messen Sie nicht die Zahl versendeter Benachrichtigungen. Ein Workflow, der 40 Nachrichten postet, die niemand liest, erreicht bei dieser Metrik die Bestnote und hat nichts verändert. Die zweite Zahl, die sich zu beobachten lohnt, ist, wie viele Verträge ihre 7-Tage-Stufe noch unentschieden erreichen. Sinkt dieser Wert bis zum zweiten Quartal nicht, liegt das Problem bei der Verantwortlichkeit, nicht beim Tool.
Im Vergleich zu den Alternativen
Gegenüber den in Ironclad und Docusign eingebauten Verlängerungshinweisen: Die laufen zuverlässig und kosten nichts extra, aber sie benachrichtigen zu einem Datum und hören dort auf. Sie erreichen weder die Lizenznutzung in einer Admin-Konsole des Anbieters noch ein Angebot, das in einer E-Mail liegt, und sie liefern keine Empfehlung — der Owner muss die Entscheidung also weiterhin selbst zusammenbauen. Genau dieser Zusammenbau ist der Schritt, der liegen bleibt. Wenn Ihr Team konsequent auf den nativen Hinweis reagiert, nutzen Sie den nativen Hinweis.
Gegenüber einer SaaS Management Plattform — der Kategorie Zylo, Productiv und Vendr: Diese kaufen Ihnen Nutzungstelemetrie und Verlängerungsverfolgung als Produkt ein, was auf der Nutzungsseite tatsächlich mehr ist, als dieser Workflow bietet. Sie kosten aber echtes Geld, verlangen ein eigenes Integrationsprojekt und übergeben dem Owner am Ende trotzdem ein Dashboard statt einer Entscheidung. Dieser Workflow ist die richtige Wahl, wenn Sie die Vertragsdaten bereits haben und die Lücke die letzte Meile ist.
Gegenüber einem eigenen Skript auf einem Cron: dieselbe Logik, und Sie verantworten dann Retries, Credential Rotation, Pagination und Observability. Der konkrete Grund, dies in n8n zu bauen, ist die Retry-Semantik pro Node beim Anthropic Aufruf und das visuelle Ausführungsprotokoll: Wenn eine neue Vertragsstruktur den Normalize Node bricht, sehen Sie, welcher Datensatz es war.
Fallstricke
Eine fehlende Kündigungsfrist ist der eine Wert, der den Workflow lautlos zerstört. Absicherung: Normalize + Compute Notice Window behandelt eine leere oder negative Kündigungsfrist als DEFAULT_NOTICE_DAYS (90) statt als Null und setzt notice_source: 'assumed'. Ein Standardwert von Null würde die Kündigungsfrist auf das Ablaufdatum legen und den Vertrag bis zu dem Tag als sicher ausweisen, an dem er sich verlängert. Jede auf einem angenommenen Wert beruhende Karte sagt das in ihrer ersten Zeile, und angenommene Verträge werden unabhängig vom Wert zwingend an die Rechtsabteilung geleitet.
Ironclad Record Feld-IDs sind mandantenspezifisch, eine fest verdrahtete Zuordnung scheitert auf einer fremden Instanz also lautlos. Absicherung: Der Workflow ruft den Record Schema Endpunkt zur Laufzeit auf und wirft schema_field_missing, wenn er das Ablauf- oder Kündigungsfristfeld nicht auflösen kann. Ein lautes Scheitern beim Import ist hier richtig; die Alternative ist ein Radar, das sauber läuft und nichts findet.
Tägliche Wiederholungsmeldungen erziehen die Leute dazu, den Kanal stummzuschalten, und erzeugen damit genau die verpasste Frist, die der Workflow verhindern soll. Absicherung: Der Node New Tier Only liest renewal_radar_log.last_tier_notified, jeder Vertrag wird also einmal pro Stufengrenze gepostet — vier Nachrichten in 90 Tagen, nicht sechzig.
Das Modell kann ein flüssiges Briefing zu einem Vertrag verfassen, dessen Nutzungsdaten nie geladen wurden. Absicherung: Score + Route berechnet die Empfehlung arithmetisch und behandelt die Modellantwort als beratend; ist context_complete falsch, liefert der deterministische Pfad renegotiate und niemals renew, denn fehlende Daten sind kein Beleg dafür, dass die aktuellen Konditionen in Ordnung sind. Uneinigkeit zwischen beiden setzt model_agrees: false und erzwingt die Eskalation mit beiden Antworten.
Der stillere Verwandte der Alarmmüdigkeit: ein Briefing, das ohne Namen ankommt. Absicherung: owner_email wird aus dem Repository durchgereicht und auf der Karte im Owner-Kanal angezeigt. Verträge ohne hinterlegten Owner werden trotzdem gepostet, landen aber im Eskalationskanal — eine Verlängerung ohne Verantwortlichen ist ein Legal Ops Problem, bevor sie ein Budgetproblem ist.
Stack
n8n orchestriert. Claude Sonnet 5 verfasst das Briefing über die Anthropic Messages API. Ironclad und Docusign sind die Vertragsrepositories; jedes für sich genügt. Postgres hält den Ausgaben- und Nutzungskontext, das Radar-Log und die Entscheidungshistorie. Slack empfängt die Owner-Hinweise und die Eskalationen an die Rechtsabteilung.
Dies ist die operative Schicht über dem Contract Lifecycle Management: Das Repository muss befüllt sein und die Kündigungsfristen müssen darin stehen, bevor irgendetwas davon läuft. Neue Lieferanten über einen Vendor Due Diligence Workflow aufzunehmen hält diese Felder künftig befüllt; dieser Workflow deckt die Verträge ab, die Sie unterschrieben haben, bevor das jemand getan hat.
# Contract Renewal Radar — setup
An n8n flow that watches vendor-contract notice windows, pulls spend and usage context, and drafts a renew / renegotiate / terminate brief before the notice deadline — not before the expiration date.
Files in this bundle:
- `contract-renewal-radar-n8n.json` — the workflow export (15 nodes, one schedule trigger)
- `schema.sql` — the three Postgres tables the flow reads and writes
- `_README.md` — this file
## Before you import
Run `schema.sql` against the database you will point the Postgres credential at. The flow reads `vendor_context`, reads and writes `renewal_radar_log`, and never touches `renewal_decisions` — that last table is written by humans and is what your success metric reads.
`vendor_context` can start empty. Contracts with no row still produce a brief; they are flagged `context_complete: false` and routed to the escalation channel rather than being recommended for renewal on missing data.
## Import
n8n → **Workflows** → **Import from File** → select `contract-renewal-radar-n8n.json`. The workflow imports with the trigger **inactive**. Leave it that way until you have run the verification sequence below.
Open **Workflow settings** and confirm **Timezone** reads `America/New_York`, or change it to yours. The notice-deadline math is calendar-date based; a timezone mismatch shifts every deadline by one day, and one day is the whole point of this flow.
## Credentials
Four credentials, each referenced in the export by a `PLACEHOLDER_*` id. n8n will show them as broken until you map each node to a real credential.
### `PLACEHOLDER_POSTGRES_CRED_ID` — Postgres
Standard Postgres credential against the database holding the three tables. Needs `SELECT` on `vendor_context`, and `SELECT`/`INSERT`/`UPDATE` on `renewal_radar_log`. Used by **Load Radar State**, **Load Spend + Usage**, and **Upsert Radar Log**.
### `PLACEHOLDER_IRONCLAD_CRED_ID` — Ironclad
Create a **Header Auth** credential with name `Authorization` and value `Bearer <your token>`. Generate the token in Ironclad under **Company Settings → API**; it needs the OAuth scope `public.records.readRecords`. Used by **Ironclad — Record Schema** and **Ironclad — List Records**.
If your Ironclad instance is on the EU or demo environment, change the host in both nodes — the export points at `ironcladapp.com`.
### `PLACEHOLDER_DOCUSIGN_CRED_ID` — Docusign
An **OAuth2 API** credential against Docusign's Agreement Manager API (formerly Navigator). You will also need `DOCUSIGN_ACCOUNT_ID` set as an environment variable on your n8n instance — the URL interpolates it. The Agreement Manager API reached general availability for eligible Agreement Manager customers in May 2026; if your account predates that, confirm entitlement before wiring this node.
If you run only one contract repository, **disable the node for the one you do not use**. The Merge node is in append mode and passes through whichever branch produces items.
### `PLACEHOLDER_ANTHROPIC_CRED_ID` — Anthropic
An **Anthropic** credential holding your API key. Used by **Claude — Renewal Brief**, which calls `claude-sonnet-5` on the Messages API with adaptive thinking at `medium` effort.
## Configure before first run
Two Code nodes hold the values you will actually tune. Open them and read the constants at the top before you run anything.
**Normalize + Compute Notice Window**
- `RENEWAL_FIELD_NAMES` — the display names of the Ironclad record fields holding expiration date, notice period, renewal type, annual value, and owner email. **These are per-tenant.** The node resolves them against the live schema and throws `schema_field_missing` if it cannot find the expiration or notice-period field. That is deliberate: a silent miss here would mark every contract safe.
- `DEFAULT_NOTICE_DAYS` (90) — what to assume when the paper does not state a notice period. Contracts that hit this default are stamped `notice_source: 'assumed'` and the Slack card says so.
- `TIERS` (90 / 60 / 30 / 7) — how many days before the notice deadline to nudge.
**Score + Route**
- `LOW_UTILIZATION` (0.40) and `SOFT_UTILIZATION` (0.70) — the seat-utilization bands that drive terminate and renegotiate.
- `UPLIFT_RENEGOTIATE` (0.10) — quoted increase above which the flow argues for pushing back.
- `ESCALATION_VALUE_CENTS` (5000000, i.e. $50,000/yr) — above this, contracts go to legal rather than to the business owner.
Also change the two Slack channels (`#legal-ops-renewals` and `#vendor-renewals`) to yours.
## First-run verification
Run these in order with the trigger still inactive. Each step proves one branch works before the next one can hide a failure.
1. **Schema resolution.** Execute **Ironclad — Record Schema** alone. Confirm the response lists your record fields, then execute **Normalize + Compute Notice Window** and check it does not throw `schema_field_missing`. If it does, fix `RENEWAL_FIELD_NAMES` — do not work around it downstream.
2. **Date math.** Temporarily set `RADAR_HORIZON_DAYS` to `3650` and execute up to **Normalize + Compute Notice Window**. Pick three contracts you know by hand and check `notice_deadline` equals `expiration_date` minus the notice period on the paper. Check that at least one contract shows `notice_source: 'assumed'` if any of your records lack a notice period. Set the horizon back to `90`.
3. **Empty-state dedup.** With `renewal_radar_log` empty, execute through **New Tier Only** and note the item count. Run the whole flow once, then execute through **New Tier Only** again — the count should now be zero, because every contract was logged at its current tier. This is the check that stops daily re-notification.
4. **Context degradation.** Delete the `vendor_context` row for one in-window contract and run that contract through **Score + Route**. Confirm `context_complete` is `false`, the recommendation is `renegotiate` (never `renew`), and `route` is `legal_escalation`.
5. **Model disagreement.** Find or force a contract where `model_agrees` is `false`. Confirm the escalation card carries the "Model recommended X; rules recommended Y" context line and that `recommendation` is the deterministic value, not the model's.
6. **Brief failure.** Temporarily point the Anthropic credential at an invalid key and run one contract. The flow must complete, `brief_error` must be populated, `rationale` must read "Model brief unavailable", and the Slack card must still post with the deterministic recommendation. Restore the key.
7. **Idempotency.** Re-run the full flow immediately. `renewal_radar_log` row count must not change, and no new Slack messages should post.
Once all seven pass, activate the trigger.
## Cost
Per contract that enters the radar: one Claude Sonnet 5 call at roughly 5–8k input tokens and 800–1,500 output tokens, plus adaptive thinking billed as output. At list pricing of $3 per million input and $15 per million output, that lands around $0.05–$0.07 per brief. A 400-contract vendor portfolio typically pushes 30–40 contracts across a tier boundary in a month, so inference runs $2–3 monthly.
n8n execution cost is a rounding error here and worth understanding, because it is the opposite of a per-item webhook flow: the fan-out across contracts happens *inside* one workflow run, so a weekday cron is about 22 executions a month. n8n Cloud Starter is €24/month for 2,500 executions, with unlimited workflows and users on every Cloud plan as of 2026 — this flow uses under 1% of the entry tier.
The real cost is getting notice periods out of the paper and into the repository. Budget that as the project, not the automation.
## Known limits
- **Ironclad pagination is capped** at 50 pages of 100 records (5,000 records). Raise `maxRequestsFetched` on **Ironclad — List Records** if your repository is larger, and re-check the dropped-record count the Normalize node logs.
- **The flow reads; it never writes back to the CLM.** Decisions are recorded by a human in `renewal_decisions`. Wiring the decision back into Ironclad or Docusign is a deliberate omission — a write scope on the contract repository is a much larger security review than a read scope, and the flow's value does not depend on it.
- **No holiday or business-day handling.** `days_to_deadline` counts calendar days. If your notice clauses specify business days, the deadline this flow computes is later than the real one, and you should shorten the tiers to compensate.
- **Not runtime-tested against a live Ironclad or Docusign tenant.** The endpoint paths, scopes, and provision field names come from vendor documentation; the response-shape handling in the Normalize node is defensive but you should expect to adjust the field extraction on first contact with your own data.
-- Contract Renewal Radar — supporting tables
-- Postgres 13+. Run this before importing the workflow.
-- ---------------------------------------------------------------------------
-- vendor_context: the spend and usage data your CLM does not hold.
-- Populate from your AP export, the vendor's admin API, or your IdP. One row
-- per contract, keyed by the same contract_id the flow builds
-- ("ironclad:<record id>" or "docusign:<agreement id>").
--
-- Every column is nullable on purpose. A contract with no row here still gets
-- a brief; it is flagged context_complete: false and routed for review rather
-- than silently recommended for renewal.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS vendor_context (
contract_id text PRIMARY KEY,
licensed_seats integer,
active_seats_30d integer,
last_term_annual_cents bigint,
quoted_renewal_annual_cents bigint,
open_support_tickets_90d integer,
replacement_effort text
CHECK (replacement_effort IN ('low', 'medium', 'high')),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- ---------------------------------------------------------------------------
-- renewal_radar_log: one row per contract, overwritten each time the contract
-- crosses into a new tier. This is both the audit trail and the deduplication
-- key — last_tier_notified is what stops the flow re-posting the same contract
-- every weekday for 90 days.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS renewal_radar_log (
contract_id text PRIMARY KEY,
source text NOT NULL,
counterparty text,
notice_deadline date NOT NULL,
expiration_date date NOT NULL,
tier text NOT NULL,
days_to_deadline integer NOT NULL,
notice_source text NOT NULL
CHECK (notice_source IN ('contract', 'assumed')),
recommendation text NOT NULL
CHECK (recommendation IN ('renew', 'renegotiate', 'terminate')),
model_recommendation text,
model_agrees boolean,
utilization numeric(5, 4),
uplift numeric(6, 4),
route text NOT NULL,
rationale text,
last_tier_notified text NOT NULL,
last_notified_on date NOT NULL DEFAULT CURRENT_DATE
);
-- The dedup read is a full-table scan today. It stays cheap into the low
-- thousands of contracts; index it once the repository is larger than that.
CREATE INDEX IF NOT EXISTS renewal_radar_log_deadline_idx
ON renewal_radar_log (notice_deadline);
-- ---------------------------------------------------------------------------
-- renewal_decisions: written by a human, not by the flow. This is the table
-- your success metric reads — the flow can only prove it sent a nudge, not
-- that anyone decided anything.
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS renewal_decisions (
id bigserial PRIMARY KEY,
contract_id text NOT NULL,
decided_on date NOT NULL,
notice_deadline date NOT NULL,
decision text NOT NULL
CHECK (decision IN ('renewed', 'renegotiated', 'terminated', 'lapsed')),
decided_by text,
notes text
);
-- The number that matters: what share of decisions landed before the notice
-- deadline rather than after it.
--
-- SELECT date_trunc('quarter', decided_on) AS quarter,
-- count(*) FILTER (WHERE decided_on <= notice_deadline)::numeric
-- / count(*) AS on_time_rate
-- FROM renewal_decisions
-- GROUP BY 1 ORDER BY 1;