An n8n flow that reads your contract repository every weekday, computes the date each vendor agreement must be cancelled or renegotiated by — the notice deadline, not the expiration date — pulls seat utilization and quoted price change from your own tables, and posts a renew / renegotiate / terminate brief to the owner before that date passes. Costs about $0.05 to $0.07 per brief in Claude inference and roughly 22 n8n executions a month.
The workflow ships in apps/web/public/artifacts/contract-renewal-radar-n8n/contract-renewal-radar-n8n.json (15 nodes, one schedule trigger). The three Postgres tables it needs are in the sibling schema.sql, and credential setup plus a seven-step verification sequence are in _README.md.
When to use
You manage somewhere north of 40 vendor agreements, most of them auto-renewing, and they live in a repository with structured dates — Ironclad, Docusign Agreement Manager, or an equivalent that exposes expiration and renewal metadata over an API. Someone owns vendor spend and is accountable when a contract rolls at a price nobody agreed to. The win is not the alert; your CLM already sends alerts. The win is that the alert arrives keyed to the notice deadline, carries the utilization and price-change numbers the owner would otherwise go hunting for, and states a recommendation, so the decision happens in one message instead of three weeks of scheduling.
When NOT to use
Skip this if your notice periods are not recorded anywhere machine-readable. The flow can compute a deadline from an expiration date and a notice period; it cannot read the notice clause out of a PDF, and a repository where that field is empty on most records will produce a radar built almost entirely on the 90-day default assumption. Extracting those clauses first is a different project and it is the one you actually need. Skip it if you have fewer than about 40 agreements, where a spreadsheet with four calendar reminders per contract genuinely wins. Skip it if procurement already runs a renewal calendar that people follow — this replaces a missing process, not a working one. And skip it if your notice clauses are written in business days rather than calendar days and you are not willing to shorten the alert tiers to compensate; the flow counts calendar days, which makes its deadline later than the real one.
Setup
Run schema.sql first. It creates vendor_context (the spend and usage your CLM does not hold), renewal_radar_log (the audit trail and the deduplication key), and renewal_decisions (written by humans, and the only table your success metric can honestly read). Then import the JSON, wire the four placeholder credentials per the README, and confirm the workflow timezone matches the one your notice deadlines are governed by.
The configuration that actually matters is a handful of constants at the top of two Code nodes. In Normalize + Compute Notice Window, RENEWAL_FIELD_NAMES maps the display names of your Ironclad record fields to the flow’s internal shape — Ironclad record metadata field ids are per-tenant, so this is the one setting nobody can ship a working default for. DEFAULT_NOTICE_DAYS is 90 and TIERS fires nudges at 90, 60, 30 and 7 days before the deadline. In Score + Route, the utilization bands (0.40 and 0.70), the price-uplift threshold (10%), and the escalation floor ($50,000 a year) decide what gets recommended and who hears about it.
Expect to tune those thresholds twice. Ship with the defaults, watch a quarter of routing decisions against what your team actually decided, and move the bands to match.
What the flow does
Daily Radar Run — 07:00 Weekdays fires. Load Radar State reads which contracts were already nudged and at which tier. Ironclad — Record Schema resolves your tenant’s field ids against RENEWAL_FIELD_NAMES, then Ironclad — List Records walks the executed-contract repository at /public/api/v1/records under the public.records.readRecords scope. Docusign — List Agreements does the same against the Agreement Manager API, whose Iris extraction returns expiration_date, renewal_type, renewal_notice_date and total_agreement_value as provisions. Merge Contract Sources appends both; disabling either node is how you run a single repository.
Normalize + Compute Notice Window is where the real work happens, and it is deliberately boring arithmetic rather than a model call. It maps both sources into one shape, then computes notice_deadline as expiration minus the notice period, on UTC calendar-date boundaries so that daylight saving cannot move a legal date. Contracts inside the 90-day horizon get a tier; everything else is dropped. New Tier Only then discards any contract already notified at its current tier, which is what keeps this from posting the same 40 contracts every weekday.
Load Spend + Usage pulls licensed and active seats, last-term and quoted-renewal values from vendor_context. Claude — Renewal Brief makes one Messages API call against claude-sonnet-5 with adaptive thinking at medium effort, returning a JSON brief with a recommendation, a rationale capped at 60 words, up to three negotiation points, and an explicit list of fields it needed and did not receive. The system prompt forbids any figure not present in the supplied JSON, which is the failure mode that makes AI-drafted commercial briefs dangerous.
Score + Route computes the recommendation deterministically from utilization and price change, then compares the model’s answer to it. The arithmetic decides the route; the model writes the prose. Disagreement is itself a routing signal — it forces the contract to legal with both recommendations shown side by side. Needs Legal Escalation splits to #legal-ops-renewals or #vendor-renewals, and both branches terminate at Upsert Radar Log, which upserts on contract_id so a re-run after a partial failure is safe.
Cost reality
Per brief: one Sonnet 5 call at 5 to 8k input tokens and 800 to 1,500 output tokens, plus adaptive thinking billed as output, so $0.05 to $0.07 at list pricing of $3 per million input and $15 per million output. A 400-contract portfolio typically pushes 30 to 40 contracts across a tier boundary monthly, which is $2 to $3 of inference.
The n8n side is worth understanding because it inverts the usual per-item economics. The fan-out across contracts happens inside a single workflow run, so a weekday cron costs about 22 executions a month against the 2,500 included in n8n Cloud Starter at €24/month — under 1% of the entry tier, with workflows and users unlimited on every Cloud plan as of 2026.
Against that, a single auto-renewal you did not intend on a mid-size contract is four figures at minimum. The economics are not the interesting part of this flow; the operational discipline is.
Success metric
Track the share of renewal decisions recorded in renewal_decisions where decided_on is on or before notice_deadline. The query is at the bottom of schema.sql. Your baseline is whatever fraction currently gets decided before the window closes, which for most teams starting this is not a number anybody has ever measured — capture it for a quarter before claiming improvement.
Do not track alerts sent. A flow that posts 40 messages nobody reads scores perfectly on that metric and has changed nothing. The secondary number worth watching is how many contracts arrive at their 7-day tier still undecided; if that count is not falling by the second quarter, the problem is ownership, not tooling.
vs alternatives
Against the renewal alerts built into Ironclad and Docusign: those fire reliably and cost nothing extra, but they notify on a date and stop there. They cannot reach seat utilization sitting in a vendor admin console or a price quote sitting in an email, and they do not produce a recommendation, so the owner still has to assemble the decision themselves. That assembly is the step that slips. If your team consistently acts on the native alert, use the native alert.
Against a SaaS management platform — the Zylo, Productiv and Vendr class — those buy you usage telemetry and renewal tracking as a product, which is genuinely more than this flow gives you on the usage side. They also cost real money, require their own integration project, and still hand the owner a dashboard rather than a decision. This flow is the right pick when you already have the contract data and the gap is the last mile.
Against a DIY script on a cron: same logic, and you then own the retries, the credential rotation, the pagination, and the observability. The specific reason to build this in n8n is the per-node retry semantics on the Anthropic call and the visual execution log — when a new contract shape breaks the Normalize node, you can see which record did it.
Watch-outs
A missing notice period is the one value that silently breaks the whole flow. Guard: Normalize + Compute Notice Window treats a null or negative notice period as DEFAULT_NOTICE_DAYS (90) rather than zero, and stamps notice_source: 'assumed'. Defaulting to zero would compute the notice deadline as the expiration date and mark the contract safe right up until it renews. Every card built on an assumed value says so on its first line, and assumed contracts are force-routed to legal regardless of value.
Ironclad record field ids are per-tenant, so a hardcoded mapping fails silently on someone else’s instance. Guard: the flow calls the record-schema endpoint at runtime and throws schema_field_missing if it cannot resolve the expiration or notice-period field. A loud failure at import is correct here; the alternative is a radar that runs clean and finds nothing.
Daily re-notification trains people to mute the channel, which produces exactly the missed deadline the flow exists to prevent. Guard: renewal_radar_log.last_tier_notified is read by the New Tier Only node, so each contract posts once per tier boundary — four messages over 90 days, not sixty.
The model can produce a fluent brief on a contract whose usage data never loaded. Guard: Score + Route computes the recommendation from arithmetic and treats the model’s answer as advisory; when context_complete is false the deterministic path returns renegotiate and never renew, because absent data is not evidence that the current terms are fine. Disagreement between the two sets model_agrees: false and forces escalation with both answers shown.
Alert fatigue’s quieter cousin: a brief that arrives with nobody’s name on it. Guard: owner_email is carried through from the repository and rendered on the owner-channel card. Contracts with no owner on file still post, but they land in the escalation channel — an unowned renewal is a legal-ops problem before it is a budget problem.
Stack
n8n orchestrates. Claude Sonnet 5 drafts the brief through the Anthropic Messages API. Ironclad and Docusign are the contract repositories; either alone is sufficient. Postgres holds the spend and usage context, the radar log, and the decision record. Slack receives the owner nudges and the legal escalations.
This is the operational layer on top of contract lifecycle management — the repository has to be populated and the notice periods have to be in it before any of this runs. Onboarding new vendors through a vendor due diligence workflow is what keeps those fields populated going forward; this flow is what covers the agreements you signed before anyone was doing that.
# 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;