Skip to main content

What You’ll Build

A contract analysis pipeline that:
  1. Uploads contracts to a searchable vault
  2. Extracts key terms (parties, dates, amounts, obligations)
  3. Identifies risky clauses with severity ratings
  4. Compares similar clauses across multiple contracts
  5. Generates a formatted risk report
Time to complete: 25 minutes

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