appscript.dev
Automation Advanced Docs

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_KEY in 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

  1. analyseRisk opens the contract Doc and reads its full text, then trims it to MAX_CONTRACT_CHARS so the prompt stays within a sensible token budget.
  2. If the Doc turns out to be empty it logs a message and returns an empty array — no wasted API call.
  3. It builds a prompt that pins the output to a strict JSON schema: an array of objects, each with a clause, a risk rating and a why explanation.
  4. 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.
  5. JSON.parse turns the reply into real objects, which analyseRisk returns.
  6. reviewContract is a thin driver: it runs analyseRisk on one Doc, sorts the findings so high-risk clauses appear first, and logs each one in a readable line.
  7. callClaude is 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:

  1. Paste the contract’s Doc ID into the DOC_ID constant inside reviewContract.
  2. In the Apps Script editor, select reviewContract and click Run.
  3. Approve the authorisation prompt the first time.
  4. 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 and max_tokens together for longer documents, or analyse in sections.
  • JSON.parse throws 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