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)
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
- Discovery Pipeline — Batch document processing

