Build a contract-clause risk analyzer
Flag risky terms in Northwind contract text — auto-renewal, exclusivity, broad IP grants.
Published Nov 7, 2025
When a client sends Northwind a contract to sign, the dangerous clauses are rarely the ones in bold. They are the quiet ones buried in the middle — a silent auto-renewal, an exclusivity term, an IP grant broader than the project warrants. Spotting them means reading the whole document carefully, and on a busy week that careful read does not always happen.
This script gives every incoming contract a first pass. It pulls the text out of a Google Doc, asks Claude to identify clauses that carry risk for Northwind, and returns each one with a low/medium/high rating and a plain-English reason. It is not a substitute for a lawyer — it is the triage step that tells you which contracts need one.
What you’ll need
- The contract you want to check, as a Google Doc.
- An Anthropic API key saved as
ANTHROPIC_API_KEYin Script Properties — see Store API keys and secrets securely. - The Doc ID, copied from the document’s URL.
The script
// Model used for the risk analysis. Sonnet is worth the extra cost here —
// judging contract risk needs reasoning over the whole document.
const RISK_MODEL = 'claude-sonnet-4-6';
// Characters of contract text to send. Keeps the prompt within a sensible
// token budget — see "Watch out for".
const MAX_CONTRACT_CHARS = 12000;
/**
* Reads a contract Doc and asks Claude to identify risky clauses.
*
* @param {string} docId - The Google Doc ID of the contract.
* @return {Object[]} One object per risky clause:
* {clause: string, risk: "low"|"medium"|"high", why: string}.
*/
function analyseRisk(docId) {
// 1. Pull the contract text, capped to a sensible length.
const text = DocumentApp.openById(docId)
.getBody()
.getText()
.slice(0, MAX_CONTRACT_CHARS);
if (!text.trim()) {
Logger.log('Contract Doc is empty — nothing to analyse.');
return [];
}
// 2. Ask Claude for strict JSON. A fixed schema is the difference between
// a parseable result and a parsing nightmare.
const prompt =
'Identify risky clauses for Northwind in this contract. ' +
'Return ONLY a JSON array — no prose, no markdown — in this shape: ' +
'[{"clause": string, "risk": "low|medium|high", "why": string}]\n\n' +
text;
// 3. Sonnet does the reasoning; parse the reply into real objects.
return JSON.parse(callClaude(prompt, RISK_MODEL, 1500));
}
/**
* Convenience driver: analyses one contract Doc and logs the findings,
* worst risks first.
*/
function reviewContract() {
const DOC_ID = '1abcContractDocId'; // the contract to review
const findings = analyseRisk(DOC_ID);
if (!findings.length) {
Logger.log('No risky clauses identified.');
return;
}
// Sort high -> medium -> low so the worst risks read first.
const order = { high: 0, medium: 1, low: 2 };
findings.sort((a, b) => order[a.risk] - order[b.risk]);
findings.forEach((f) => {
Logger.log('[' + f.risk.toUpperCase() + '] ' + f.clause + ' — ' + f.why);
});
}
/**
* Minimal Anthropic API call. The key lives in Script Properties — it is
* never pasted into the code.
*/
function callClaude(prompt, model = 'claude-haiku-4-5-20251001', maxTokens = 400) {
const key = PropertiesService.getScriptProperties()
.getProperty('ANTHROPIC_API_KEY');
const res = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
method: 'post',
contentType: 'application/json',
headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' },
payload: JSON.stringify({
model,
max_tokens: maxTokens,
messages: [{ role: 'user', content: prompt }],
}),
muteHttpExceptions: true,
});
return JSON.parse(res.getContentText()).content[0].text.trim();
}
How it works
analyseRiskopens the contract Doc and reads its full text, then trims it toMAX_CONTRACT_CHARSso the prompt stays within a sensible token budget.- If the Doc turns out to be empty it logs a message and returns an empty array — no wasted API call.
- It builds a prompt that pins the output to a strict JSON schema: an array of
objects, each with a
clause, ariskrating and awhyexplanation. - It calls Claude Sonnet via
callClaude. Sonnet is the right choice here because judging contract risk needs reasoning across the whole document, not a quick classification. JSON.parseturns the reply into real objects, whichanalyseRiskreturns.reviewContractis a thin driver: it runsanalyseRiskon one Doc, sorts the findings so high-risk clauses appear first, and logs each one in a readable line.callClaudeis a small wrapper around the Anthropic Messages API; the key comes from Script Properties, never the code.
Example run
Run reviewContract against a typical client agreement and the execution log
fills with something like:
[HIGH] Auto-renewal — Contract renews automatically for 12 months unless
cancelled 90 days in advance, with no notice to Northwind.
[HIGH] IP assignment — Assigns all Northwind background IP to the client,
not just the deliverables produced under this contract.
[MEDIUM] Exclusivity — Bars Northwind from working with competing firms for
the contract term, which is not defined.
[LOW] Payment terms — 30-day payment window; longer than Northwind's
standard 14 days but not unusual.
That is the triage list: two clauses that need a lawyer before signing, one to negotiate, and one that is merely worth noting.
Run it
This is a per-contract check, run by hand when a contract arrives:
- Paste the contract’s Doc ID into the
DOC_IDconstant insidereviewContract. - In the Apps Script editor, select
reviewContractand click Run. - Approve the authorisation prompt the first time.
- Open Executions to read the findings in the log.
To analyse contracts in bulk, call analyseRisk from your own loop over a list
of Doc IDs and write each result set to a sheet instead of the log.
Watch out for
- This is triage, not legal advice. Claude can miss a risk or over-flag a harmless clause — every contract still needs a human, and anything rated medium or high needs a lawyer.
- The text is capped at
MAX_CONTRACT_CHARS(12,000). A long contract is truncated, so risks in the final pages may be missed — raise the cap andmax_tokenstogether for longer documents, or analyse in sections. JSON.parsethrows if Claude wraps the array in a markdown code fence. If that happens, strip the fence before parsing rather than reaching for regex.- Risk ratings are judgements, not facts. Treat them as a way to prioritise which clauses to read first, not as a final verdict.
- Sonnet costs more per call than Haiku. That trade-off is deliberate here, but it means a large batch of contracts adds up — analyse only what you need to.
Related
Build an AI keyword-clustering tool
Group Northwind's tracked search terms into topic clusters — for SEO content planning.
Updated Feb 19, 2026
Build an AI customer-churn predictor
Flag at-risk Northwind accounts from behavioural signals — usage, support tickets, billing.
Updated Feb 15, 2026
Build a context-aware AI data validator
Catch values that look wrong in context — '£10' for a Northwind retainer is suspicious.
Updated Feb 7, 2026
Auto-categorize a photo library
Tag Northwind Drive images by visual content — product, team, event, behind-the-scenes.
Updated Feb 3, 2026
Build an AI bug-triage system
Categorise and prioritise Northwind's reported issues automatically — type, severity, owner.
Updated Jan 22, 2026