Trigger icon
Schedule icon
Switch icon
Fail icon
ChatCompletion icon
Anthropic icon
Script icon
If icon
SlackIncomingWebhook icon
Pause icon
Set icon
GetKeys icon
Loop icon

Contract Renewal Radar to Read Contracts with AI, Track Notice Deadlines, and Decide Renewals Before They Auto-Renew

Read signed contracts with AI, track every notice deadline, and get renew, renegotiate or cancel decisions before contracts auto-renew.

Categories
AIBusiness

Most software and service contracts renew automatically unless someone gives notice 30 to 90 days before the end of the term. The date is buried in a clause nobody rereads, so companies discover a renewal when the invoice arrives and are locked in for another year. This blueprint gives procurement, finance and legal teams a contract registry that reads each signed PDF, works out the notice deadline, and asks the owner for a decision while there is still time to act.

How it works

Filing a contract (ingest mode: a PDF dropped into the S3 inbox, or uploaded manually):

  1. new_contract (io.kestra.plugin.aws.s3.Trigger) picks up each PDF in contracts/inbox/ and moves it to contracts/filed/, so it's read once.
  2. read_contract (io.kestra.plugin.ai.completion.ChatCompletion with a PDF content block) extracts the vendor, value, dates, auto-renewal, renewal term, notice period, notice method and address, and quotes the renewal clause.
  3. check_terms (io.kestra.plugin.scripts.python.Script) checks the extraction in code. Missing dates, a notice period that isn't a number such as "reasonable notice", or low confidence send it to review (io.kestra.plugin.core.flow.Pause), where a person corrects the end date, auto-renewal, notice period or renewal term.
  4. finalize computes the notice deadline, and rolls forward a term that has already renewed. save_contract (io.kestra.plugin.core.kv.Set) files it in the KV registry, and post_filed posts the deadline to Slack. A contract already on file is never overwritten.

The daily check (daily mode, io.kestra.plugin.core.trigger.Schedule at 08:00):

  1. registry (io.kestra.plugin.core.kv.GetKeys) loads every contract. plan:
    • rolls renewed contracts forward;
    • closes ended and cancelled ones;
    • flags missed notice deadlines;
    • finds every contract whose deadline falls inside decision_window_days;
    • writes renewals.csv.
  2. digest posts the upcoming deadlines to Slack, only on days with something to report.
  3. decisions (Pause) shows one form with that day's due contracts in three dynamic MULTISELECT lists: Cancel, Renegotiate and Renew, plus a note. The form closes after 23 hours. A contract left unpicked is asked again the next day, and is escalated in Slack when its deadline is 7 days or less away.
  4. decide records each decision with the note and time. For each cancellation, it drafts a termination notice that uses the contract's own notice method, address, deadline and clause. update_registry (io.kestra.plugin.core.flow.Loop) writes the changed entries back, and post_result reports decisions and escalations.

Failures go to Slack through alert_failure.

Prerequisites

  • An Anthropic API key, and Docker on the worker for the Python scripts.
  • For the inbox trigger: an S3 bucket (or an S3-compatible store) with a contracts/inbox/ prefix. Set bucket, s3_endpoint and region under variables.
  • Secrets:
    • ANTHROPIC_API_KEY: reads the contract PDFs.
    • S3_ACCESS_KEY_ID and S3_SECRET_KEY_ID: read the inbox and move filed PDFs; needed only for the new_contract trigger.
    • SLACK_WEBHOOK_URL: Slack incoming webhook for filed contracts, review requests, the digest, decisions, escalations and failure alerts.

Inputs

  • mode (SELECT ingest or daily, default ingest): daily is what the schedule runs.
  • contract (FILE, optional): a contract PDF for a manual ingest run.
  • owner (STRING, optional): the person who decides about this contract, shown in reminders. Contracts from the S3 trigger are unassigned.
  • decision_window_days (INT, default 45): how many days before the notice deadline to start asking.
  • dry_run (BOOL, default false): read and report only. Nothing is filed, no decision is requested and the registry is unchanged. Nothing is ever sent to a vendor; termination notices are drafts for a person to send.

Quick start

  1. Add the secrets and save the flow.
  2. Run ingest with a contract PDF and dry_run set to true, and read the extracted deadline in Slack or in the finalize outputs.
  3. Run it again without the dry run to file it, then file a few more.
  4. Run daily, decide in the form, and check the drafted notice in the decide output files.
  5. Set the bucket variables, then enable new_contract and daily.

Outputs

  • {{ outputs.filed_contract }} / {{ outputs.finalize.vars.key }}: the registry key of the filed contract.
  • {{ outputs.notice_deadline }} / {{ outputs.finalize.vars.notice_deadline }}: its notice deadline.
  • {{ outputs.due }} / {{ outputs.plan.vars.due }} and {{ outputs.plan.vars.decision_options }}: contracts that need a decision.
  • {{ outputs.calendar }} / {{ outputs.plan.outputFiles['renewals.csv'] }}: every contract, sorted by deadline.
  • {{ outputs.decide.outputFiles }}: one drafted termination notice per cancellation (notices/<key>.txt).
  • KV keys contract-<vendor>-<hash>: the registry, one JSON entry per contract with its status (active, awaiting decision, cancelling, renegotiating, renewing, notice missed, ended), decision and history.

Common pitfalls

  • Always check a contract's deadline in the first Slack message: the model can misread a date, and only unclear terms are sent to review automatically.
  • Notice periods in months are counted as 30 days, which errs on the early side. Correct the notice period in the review form if a contract defines months differently.
  • Re-dropping a PDF that is already on file changes nothing. To re-file a contract with new terms, delete its KV entry first, or upload the amended contract (a new file gets a new key).
  • A Cancel decision only drafts the notice. Someone must still send it, by the method the contract requires, before the deadline.
  • Contracts from the S3 trigger have no owner. Use the manual upload when the owner matters for reminders.

How to extend

  • Email the drafted notice to the owner, or open a ticket for the cancellation.
  • Store renewals.csv in S3 or a spreadsheet after each daily run.
  • Read renewal price caps and flag contracts with large automatic increases.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.