Skip to main content

What You’ll Build

A script that:
  1. Searches your indexed documents in a vault
  2. Analyzes the results with an LLM
  3. Returns structured insights with source citations
Time to complete: 15 minutes

Architecture

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

curl -X POST https://api.case.dev/vault \
  -H "Authorization: Bearer $CASEDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Legal Research Vault"}'
casedev vault create --name "Legal Research Vault"
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}`);
import os
import casedev

client = casedev.Casedev(api_key=os.environ['CASEDEV_API_KEY'])

vault = client.vault.create(name='Legal Research Vault')

print(f'Vault ID: {vault.id}')
vault, _ := client.Vault.New(ctx, casedev.VaultNewParams{
	Name: casedev.F("Legal Research Vault"),
})
fmt.Println(vault.ID)
Save the returned id — you’ll need it.

Upload a document

# Get upload URL
UPLOAD_RESPONSE=$(curl -s -X POST "https://api.case.dev/vault/$VAULT_ID/upload" \
  -H "Authorization: Bearer $CASEDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"filename": "contract.pdf", "contentType": "application/pdf"}')

UPLOAD_URL=$(echo $UPLOAD_RESPONSE | jq -r '.uploadUrl')
OBJECT_ID=$(echo $UPLOAD_RESPONSE | jq -r '.objectId')

# Upload your file
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @contract.pdf
casedev vault upload \
  --id $VAULT_ID \
  --filename "document.pdf" \
  --content-type "application/pdf"
// 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}`);
import requests

# Get upload URL
upload = client.vault.upload(vault.id,
    filename='contract.pdf',
    content_type='application/pdf'
)

# Upload the file directly to S3
with open('contract.pdf', 'rb') as f:
    requests.put(upload.upload_url, data=f,
        headers={'Content-Type': 'application/pdf'})

print(f'Object ID: {upload.object_id}')
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)

Ingest (index) the document

curl -X POST "https://api.case.dev/vault/$VAULT_ID/ingest/$OBJECT_ID" \
  -H "Authorization: Bearer $CASEDEV_API_KEY"

# Check status (poll until completed)
curl "https://api.case.dev/vault/$VAULT_ID/objects/$OBJECT_ID" \
  -H "Authorization: Bearer $CASEDEV_API_KEY" | jq '.ingestionStatus'
casedev vault ingest --id $VAULT_ID --object-id $OBJECT_ID
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}`);
import time

client.vault.ingest(upload.object_id, id=vault.id)

# Poll until complete
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'Ingestion: {obj.ingestion_status}')
result, _ := client.Vault.Ingest(ctx, objectID, casedev.VaultIngestParams{
	ID: casedev.F(vaultID),
})
fmt.Println(result.Status)
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:
curl -X POST "https://api.case.dev/vault/$VAULT_ID/search" \
  -H "Authorization: Bearer $CASEDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "What are the key terms of this agreement?",
    "method": "hybrid",
    "limit": 10
  }'
casedev vault search \
  --id $VAULT_ID \
  --query "What are the key terms of this agreement?"
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`);
search_results = client.vault.search(vault_id,
    query='What are the key terms of this agreement?',
    method='hybrid',
    top_k=10
)

