Pull every redline from a contract
Redlined contracts are exactly the long-tail complexity that breaks template-based pipelines. Reducto reads every strikethrough, underline, and annotation as structured tags, so your team surfaces all 500+ revisions in code instead of page by page.
Contract negotiations leave a long trail of edits, and somewhere in 165 pages of redlines is every change that actually matters. Reading them by hand is slow and easy to get wrong. This cookbook hands that work to Reducto: parse a redlined contract once and get back every insertion, deletion, and replacement as a structured list your code can sort, count, and route.
What you'll build
markdownRedlined contract (PDF or DOCX) → Reducto Parse with change_tracking → Output tagged with <change>, <s>, <u> → A structured list of every revision
Setup
Grab an API key from studio.reducto.ai → API Keys → Create new API key, then set it and install the SDK:
bashexport REDUCTO_API_KEY="your-api-key-here" pip install reducto # Python npm install reducto # JavaScript
Reducto handles both Word docs (native track-changes metadata, most accurate) and PDFs (visual detection of underlines and strikethroughs), with no templates or per-document setup. Need a sample? Use this 165-page union labor agreement with extensive redlines.
Step 1: Parse with change tracking
The one setting that matters is formatting.include: ["change_tracking"]. It tells Reducto to wrap revisions in HTML tags instead of returning flat text.
pythonfrom pathlib import Path from reducto import Reducto client = Reducto() upload = client.upload(file=Path("redlined_contract.pdf")) result = client.parse.run( input=upload.file_id, formatting={"include": ["change_tracking"]}, )
javascriptimport Reducto from "reducto"; import fs from "fs"; const client = new Reducto(); const upload = await client.upload({ file: fs.createReadStream("redlined_contract.pdf"), }); const result = await client.parse.run({ input: upload.file_id, formatting: { include: ["change_tracking"] }, });
Large documents come back as a result URL to keep responses light. Fetch it and stitch the chunks together:
pythonimport requests if result.result.type == "url": data = requests.get(result.result.url).json() full_content = "\n".join(c.get("content", "") for c in data.get("chunks", [])) else: full_content = "\n".join(c.content for c in result.result.chunks)
javascriptlet fullContent; if (result.result.type === "url") { const data = await (await fetch(result.result.url)).json(); fullContent = (data.chunks || []).map((c) => c.content || "").join("\n"); } else { fullContent = result.result.chunks.map((c) => c.content).join("\n"); }
Step 2: Read the tags
Every revision shows up as simple, predictable markup:
| Tag | Meaning | Example |
|---|---|---|
<s> | Deletion | <change><s>forty-eight (48)</s></change> |
<u> | Insertion | <change><u>and any PEOPLE deduction</u></change> |
<change> | Revision group | <change><s>forty-eight (48)</s> <u>fifty (50)</u></change> |
A single <change> block can hold a deletion, an insertion, or both (a replacement).
Step 3: Extract changes in code
Parse the tags into a structured list of deletions and insertions:
pythonimport re def extract_changes(content): changes = [] for match in re.finditer(r"<change>(.*?)</change>", content, re.DOTALL): block = match.group(1) changes.append({ "deleted": re.findall(r"<s>(.*?)</s>", block, re.DOTALL), "inserted": re.findall(r"<u>(.*?)</u>", block, re.DOTALL), }) return changes
javascriptfunction extractChanges(content) { const changes = []; const blocks = content.matchAll(/<change>([\s\S]*?)<\/change>/g); for (const [, block] of blocks) { changes.push({ deleted: [...block.matchAll(/<s>([\s\S]*?)<\/s>/g)].map((m) => m[1]), inserted: [...block.matchAll(/<u>([\s\S]*?)<\/u>/g)].map((m) => m[1]), }); } return changes; }
Then categorize to prioritize review. Replacements usually need the closest look:
pythonchanges = extract_changes(full_content) deletions = sum(1 for c in changes if c["deleted"] and not c["inserted"]) insertions = sum(1 for c in changes if c["inserted"] and not c["deleted"]) replacements = sum(1 for c in changes if c["deleted"] and c["inserted"]) print(f"{len(changes)} revisions: {deletions} deletions, " f"{insertions} insertions, {replacements} replacements")
On the sample contract that's 555 revisions across 165 pages (191 deletions, 270 insertions, and 94 replacements), from a single Parse call, zero-shot, with no template to maintain.
Prefer no code?
Do the same thing in Reducto Studio: upload your contract, open Configurations → Advanced → Formatting, check change_tracking, and Run. The output shows the <change>, <s>, and <u> tags inline. Export as JSON or deploy the config for repeated use.
Where this goes next
Change tracking is one task on one platform. Once the revisions are structured, chain the rest of the toolkit onto the same pipeline:
- Pair with Extract: pull grounded, cited fields (dates, amounts, parties) straight from the changed clauses.
- Route by clause type: use Classify to send indemnification changes to legal, pricing to finance.
- Build approval queues: accept or reject each revision, with human-in-the-loop where it matters.
- Summarize negotiations: brief stakeholders without making them read all 165 pages.