What You’ll Build
A script that:
Searches your indexed documents in a vault
Analyzes the results with an LLM
Returns structured insights with source citations
Time to complete: 15 minutes
Architecture
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Your App │ ──▶ │ Vault Search │ ──▶ │ LLM │
│ │ │ (Hybrid) │ │ Analysis │
│ vaultId │ │ 10 chunks │ │ Structured │
│ query │ │ + sources │ │ response │
└──────────────┘ └──────────────┘ └──────────────┘
Prerequisites
Case.dev API key (get one here )
A vault with ingested documents (we’ll set one up if you don’t have one)
Step 1: Set Up Your Vault
If you already have a vault with indexed documents, skip to Step 2.
Create a vault
TypeScript
Python
cURL
Go
CLI
import Casedev from 'casedev' ;
const client = new Casedev ({ apiKey: process.env. CASEDEV_API_KEY });
const vault = await client.vault. create ({
name: 'Legal Research Vault'
});
console. log ( `Vault ID: ${ vault . id }` );
Save the returned id — you’ll need it.
Upload a document
TypeScript
Python
cURL
Go
CLI
// Get upload URL
const upload = await client.vault. upload (vault.id, {
filename: 'contract.pdf' ,
contentType: 'application/pdf'
});
// Upload the file directly to S3
await fetch (upload.uploadUrl, {
method: 'PUT' ,
headers: { 'Content-Type' : 'application/pdf' },
body: fs. readFileSync ( 'contract.pdf' )
});
console. log ( `Object ID: ${ upload . objectId }` );
Ingest (index) the document
TypeScript
Python
cURL
Go
CLI
await client.vault. ingest (vault.id, upload.objectId);
// Poll until complete
let obj = await client.vault.objects. retrieve (vault.id, upload.objectId);
while (obj.ingestionStatus === 'processing' ) {
await new Promise ( r => setTimeout (r, 5000 ));
obj = await client.vault.objects. retrieve (vault.id, upload.objectId);
}
console. log ( `Ingestion: ${ obj . ingestionStatus }` );
Ingestion is async. Wait for ingestionStatus: "completed" before searching. For production, use webhooks instead of polling.
Step 2: Search Your Documents
Query your vault with a natural language question:
TypeScript
Python
cURL
Go
CLI
const searchResults = await client.vault. search (vaultId, {
query: 'What are the key terms of this agreement?' ,
method: 'hybrid' ,
limit: 10
});
console. log ( `Found ${ searchResults . chunks . length } relevant passages` );
{
"chunks" : [
{
"text" : "The Parties agree to the following terms..." ,
"object_id" : "obj_xyz789" ,
"hybridScore" : 0.89 ,
"vectorScore" : 0.92 ,
"bm25Score" : 0.78
}
],
"sources" : [
{ "id" : "obj_xyz789" , "filename" : "contract.pdf" }
]
}
Step 3: Analyze with an LLM
Pass the search results to an LLM for structured analysis:
const chunks = searchResults.chunks. map ( c => c.text). join ( ' \n\n ' );
const sources = searchResults.sources. map ( s => s.filename). join ( ', ' );
const analysis = await client.llm.v1.chat. createCompletion ({
model: 'anthropic/claude-sonnet-4.5' ,
messages: [
{
role: 'system' ,
content: 'You are a legal document analyst. Analyze the provided document excerpts and answer the user \' s question. Always cite specific passages to support your analysis.'
},
{
role: 'user' ,
content: `## Document Excerpts \n\n ${ chunks } \n\n ## Sources \n ${ sources } \n\n ## Question \n What are the key terms of this agreement?`
}
],
temperature: 0.3
});
console. log (analysis.choices[ 0 ].message.content);
{
"choices" : [{
"message" : {
"content" : "Based on the contract excerpts, the key terms include: \n\n 1. **Payment Terms**: Section 3.2 states that payment is due within 30 days... \n\n 2. **Termination**: Either party may terminate with 90 days written notice (Section 7.1)... \n\n 3. **Liability Cap**: Liability is limited to the total fees paid in the preceding 12 months (Section 9.3)..."
}
}],
"usage" : {
"prompt_tokens" : 1245 ,
"completion_tokens" : 387 ,
"total_tokens" : 1632 ,
"cost" : 0.004896
}
}
Complete Example
Putting it all together — a reusable function that searches and analyzes:
import Casedev from 'casedev' ;
const client = new Casedev ({ apiKey: process.env. CASEDEV_API_KEY });
async function analyzeDocuments ( vaultId : string , query : string ) {
// 1. Search
const searchResults = await client.vault. search (vaultId, {
query,
method: 'hybrid' ,
limit: 10
});
const chunks = searchResults.chunks. map ( c => c.text). join ( ' \n\n ' );
const sources = searchResults.sources. map ( s => s.filename). join ( ', ' );
// 2. Analyze
const analysis = await client.llm.v1.chat. createCompletion ({
model: 'anthropic/claude-sonnet-4.5' ,
messages: [
{
role: 'system' ,
content: `You are a senior legal analyst. Provide comprehensive analysis with:
1. Executive Summary
2. Key Findings (cite specific passages)
3. Supporting Evidence
4. Recommendations`
},
{
role: 'user' ,
content: `## Document Excerpts \n\n ${ chunks } \n\n ## Sources \n ${ sources } \n\n ## Question \n ${ query }`
}
],
temperature: 0.3
});
return {
answer: analysis.choices[ 0 ].message.content,
sources: searchResults.sources,
usage: analysis.usage
};
}
// Run it
const result = await analyzeDocuments ( 'vault_abc123' , 'What are the indemnification clauses?' );
console. log (result.answer);
Extending the Analyzer
Run a second LLM pass to extract structured entities:
const entities = await client.llm.v1.chat. createCompletion ({
model: 'openai/gpt-4o' ,
messages: [
{
role: 'system' ,
content: 'Extract named entities as JSON: {people: [], organizations: [], dates: [], locations: [], monetary_amounts: []}'
},
{ role: 'user' , content: chunks }
],
temperature: 0
});
Generate a PDF Report
Convert the analysis into a formatted document:
const report = await client.format.v1. document ({
content: `# Legal Analysis Report \n\n **Query:** ${ query } \n\n ${ analysis . choices [ 0 ]. message . content }` ,
input_format: 'md' ,
output_format: 'pdf'
});
Production Tips
Error Handling
try {
const result = await analyzeDocuments (vaultId, query);
console. log (result.answer);
} catch (error) {
if (error.status === 404 ) {
console. error ( 'Vault not found — check your vault ID' );
} else if (error.status === 429 ) {
console. error ( 'Rate limited — retry after a delay' );
} else {
console. error ( 'Analysis failed:' , error.message);
}
}
Use temperature: 0 for factual extraction tasks. Try cheaper models like deepseek/deepseek-chat for simpler analysis.
Next Steps