print(f'Found {len(search_results.chunks)} relevant passages')
results, _ := client.Vault.Search(ctx, vaultID, casedev.VaultSearchParams{
	Query: casedev.F("What are the key terms of this agreement?"),
	Method: casedev.F(casedev.VaultSearchParamsMethodHybrid),
})
for _, chunk := range results.Chunks {
	fmt.Println(chunk.Text)
}
Response
{
  "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:
casedev llm:v1:chat create-completion \
  --model anthropic/claude-sonnet-4.5 \
  --message '{role: system, content: "You are a legal document analyst. Cite specific passages to support your analysis."}' \
  --message '{role: user, content: "<document excerpts>\n\nQuestion: What are the key terms of this agreement?"}' \
  --temperature 0.3
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\nWhat are the key terms of this agreement?`
    }
  ],
  temperature: 0.3
});

console.log(analysis.choices[0].message.content);
chunks = '\n\n'.join(c.text for c in search_results.chunks)
sources = ', '.join(s.filename for s in search_results.sources)

analysis = client.llm.v1.chat.create_completion(
    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': f'## Document Excerpts\n\n{chunks}\n\n## Sources\n{sources}\n\n## Question\nWhat are the key terms of this agreement?'
        }
    ],
    temperature=0.3
)

print(analysis.choices[0].message.content)
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 legal document analyst. Analyze the provided document excerpts and answer the user's question. Always cite specific passages to support your analysis."),
		},
		{
			Role:    casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleUser),
			Content: casedev.F("## Document Excerpts\n\n" + chunks + "\n\n## Sources\n" + sources + "\n\n## Question\nWhat are the key terms of this agreement?"),
		},
	}),
	Temperature: casedev.F(float64(0.3)),
})
fmt.Println(resp.Choices[0].Message.Content)
Response
{
  "choices": [{
    "message": {
      "content": "Based on the contract excerpts, the key terms include:\n\n1. **Payment Terms**: Section 3.2 states that payment is due within 30 days...\n\n2. **Termination**: Either party may terminate with 90 days written notice (Section 7.1)...\n\n3. **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:
# 1. Search vault for relevant documents
casedev vault search --id $VAULT_ID \
  --query "What are the indemnification clauses?" \
  --method hybrid --limit 10

# 2. Analyze with LLM
casedev llm:v1:chat create-completion \
  --model anthropic/claude-sonnet-4.5 \
  --message '{role: system, content: "You are a senior legal analyst. Answer using only the provided excerpts."}' \
  --message '{role: user, content: "<document excerpts from search>\n\nQuestion: What are the indemnification clauses?"}' \
  --temperature 0.3
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);
import os
import casedev

client = casedev.Casedev(api_key=os.environ['CASEDEV_API_KEY'])

def analyze_documents(vault_id: str, query: str) -> dict:
    # 1. Search
    search_results = client.vault.search(vault_id,
        query=query,
        method='hybrid',
        top_k=10
    )

    chunks = '\n\n'.join(c.text for c in search_results.chunks)
    sources = ', '.join(s.filename for s in search_results.sources)

    # 2. Analyze
    analysis = client.llm.v1.chat.create_completion(
        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': f'## 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': search_results.sources,
        'usage': analysis.usage
    }

# Run it
result = analyze_documents('vault_abc123', 'What are the indemnification clauses?')
print(result['answer'])
// 1. Search vault
searchResults, _ := client.Vault.Search(ctx, vaultID, casedev.VaultSearchParams{
	Query:  casedev.F(query),
	Method: casedev.F(casedev.VaultSearchParamsMethodHybrid),
	TopK:   casedev.F(int64(10)),
})

// Build context from chunks
var chunks string
for _, c := range searchResults.Chunks {
	chunks += c.Text + "\n\n"
}
var sources string
for _, s := range searchResults.Sources {
	sources += s.Filename + ", "
}

// 2. Analyze with LLM
analysis, _ := 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 legal analyst. Answer questions using only the provided document excerpts. Cite sources."),
		},
		{
			Role:    casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleUser),
			Content: casedev.F("## Document Excerpts\n\n" + chunks + "\n\n## Question\n" + query),
		},
	}),
	Temperature: casedev.F(float64(0.3)),
})
fmt.Println(analysis.Choices[0].Message.Content)

Extending the Analyzer

Add Entity Extraction

Run a second LLM pass to extract structured entities:
casedev llm:v1:chat create-completion \
  --model openai/gpt-4o \
  --message '{role: system, content: "Extract named entities as JSON: {people: [], organizations: [], dates: [], locations: [], monetary_amounts: []}"}' \
  --message '{role: user, content: "<document text>"}' \
  --temperature 0
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
});
entities = client.llm.v1.chat.create_completion(
    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
)
entities, _ := client.Llm.V1.Chat.NewCompletion(ctx, casedev.LlmV1ChatNewCompletionParams{
	Model: casedev.F("openai/gpt-4o"),
	Messages: casedev.F([]casedev.LlmV1ChatNewCompletionParamsMessage{
		{
			Role:    casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleSystem),
			Content: casedev.F("Extract named entities as JSON: {people: [], organizations: [], dates: [], locations: [], monetary_amounts: []}"),
		},
		{
			Role:    casedev.F(casedev.LlmV1ChatNewCompletionParamsMessagesRoleUser),
			Content: casedev.F(chunks),
		},
	}),
	Temperature: casedev.F(float64(0)),
})
fmt.Println(entities.Choices[0].Message.Content)

Generate a PDF Report

Convert the analysis into a formatted document:
casedev format:v1 document \
  --content "# Report" \
  --input-format md --output-format pdf
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'
});
report = client.format.v1.create_document(
    content=f'# Legal Analysis Report\n\n**Query:** {query}\n\n{analysis.choices[0].message.content}',
    input_format='md',
    output_format='pdf'
)
result, _ := client.Format.V1.NewDocument(ctx, casedev.FormatV1NewDocumentParams{
	Content:      casedev.F("# Report\n\nContent here..."),
	InputFormat:  casedev.F("md"),
	OutputFormat: casedev.F("pdf"),
})
// result 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 "analysis query"
# Error: 404 Not Found — check your vault ID
# Error: 429 Too Many Requests — retry after a delay
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);
  }
}
try:
    result = analyze_documents(vault_id, query)
    print(result['answer'])
except casedev.NotFoundError:
    print('Vault not found — check your vault ID')
except casedev.RateLimitError:
    print('Rate limited — retry after a delay')
except casedev.APIError as e:
    print(f'Analysis failed: {e.message}')
result, err := analyzeDocuments(ctx, vaultID, query)
if err != nil {
	var apiErr *casedev.Error
	if errors.As(err, &apiErr) {
		switch apiErr.StatusCode {
		case 404:
			fmt.Println("Vault not found — check your vault ID")
		case 429:
			fmt.Println("Rate limited — retry after a delay")
		default:
			fmt.Printf("Analysis failed: %s\n", apiErr.Message)
		}
	}
}
Use temperature: 0 for factual extraction tasks. Try cheaper models like deepseek/deepseek-chat for simpler analysis.

Next Steps