What You’ll Build
A contract analysis pipeline that:- Uploads contracts to a searchable vault
- Extracts key terms (parties, dates, amounts, obligations)
- Identifies risky clauses with severity ratings
- Compares similar clauses across multiple contracts
- Generates a formatted risk report
Architecture
Prerequisites
- Case.dev API key (get one here)
- Contract documents (PDF, DOCX, or images)
Step 1: Create a Contract Vault
Set up a vault to store and index your contracts:casedev vault create --name "Contracts — Q1 2024"
import Casedev from 'casedev';
const client = new Casedev({ apiKey: process.env.CASEDEV_API_KEY });
const vault = await client.vault.create({
name: 'Contracts — Q1 2024'
});
console.log(`Vault ID: ${vault.id}`);
import os
import casedev
client = casedev.Casedev(api_key=os.environ['CASEDEV_API_KEY'])
vault = client.vault.create(name='Contracts — Q1 2024')
print(f'Vault ID: {vault.id}')
vault, _ := client.Vault.New(ctx, casedev.VaultNewParams{
Name: casedev.F("Contracts — Q1 2024"),
})
fmt.Println(vault.ID)
Step 2: Upload and Index a Contract
Upload a contract, run OCR if needed, and index it for search:casedev vault upload \
--id $VAULT_ID \
--filename "document.pdf" \
--content-type "application/pdf"
import fs from 'fs';
async function uploadContract(
vaultId: string,
filePath: string,
metadata?: Record<string, string>
) {
const filename = filePath.split('/').pop()!;
const contentType = filename.endsWith('.pdf') ? 'application/pdf' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
// 1. Upload to vault
const upload = await client.vault.upload(vaultId, {
filename,
contentType,
metadata: {
type: 'contract',
...metadata
}
});
await fetch(upload.uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': contentType },
body: fs.readFileSync(filePath)
});
// 2. Ingest (OCR + embedding generation)
await client.vault.ingest(vaultId, upload.objectId);
// 3. Wait for indexing
let obj = await client.vault.objects.retrieve(vaultId, upload.objectId);
while (obj.ingestionStatus === 'processing') {
await new Promise(r => setTimeout(r, 5000));
obj = await client.vault.objects.retrieve(vaultId, upload.objectId);
}
console.log(`Uploaded and indexed: ${filename} (${obj.ingestionStatus})`);
return { objectId: upload.objectId, filename };
}
import time
import requests
def upload_contract(vault_id: str, file_path: str, metadata: dict = None):
filename = os.path.basename(file_path)
content_type = 'application/pdf' if filename.endswith('.pdf') else 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
# 1. Upload to vault
upload = client.vault.upload(vault_id,
filename=filename,
content_type=content_type,
metadata={
'type': 'contract',
**(metadata or {})
}
)
with open(file_path, 'rb') as f:
requests.put(upload.upload_url, data=f,
headers={'Content-Type': content_type})
# 2. Ingest (OCR + embedding generation)
client.vault.ingest(upload.object_id, id=vault_id)
# 3. Wait for indexing
obj = client.vault.objects.retrieve(vault_id, upload.object_id)
while obj.ingestion_status == 'processing':
time.sleep(5)
obj = client.vault.objects.retrieve(vault_id, upload.object_id)
print(f'Uploaded and indexed: {filename} ({obj.ingestion_status})')
return {'object_id': upload.object_id, 'filename': filename}
upload, _ := client.Vault.Upload(ctx, vaultID, casedev.VaultUploadParams{
Filename: casedev.F("document.pdf"),
ContentType: casedev.F("application/pdf"),
})
// PUT file to upload.UploadURL via net/http
fmt.Println(upload.ObjectID)
Ingestion handles everything. Vault ingestion automatically runs OCR on scanned PDFs, chunks the text, and generates embeddings. You don’t need to call OCR separately.
Step 3: Extract Key Terms
Use vault search to retrieve the contract text and extract structured terms:# Search vault for key contract sections
casedev vault search --id $VAULT_ID \
--query "parties effective date termination payment obligations" \
--method hybrid --limit 15
# Then extract terms via LLM (pipe contract text as content)
casedev llm:v1:chat create-completion \
--model anthropic/claude-sonnet-4.5 \
--message '{role: system, content: "You are a contract analyst. Extract key terms and return as JSON."}' \
--message '{role: user, content: "<contract text from search results>"}' \
--temperature 0
async function extractKeyTerms(vaultId: string, objectId: string) {
// Search for key sections of this specific contract
const results = await client.vault.search(vaultId, {
query: 'parties effective date termination payment obligations liability governing law',
method: 'hybrid',
limit: 15
});
const contractText = results.chunks.map(c => c.text).join('\n\n');
const extraction = await client.llm.v1.chat.createCompletion({
model: 'anthropic/claude-sonnet-4.5',
messages: [
{
role: 'system',
content: `You are a contract analyst. Extract key terms and return as JSON:
{
"parties": [{"name": "...", "role": "..."}],
"effective_date": "YYYY-MM-DD",
"termination_date": "YYYY-MM-DD",
"value": {"amount": 0, "currency": "USD"},
"governing_law": "...",
"key_obligations": ["..."],
"termination_clauses": ["..."],
"renewal": {"type": "auto|manual|none", "notice_period": "..."},
"risk_flags": [{"clause": "...", "section": "...", "severity": "high|medium|low", "reason": "..."}]
}`
},
{ role: 'user', content: contractText }
],
temperature: 0
});
return JSON.parse(extraction.choices[0].message.content);
}
import json
def extract_key_terms(vault_id: str, object_id: str):
# Search for key sections of this specific contract
results = client.vault.search(vault_id,
query='parties effective date termination payment obligations liability governing law',
method='hybrid',
top_k=15
)
contract_text = '\n\n'.join(c.text for c in results.chunks)
extraction = client.llm.v1.chat.create_completion(
model='anthropic/claude-sonnet-4.5',
messages=[
{
'role': 'system',
'content': '''You are a contract analyst. Extract key terms and return as JSON:
{
"parties": [{"name": "...", "role": "..."}],
"effective_date": "YYYY-MM-DD",
"termination_date": "YYYY-MM-DD",
"value": {"amount": 0, "currency": "USD"},
"governing_law": "...",
"key_obligations": ["..."],
"termination_clauses": ["..."],
"renewal": {"type": "auto|manual|none", "notice_period": "..."},
"risk_flags": [{"clause": "...", "section": "...", "severity": "high|medium|low", "reason": "..."}]
}'''
},
{'role': 'user', 'content': contract_text}
],
temperature=0
)
return json.loads(extraction.choices[0].message.content)
// Search for key sections
results, _ := client.Vault.Search(ctx, vaultID, casedev.VaultSearchParams{
Query: casedev.F("parties effective date termination payment obligations liability governing law"),
Method: casedev.F(casedev.VaultSearchParamsMethodHybrid),
TopK: casedev.F(int64(15)),
})
// Build contract text from chunks
var contractText string
for _, chunk := range results.Chunks {
contractText += chunk.Text + "\n\n"
}
// Extract key terms via LLM
resp, _ := client.Llm.V1.Chat.NewCompletion(ctx, casedev.LlmV1ChatNewCompletionParams{
Model: casedev.F("anthropic/claude-sonnet-4.5"),
Messages: casedev.F([]casedev.LlmV1ChatNewCompletionParamsMessage{
{
Role: casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleSystem),
Content: casedev.F("You are a contract analyst. Extract key terms and return as JSON."),
},
{
Role: casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleUser),
Content: casedev.F(contractText),
},
}),
Temperature: casedev.F(float64(0)),
})
fmt.Println(resp.Choices[0].Message.Content)
Example Output
{
"parties": [
{"name": "Acme Corp", "role": "Vendor"},
{"name": "BigCo Inc", "role": "Client"}
],
"effective_date": "2024-01-01",
"termination_date": "2026-12-31",
"value": {"amount": 500000, "currency": "USD"},
"governing_law": "Delaware",
"key_obligations": [
"Vendor shall deliver software by Q2 2024",
"Client shall provide access to systems within 30 days"
],
"termination_clauses": [
"Either party may terminate with 90 days notice",
"Immediate termination for material breach"
],
"renewal": {"type": "auto", "notice_period": "60 days"},
"risk_flags": [
{
"clause": "Unlimited liability",
"section": "Section 8.2",
"severity": "high",
"reason": "No cap on liability exposes vendor to unlimited damages"
},
{
"clause": "Non-compete — 5 years",
"section": "Section 12.1",
"severity": "high",
"reason": "Non-compete extends 5 years post-termination, unusually long"
},
{
"clause": "Auto-renewal with price escalation",
"section": "Section 3.4",
"severity": "medium",
"reason": "Annual 8% price increase on auto-renewal without cap"
}
]
}
Step 4: Compare Clauses Across Contracts
Search your vault to find and compare similar clauses across multiple contracts:# Search for liability clauses across contracts
casedev vault search --id $VAULT_ID \
--query "liability clause terms conditions" \
--method hybrid --limit 20
# Compare via LLM
casedev llm:v1:chat create-completion \
--model anthropic/claude-sonnet-4.5 \
--message '{role: system, content: "You are a senior contract attorney. Compare liability clauses."}' \
--message '{role: user, content: "Compare these liability clauses: <clause text from search>"}' \
--temperature 0.3
async function compareClauses(vaultId: string, clauseType: string) {
// Search for a specific clause type across all contracts
const results = await client.vault.search(vaultId, {
query: `${clauseType} clause terms conditions`,
method: 'hybrid',
limit: 20
});
const chunks = results.chunks.map(c => ({
text: c.text,
source: c.filename
}));
const comparison = await client.llm.v1.chat.createCompletion({
model: 'anthropic/claude-sonnet-4.5',
messages: [
{
role: 'system',
content: `You are a senior contract attorney. Compare ${clauseType} clauses across multiple contracts.
For each contract:
- Summarize the clause terms
- Note any unusual or favorable/unfavorable provisions
- Rate the risk level (high/medium/low)
Then provide:
- A comparison table
- A recommendation on which terms are most favorable
- Suggested negotiation points for future contracts`
},
{
role: 'user',
content: `Compare these ${clauseType} clauses:\n\n${
chunks.map(c => `### ${c.source}\n${c.text}`).join('\n\n')
}`
}
],
temperature: 0.3
});
return comparison.choices[0].message.content;
}
// Example: Compare liability clauses across all contracts
const liabilityComparison = await compareClauses(vault.id, 'liability');
console.log(liabilityComparison);
def compare_clauses(vault_id: str, clause_type: str):
# Search for a specific clause type across all contracts
results = client.vault.search(vault_id,
query=f'{clause_type} clause terms conditions',
method='hybrid',
top_k=20
)
chunks = [{'text': c.text, 'source': c.filename} for c in results.chunks]
comparison = client.llm.v1.chat.create_completion(
model='anthropic/claude-sonnet-4.5',
messages=[
{
'role': 'system',
'content': f'''You are a senior contract attorney. Compare {clause_type} clauses across multiple contracts.
For each contract:
- Summarize the clause terms
- Note any unusual or favorable/unfavorable provisions
- Rate the risk level (high/medium/low)
Then provide:
- A comparison table
- A recommendation on which terms are most favorable
- Suggested negotiation points for future contracts'''
},
{
'role': 'user',
'content': f"Compare these {clause_type} clauses:\n\n" + '\n\n'.join(
f"### {c['source']}\n{c['text']}" for c in chunks
)
}
],
temperature=0.3
)
return comparison.choices[0].message.content
# Example: Compare liability clauses across all contracts
liability_comparison = compare_clauses(vault.id, 'liability')
print(liability_comparison)
// Search for a specific clause type across all contracts
results, _ := client.Vault.Search(ctx, vaultID, casedev.VaultSearchParams{
Query: casedev.F("liability clause terms conditions"),
Method: casedev.F(casedev.VaultSearchParamsMethodHybrid),
TopK: casedev.F(int64(20)),
})
// Build context from chunks
var clauseText string
for _, chunk := range results.Chunks {
clauseText += fmt.Sprintf("### %s\n%s\n\n", chunk.Filename, chunk.Text)
}
// Compare clauses via LLM
comparison, _ := client.Llm.V1.Chat.NewCompletion(ctx, casedev.LlmV1ChatNewCompletionParams{
Model: casedev.F("anthropic/claude-sonnet-4.5"),
Messages: casedev.F([]casedev.LlmV1ChatNewCompletionParamsMessage{
{
Role: casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleSystem),
Content: casedev.F("You are a senior contract attorney. Compare liability clauses across multiple contracts."),
},
{
Role: casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleUser),
Content: casedev.F("Compare these liability clauses:\n\n" + clauseText),
},
}),
Temperature: casedev.F(float64(0.3)),
})
fmt.Println(comparison.Choices[0].Message.Content)
Step 5: Generate a Risk Report
Combine all analysis into a formatted PDF report:# Generate a formatted PDF report from markdown
casedev format:v1 document \
--content "$(cat report.md)" \
--input-format md \
--output-format pdf \
--header "CONFIDENTIAL — Contract Risk Report" \
--footer "Page {{page}} of {{pages}}"
async function generateRiskReport(
vaultId: string,
contracts: Array<{ objectId: string; filename: string }>
) {
let reportContent = `# Contract Risk Report\n\n**Generated:** ${new Date().toLocaleDateString()}\n**Contracts analyzed:** ${contracts.length}\n\n---\n\n`;
// Analyze each contract
for (const contract of contracts) {
reportContent += `## ${contract.filename}\n\n`;
const terms = await extractKeyTerms(vaultId, contract.objectId);
reportContent += `**Parties:** ${terms.parties.map(p => `${p.name} (${p.role})`).join(', ')}\n`;
reportContent += `**Effective:** ${terms.effective_date} — ${terms.termination_date}\n`;
reportContent += `**Value:** ${terms.value.currency} ${terms.value.amount.toLocaleString()}\n`;
reportContent += `**Governing Law:** ${terms.governing_law}\n\n`;
if (terms.risk_flags.length > 0) {
reportContent += `### Risk Flags\n\n`;
reportContent += `| Severity | Clause | Section | Reason |\n|----------|--------|---------|--------|\n`;
for (const flag of terms.risk_flags) {
reportContent += `| **${flag.severity.toUpperCase()}** | ${flag.clause} | ${flag.section} | ${flag.reason} |\n`;
}
reportContent += '\n';
}
reportContent += '---\n\n';
}
// Cross-contract comparison
reportContent += `## Cross-Contract Clause Comparison\n\n`;
for (const clauseType of ['liability', 'termination', 'indemnification']) {
reportContent += `### ${clauseType.charAt(0).toUpperCase() + clauseType.slice(1)} Clauses\n\n`;
const comparison = await compareClauses(vaultId, clauseType);
reportContent += comparison + '\n\n';
}
// Convert to PDF
const report = await client.format.v1.document({
content: reportContent,
input_format: 'md',
output_format: 'pdf',
options: {
header: 'CONFIDENTIAL — Contract Risk Report',
footer: 'Page {{page}} of {{pages}}',
styles: {
h1: { font: 'Times New Roman', size: 16, bold: true, alignment: 'center' },
h2: { font: 'Times New Roman', size: 13, bold: true },
h3: { font: 'Times New Roman', size: 11, bold: true },
p: { font: 'Times New Roman', size: 10, spacingAfter: 6 }
}
}
});
return report;
}
from datetime import date
def generate_risk_report(vault_id: str, contracts: list[dict]):
report_content = f'# Contract Risk Report\n\n**Generated:** {date.today().isoformat()}\n**Contracts analyzed:** {len(contracts)}\n\n---\n\n'
# Analyze each contract
for contract in contracts:
report_content += f"## {contract['filename']}\n\n"
terms = extract_key_terms(vault_id, contract['object_id'])
parties = ', '.join(f"{p['name']} ({p['role']})" for p in terms['parties'])
report_content += f"**Parties:** {parties}\n"
report_content += f"**Effective:** {terms['effective_date']} — {terms['termination_date']}\n"
report_content += f"**Value:** {terms['value']['currency']} {terms['value']['amount']:,}\n"
report_content += f"**Governing Law:** {terms['governing_law']}\n\n"
if terms['risk_flags']:
report_content += '### Risk Flags\n\n'
report_content += '| Severity | Clause | Section | Reason |\n|----------|--------|---------|--------|\n'
for flag in terms['risk_flags']:
report_content += f"| **{flag['severity'].upper()}** | {flag['clause']} | {flag['section']} | {flag['reason']} |\n"
report_content += '\n'
report_content += '---\n\n'
# Cross-contract comparison
report_content += '## Cross-Contract Clause Comparison\n\n'
for clause_type in ['liability', 'termination', 'indemnification']:
report_content += f'### {clause_type.capitalize()} Clauses\n\n'
comparison = compare_clauses(vault_id, clause_type)
report_content += comparison + '\n\n'
# Convert to PDF
report = client.format.v1.create_document(
content=report_content,
input_format='md',
output_format='pdf',
options={
'header': 'CONFIDENTIAL — Contract Risk Report',
'footer': 'Page {{page}} of {{pages}}',
'styles': {
'h1': {'font': 'Times New Roman', 'size': 16, 'bold': True, 'alignment': 'center'},
'h2': {'font': 'Times New Roman', 'size': 13, 'bold': True},
'h3': {'font': 'Times New Roman', 'size': 11, 'bold': True},
'p': {'font': 'Times New Roman', 'size': 10, 'spacingAfter': 6}
}
}
)
return report
// Build report content from analysis (see extractKeyTerms + compareClauses above)
reportContent := fmt.Sprintf("# Contract Risk Report\n\n**Generated:** %s\n\n---\n\n", time.Now().Format("2006-01-02"))
// ... add per-contract analysis and cross-contract comparisons ...
// Convert to PDF
report, _ := client.Format.V1.NewDocument(ctx, casedev.FormatV1NewDocumentParams{
Content: casedev.F(reportContent),
InputFormat: casedev.F("md"),
OutputFormat: casedev.F("pdf"),
Options: casedev.F(casedev.FormatV1NewDocumentParamsOptions{
Header: casedev.F("CONFIDENTIAL — Contract Risk Report"),
Footer: casedev.F("Page {{page}} of {{pages}}"),
}),
})
// report is *http.Response with PDF body
Production Tips
Error Handling
# CLI displays errors to stderr with status codes
casedev vault search --id $VAULT_ID --query "key terms"
# Error: 404 Not Found — Contract not found in vault
# Error: 429 Too Many Requests — retry after a delay
try {
const terms = await extractKeyTerms(vaultId, objectId);
} catch (error) {
if (error.status === 404) {
console.error('Contract not found in vault');
} else if (error.status === 429) {
console.error('Rate limited — retry after a delay');
} else {
console.error('Extraction failed:', error.message);
}
}
try:
terms = extract_key_terms(vault_id, object_id)
except casedev.NotFoundError:
print('Contract not found in vault')
except casedev.RateLimitError:
print('Rate limited — retry after a delay')
except casedev.APIError as e:
print(f'Extraction failed: {e.message}')
terms, err := extractKeyTerms(ctx, vaultID, objectID)
if err != nil {
var apiErr *casedev.Error
if errors.As(err, &apiErr) {
switch apiErr.StatusCode {
case 404:
fmt.Println("Contract not found in vault")
case 429:
fmt.Println("Rate limited — retry after a delay")
default:
fmt.Printf("Extraction failed: %s\n", apiErr.Message)
}
}
}
Use Webhooks for Production Pipelines
Instead of polling for ingestion status, subscribe to vault events:casedev vault:events:subscriptions create \
--id $VAULT_ID \
--callback-url "https://your-app.com/webhooks/vault" \
--event-type object.ingested \
--event-type object.failed
await client.vault.events.subscriptions.create(vaultId, {
url: 'https://your-app.com/webhooks/vault',
events: ['object.ingested', 'object.failed']
});
client.vault.events.subscriptions.create(vault_id,
callback_url='https://your-app.com/webhooks/vault',
event_types=['object.ingested', 'object.failed']
)
client.Vault.Events.Subscriptions.New(ctx, vaultID, casedev.VaultEventSubscriptionNewParams{
CallbackURL: casedev.F("https://your-app.com/webhooks/vault"),
EventTypes: casedev.F([]string{"object.ingested", "object.failed"}),
})
Use
temperature: 0 for key term extraction and risk identification. The lower temperature ensures more deterministic, factual outputs.Next Steps
- Vault Search Reference — Advanced search methods and filtering
- Chat Completions — Streaming, structured output, and more
- Format Service — PDF and DOCX generation
- Discovery Pipeline — Batch document processing

