FULL END TO END FLOW — ERP Agent with RAG + Excel Export
THE FULL END TO END FLOW — ERP Agent with RAG + Excel Export¶
The System Setup — Configured Once, Sent Every Call¶
System Prompt:
You are a business intelligence assistant for an ERP system.
## Behaviour
- When a user asks a vague question, do not ask for clarification.
Make reasonable assumptions, state them explicitly, then answer.
- Always fetch data using tools before answering. Never guess numbers.
- When answering always provide:
- Direct answer to the question
- Supporting breakdown
- Assumptions you made
- A follow-up suggestion where relevant
## Export Rules
- User asks for Excel / spreadsheet / .xlsx →
call export_excel. Pass all data as a clean CSV string.
Include a header row. Pure CSV only — no markdown, no formatting.
- User asks for PDF / printable report →
call export_pdf. Pass data as clean HTML with <table> tags.
Inline styles only. Include an <h1> title.
- "download this", "save this", "send me a file" with no format stated →
ask which format before calling any export tool.
- Never embed raw file data in your text response.
Always use the export tools. Tell the user the file is ready with the URL.
## RAG Tool Usage
- When the user asks about historical interactions, complaints, past issues,
support history, or anything that is not structured data →
call search_customer_history before answering.
- Never guess historical context. Always retrieve it.
## Today's date: 2026-05-09
Tool Definitions — sent with every API call:
[
{
"name": "get_inactive_customers",
"description": "Gets customers with no ERP activity in the past N months. Returns customer ID, name, segment, last activity date, account manager.",
"input_schema": {
"type": "object",
"properties": {
"months": {
"type": "integer",
"description": "How many months back to check for inactivity"
}
},
"required": ["months"]
}
},
{
"name": "get_customer_activity_summary",
"description": "Gets activity summary for a batch of customers. Returns order count, last order value, lifetime value, payment status. Always call this after get_inactive_customers to enrich the data.",
"input_schema": {
"type": "object",
"properties": {
"customer_ids": {
"type": "array",
"items": { "type": "integer" },
"description": "List of customer IDs — pass all at once, not one at a time"
}
},
"required": ["customer_ids"]
}
},
{
"name": "get_customer_order_frequency",
"description": "Gets order frequency tier for a batch of customers over a given period. Returns: frequent / infrequent / never-frequent per customer.",
"input_schema": {
"type": "object",
"properties": {
"customer_ids": {
"type": "array",
"items": { "type": "integer" }
},
"months": {
"type": "integer",
"description": "Period in months to analyse frequency over"
}
},
"required": ["customer_ids", "months"]
}
},
{
"name": "search_customer_history",
"description": "Semantic search across historical support tickets, customer emails, and communication logs. Use when the user asks about past issues, complaints, historical interactions, or anything not in structured tables.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Natural language search query describing what to look for"
},
"customer_name": {
"type": "string",
"description": "Optional — scope the search to a specific customer"
}
},
"required": ["query"]
}
},
{
"name": "export_excel",
"description": "Generates an Excel file from CSV data. Call when user requests Excel, spreadsheet, or .xlsx. Returns a download URL.",
"input_schema": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "Filename without extension"
},
"csv": {
"type": "string",
"description": "Full CSV string including header row. Clean data only — no markdown, no extra formatting."
}
},
"required": ["filename", "csv"]
}
}
]
TURN 1 — "show me all customers who have not had any activity in past x months"¶
→ API Call 1: Your App → Anthropic¶
{
"model": "claude-sonnet-4-6",
"max_tokens": 4096,
"system": "...system prompt above...",
"tools": [...all five tool definitions...],
"messages": [
{
"role": "user",
"content": "show me all customers who have not had any activity in past x months"
}
]
}
One message. Full tool list. System prompt. Nothing else.
← API Response 1: Anthropic → Your App¶
{
"id": "msg_001",
"role": "assistant",
"stop_reason": "tool_use",
"content": [
{
"type": "tool_use",
"id": "call_001",
"name": "get_inactive_customers",
"input": {
"months": 6
}
}
]
}
What happened:
- User said "x months" — no number
- Claude assumed 6. Reasonable industry default
- No text block. Claude went straight to the tool call
stop_reason: tool_use— Claude has stopped. It is waiting. It called nothing- Your loop reads
call_001, readsget_inactive_customers, readsmonths: 6
Your app executes:
var result = await GetInactiveCustomers(months: 6);
// runs: SELECT customer_id, name, segment, last_activity_date, account_manager
// FROM customers
// WHERE last_activity_date < NOW() - INTERVAL '6 months'
DB returns:
[
{ "customer_id": 101, "name": "Acme Corp", "segment": "Enterprise", "last_activity": "2025-08-12", "account_manager": "Sarah Jones" },
{ "customer_id": 102, "name": "BuildCo Ltd", "segment": "Enterprise", "last_activity": "2025-07-03", "account_manager": "Mike Peters" },
{ "customer_id": 103, "name": "TechStart Inc", "segment": "Mid-Market", "last_activity": "2025-09-01", "account_manager": "Sarah Jones" },
{ "customer_id": 104, "name": "RetailPlus", "segment": "Mid-Market", "last_activity": "2025-08-22", "account_manager": "Dan Walsh" },
{ "customer_id": 105, "name": "QuickTrade", "segment": "Small Business", "last_activity": "2025-06-15", "account_manager": "Mike Peters" }
]
50 customers total. Truncated here for readability.
→ API Call 2: Your App → Anthropic¶
You append the assistant message and tool result to history. Full payload:
{
"model": "claude-sonnet-4-6",
"max_tokens": 4096,
"system": "...system prompt...",
"tools": [...all tools...],
"messages": [
{
"role": "user",
"content": "show me all customers who have not had any activity in past x months"
},
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "call_001",
"name": "get_inactive_customers",
"input": { "months": 6 }
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "call_001",
"content": "[{\"customer_id\":101,\"name\":\"Acme Corp\",\"segment\":\"Enterprise\",\"last_activity\":\"2025-08-12\",\"account_manager\":\"Sarah Jones\"},{\"customer_id\":102,...}]"
}
]
}
]
}
Critical structure notes:
- The assistant message echoes back the exact
tool_useblock Claude returned — verbatim - The tool result is a
userrole message — not assistant tool_use_id: "call_001"matches — this is how Claude pairs the result to the call- The content is your serialised DB result as a string
← API Response 2: Anthropic → Your App¶
Claude has 50 customers. Only IDs, names, segments, dates. It wants richer data.
{
"id": "msg_002",
"role": "assistant",
"stop_reason": "tool_use",
"content": [
{
"type": "tool_use",
"id": "call_002",
"name": "get_customer_activity_summary",
"input": {
"customer_ids": [101, 102, 103, 104, 105, 106, 107, 108, 109, 110,
111, 112, 113, 114, 115, 116, 117, 118, 119, 120,
121, 122, 123, 124, 125, 126, 127, 128, 129, 130,
131, 132, 133, 134, 135, 136, 137, 138, 139, 140,
141, 142, 143, 144, 145, 146, 147, 148, 149, 150]
}
}
]
}
What happened:
- Claude batched all 50 IDs into one call — not 50 individual calls
- This is because you defined the tool with an array parameter
- Tool description said "pass all at once, not one at a time" — Claude followed it
Your app executes:
var result = await GetCustomerActivitySummary(customerIds: [...50 ids...]);
// runs: SELECT c.customer_id, c.name,
// COUNT(o.id) as total_orders,
// MAX(o.value) as last_order_value,
// SUM(o.value) as lifetime_value,
// ps.status as payment_status
// FROM customers c
// LEFT JOIN orders o ON o.customer_id = c.id
// LEFT JOIN payment_status ps ON ps.customer_id = c.id
// WHERE c.customer_id = ANY(@ids)
// GROUP BY c.customer_id, ps.status
DB returns:
[
{ "customer_id": 101, "name": "Acme Corp", "total_orders": 45, "last_order_value": 1200.00, "lifetime_value": 87400.00, "payment_status": "Good" },
{ "customer_id": 102, "name": "BuildCo Ltd", "total_orders": 38, "last_order_value": 3400.00, "lifetime_value": 62000.00, "payment_status": "Good" },
{ "customer_id": 103, "name": "TechStart", "total_orders": 12, "last_order_value": 450.00, "lifetime_value": 8200.00, "payment_status": "Late" },
{ "customer_id": 104, "name": "RetailPlus", "total_orders": 6, "last_order_value": 220.00, "lifetime_value": 3100.00, "payment_status": "Good" },
{ "customer_id": 105, "name": "QuickTrade", "total_orders": 4, "last_order_value": 180.00, "lifetime_value": 1800.00, "payment_status": "Good" }
]
→ API Call 3: Your App → Anthropic¶
{
"messages": [
{ "role": "user", "content": "show me all customers..." },
{ "role": "assistant", "content": [{ "type": "tool_use", "id": "call_001", "name": "get_inactive_customers", "input": {"months":6} }] },
{ "role": "user", "content": [{ "type": "tool_result", "tool_use_id": "call_001", "content": "[...50 customers...]" }] },
{ "role": "assistant", "content": [{ "type": "tool_use", "id": "call_002", "name": "get_customer_activity_summary", "input": {"customer_ids":[101,102,...50]} }] },
{ "role": "user", "content": [{ "type": "tool_result", "tool_use_id": "call_002", "content": "[...50 summaries...]" }] }
]
}
History is growing. Every prior exchange replayed in full. Claude sees: original question, both tool calls, both results.
← API Response 3: Anthropic → Your App — end_turn¶
{
"id": "msg_003",
"role": "assistant",
"stop_reason": "end_turn",
"content": [
{
"type": "text",
"text": "## Inactive Customers — Last 6 Months\n\n**Assumption:** Interpreted \"past x months\" as 6 months since no timeframe was specified.\n\n50 customers have had no recorded activity since November 2025.\n\n### By Segment\n| Segment | Count | % of Inactive |\n|---------|-------|---------------|\n| Enterprise | 12 | 24% |\n| Mid-Market | 18 | 36% |\n| Small Business | 20 | 40% |\n\n### Notable High-Value Accounts\n| Customer | Segment | Last Activity | Lifetime Value | Account Manager |\n|----------|---------|---------------|----------------|-----------------|\n| Acme Corp | Enterprise | Aug 2025 | £87,400 | Sarah Jones |\n| BuildCo Ltd | Enterprise | Jul 2025 | £62,000 | Mike Peters |\n| TechStart Inc | Mid-Market | Sep 2025 | £8,200 | Sarah Jones |\n\n**Suggested next step:** Would you like me to identify which of these were previously frequent buyers to help prioritise re-engagement outreach?"
}
]
}
What happened:
stop_reason: end_turn— done for this turn- Single
textblock — markdown formatted - Claude stated its assumption about "x months"
- Suggested a follow-up unprompted because system prompt told it to
- Your app appends to session history, saves to Redis/DB, returns text to frontend
- Frontend renders markdown — tables appear properly
TURN 2 — "and how many of these users have not been frequent to the platform"¶
→ API Call 4: Your App → Anthropic¶
Append new user message to history. Send everything:
{
"messages": [
{ "role": "user", "content": "show me all customers who have not had any activity in past x months" },
{ "role": "assistant", "content": [{ "type": "tool_use", "id": "call_001", ... }] },
{ "role": "user", "content": [{ "type": "tool_result", "tool_use_id": "call_001", "content": "[...50 customers...]" }] },
{ "role": "assistant", "content": [{ "type": "tool_use", "id": "call_002", ... }] },
{ "role": "user", "content": [{ "type": "tool_result", "tool_use_id": "call_002", "content": "[...50 summaries...]" }] },
{ "role": "assistant", "content": [{ "type": "text", "text": "## Inactive Customers — Last 6 Months..." }] },
{
"role": "user",
"content": "and how many of these users have not been frequent to the platform"
}
]
}
Critical: Claude has the 50 customer IDs in context. It will not re-call get_inactive_customers. It already knows who they are.
← API Response 4: Anthropic → Your App¶
{
"id": "msg_004",
"role": "assistant",
"stop_reason": "tool_use",
"content": [
{
"type": "tool_use",
"id": "call_003",
"name": "get_customer_order_frequency",
"input": {
"customer_ids": [101, 102, 103, 104, 105, ...all 50],
"months": 12
}
}
]
}
What happened:
- Claude pulled IDs from earlier context — zero re-fetching
- "frequent" is undefined — Claude will define it, state the definition
- Picked 12 months as a reasonable measurement period — will state this
- Batched all 50
Your app executes and DB returns:
[
{ "customer_id": 101, "name": "Acme Corp", "orders_per_month": 3.8, "frequency_tier": "frequent" },
{ "customer_id": 102, "name": "BuildCo Ltd", "orders_per_month": 2.9, "frequency_tier": "frequent" },
{ "customer_id": 103, "name": "TechStart", "orders_per_month": 0.4, "frequency_tier": "infrequent" },
{ "customer_id": 104, "name": "RetailPlus", "orders_per_month": 0.1, "frequency_tier": "never-frequent" },
{ "customer_id": 105, "name": "QuickTrade", "orders_per_month": 0.2, "frequency_tier": "never-frequent" }
]
→ API Call 5 → ← API Response 5: end_turn¶
Full history sent. Claude responds:
{
"id": "msg_005",
"stop_reason": "end_turn",
"content": [
{
"type": "text",
"text": "## Frequency Analysis of 50 Inactive Customers\n\n**Assumptions:**\n- Defined \"frequent\" as 2+ orders per month averaged over the past 12 months\n- Period: May 2025 – May 2026\n\n### Breakdown\n| Frequency Tier | Count | % of Inactive |\n|----------------|-------|---------------|\n| Previously frequent (now gone quiet) | 16 | 32% |\n| Infrequent (1–2 orders/month) | 22 | 44% |\n| Never frequent (<1 order/month) | 12 | 24% |\n\n**34 of 50 were already infrequent or never frequent.** Different strategy needed for these versus the 16 who dropped off.\n\n### The 16 Previously Frequent — Highest Re-engagement Priority\n| Customer | Avg Orders/Month | Last Activity | Lifetime Value | Account Manager |\n|----------|-----------------|---------------|----------------|-----------------|\n| Acme Corp | 3.8 | Aug 2025 | £87,400 | Sarah Jones |\n| BuildCo Ltd | 2.9 | Jul 2025 | £62,000 | Mike Peters |\n| ... | | | | |\n\nWould you like me to export this full list to Excel?"
}
]
}
Claude suggested the export. It saw the export tool, the data, and made the connection.
TURN 3 — "yes export to excel"¶
→ API Call 6: Your App → Anthropic¶
{
"messages": [
...all 5 turns of history...,
{
"role": "user",
"content": "yes export to excel"
}
]
}
Claude has in context:
- 50 customer names, segments, last activity dates, account managers — from call_001 result
- Lifetime values, order counts, payment status — from call_002 result
- Frequency tiers, orders per month — from call_003 result
It has everything. No data tools needed.
← API Response 6: Anthropic → Your App¶
{
"id": "msg_006",
"role": "assistant",
"stop_reason": "tool_use",
"content": [
{
"type": "tool_use",
"id": "call_004",
"name": "export_excel",
"input": {
"filename": "inactive-customers-may-2026",
"csv": "Customer,Segment,Last Activity,Lifetime Value (GBP),Avg Orders/Month,Frequency Tier,Account Manager\nAcme Corp,Enterprise,2025-08-12,87400,3.8,Previously Frequent,Sarah Jones\nBuildCo Ltd,Enterprise,2025-07-03,62000,2.9,Previously Frequent,Mike Peters\nTechStart Inc,Mid-Market,2025-09-01,8200,0.4,Infrequent,Sarah Jones\nRetailPlus,Mid-Market,2025-08-22,3100,0.1,Never Frequent,Dan Walsh\nQuickTrade,Small Business,2025-06-15,1800,0.2,Never Frequent,Mike Peters\n..."
}
}
]
}
What happened — this is the key moment:
- Claude constructed the entire CSV from data sitting in message history
- It joined three separate tool results together in memory — inactive list + summary + frequency
- It picked sensible column headers and ordering
- It sorted by lifetime value descending without being asked — applied judgement
stop_reason: tool_use— waiting for you to generate the file- Claude never touched your filesystem. It just produced a CSV string.
Your app executes export_excel:
public async Task<string> ExportExcel(string filename, string csv)
{
using var workbook = new XLWorkbook();
var sheet = workbook.AddWorksheet("Inactive Customers");
var rows = csv.Split('\n');
for (int r = 0; r < rows.Length; r++)
{
var cols = rows[r].Split(',');
for (int c = 0; c < cols.Length; c++)
sheet.Cell(r + 1, c + 1).Value = cols[c].Trim('"');
}
// Style header
var header = sheet.Row(1);
header.Style.Font.Bold = true;
header.Style.Fill.BackgroundColor = XLColor.FromHtml("#1e1e2e");
header.Style.Font.FontColor = XLColor.White;
sheet.Columns().AdjustToContents();
using var ms = new MemoryStream();
workbook.SaveAs(ms);
// Save to blob storage, get back a URL
var url = await _fileStore.SaveAsync($"{filename}.xlsx", ms.ToArray());
return JsonSerializer.Serialize(new {
url = url,
filename = $"{filename}.xlsx"
});
}
Tool returns:
{ "url": "/files/inactive-customers-may-2026.xlsx", "filename": "inactive-customers-may-2026.xlsx" }
→ API Call 7: Your App → Anthropic¶
{
"messages": [
...all previous history...,
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": "call_004",
"name": "export_excel",
"input": {
"filename": "inactive-customers-may-2026",
"csv": "Customer,Segment,Last Activity,..."
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "call_004",
"content": "{\"url\":\"/files/inactive-customers-may-2026.xlsx\",\"filename\":\"inactive-customers-may-2026.xlsx\"}"
}
]
}
]
}
← API Response 7: Anthropic → Your App — Final¶
{
"id": "msg_007",
"role": "assistant",
"stop_reason": "end_turn",
"content": [
{
"type": "text",
"text": "Your Excel file is ready.\n\n**[Download inactive-customers-may-2026.xlsx](/files/inactive-customers-may-2026.xlsx)**\n\nThe file includes all 50 inactive customers with segment, last activity date, lifetime value, order frequency tier, and account manager — sorted by lifetime value descending so your highest-value at-risk accounts are at the top."
}
]
}
Your app saves session. Returns text to frontend. Frontend renders markdown — download link is a real clickable anchor.
TURN 4 — "has Acme Corp ever complained about delivery issues"¶
This is where RAG enters. This question cannot be answered from structured tables.
→ API Call 8: Your App → Anthropic¶
{
"messages": [
...all 7 turns of history...,
{
"role": "user",
"content": "has Acme Corp ever complained about delivery issues"
}
]
}
← API Response 8: Anthropic → Your App¶
{
"id": "msg_008",
"role": "assistant",
"stop_reason": "tool_use",
"content": [
{
"type": "tool_use",
"id": "call_005",
"name": "search_customer_history",
"input": {
"query": "Acme Corp delivery complaints late shipment issues",
"customer_name": "Acme Corp"
}
}
]
}
What happened:
- Claude recognised this is a historical question — not answerable from structured data
- It reached for
search_customer_history— the RAG tool - It formed a natural language query from the user's question
- It scoped the search to Acme Corp
- Your loop handles this identically to any other tool call
Your app executes search_customer_history:
public async Task<string> SearchCustomerHistory(string query, string? customerName = null)
{
// 1. Embed the query using your embedding model
var queryVector = await _embedder.EmbedAsync(query);
// queryVector is now float[1536] — a mathematical representation of meaning
// 2. Vector search in pgvector
// SQL: SELECT text, source, created_at,
// 1 - (embedding <=> $1) AS similarity
// FROM document_chunks
// WHERE ($2 IS NULL OR customer_name = $2)
// ORDER BY embedding <=> $1
// LIMIT 5
var results = await _vectorDb.SearchAsync(
vector: queryVector,
customerName: customerName,
topK: 5);
// 3. Format results as readable text for Claude
return string.Join("\n\n---\n\n", results.Select(r =>
$"[{r.Source} — {r.Date:yyyy-MM-dd}]\n{r.Text}"));
}
pgvector finds and returns the 5 most semantically similar chunks:
[Support Ticket #4421 — 2024-03-12]
Acme Corp reported that order #8821 arrived 4 days past the committed
delivery date. Customer contact Sarah Mitchell expressed frustration and
asked for a credit note. Resolved with 5% discount on next order.
---
[Support Ticket #3892 — 2023-11-08]
Follow-up call from Acme Corp regarding delayed shipment on order #7103.
Original ETA was 2023-10-28. Actual delivery 2023-11-06. No escalation,
customer accepted apology.
---
[Email Thread — 2024-01-15]
Sarah from Acme Corp wrote asking about status of order #8104 which was
showing as dispatched but not received after 8 days. Tracked and confirmed
carrier delay. Customer satisfied with response.
---
[Support Ticket #2201 — 2023-06-22]
Acme Corp raised issue about damaged packaging on delivery of order #6540.
Not strictly a late delivery but flagged under logistics complaints.
Replacement sent same day.
---
[Internal Note — 2024-02-03]
Account manager Sarah Jones flagged Acme Corp as sensitive on delivery
commitments following repeated incidents in 2023-2024. Recommend
prioritising their orders in dispatch queue.
What just happened under the hood in pgvector:
- Your query "Acme Corp delivery complaints late shipment issues" was converted to a vector
- pgvector compared that vector against every stored chunk using cosine distance
- The 5 chunks whose vectors were geometrically closest came back
- None of them necessarily contained the exact words "delivery complaints" — they matched on meaning
→ API Call 9: Your App → Anthropic¶
{
"messages": [
...all previous history...,
{
"role": "assistant",
"content": [{ "type": "tool_use", "id": "call_005", "name": "search_customer_history", "input": {...} }]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "call_005",
"content": "[Support Ticket #4421 — 2024-03-12]\nAcme Corp reported that order #8821 arrived 4 days past...\n\n---\n\n[Support Ticket #3892 — 2023-11-08]\n..."
}
]
}
]
}
← API Response 9: Anthropic → Your App — end_turn¶
{
"id": "msg_009",
"role": "assistant",
"stop_reason": "end_turn",
"content": [
{
"type": "text",
"text": "## Acme Corp — Delivery Complaint History\n\nYes — Acme Corp has raised delivery-related issues on **at least 4 occasions** between June 2023 and March 2024.\n\n| Date | Ticket | Issue | Resolution |\n|------|--------|-------|------------|\n| Mar 2024 | #4421 | Order #8821 arrived 4 days late | 5% discount applied |\n| Jan 2024 | Email | Order #8104 not received after 8 days | Carrier delay confirmed |\n| Nov 2023 | #3892 | Order #7103 delivered 9 days late | Apology accepted |\n| Jun 2023 | #2201 | Damaged packaging on order #6540 | Replacement sent |\n\n**Account manager note (Feb 2024):** Sarah Jones flagged Acme Corp as sensitive on delivery commitments and recommended prioritising their orders in the dispatch queue.\n\nGiven that Acme Corp is also on your inactive list with £87,400 lifetime value, delivery reliability is likely a contributing factor to their disengagement. This is worth addressing directly in any re-engagement outreach."
}
]
}
What happened:
- Claude synthesised 5 raw ticket chunks into a structured, readable answer
- It cross-referenced with earlier context — knew Acme Corp's lifetime value from Turn 1
- It connected the delivery history to the inactivity without being asked
- That connection is the intelligence — you never hardcoded it
The Complete Message History After All 4 Turns¶
[01] user → "show me all customers who have not had any activity..."
[02] assistant → tool_use: get_inactive_customers(months: 6)
[03] user → tool_result: call_001 → [50 customers]
[04] assistant → tool_use: get_customer_activity_summary(customer_ids: [...50])
[05] user → tool_result: call_002 → [50 summaries with lifetime values]
[06] assistant → text: "## Inactive Customers — Last 6 Months..."
[07] user → "and how many of these users have not been frequent..."
[08] assistant → tool_use: get_customer_order_frequency(customer_ids: [...50], months: 12)
[09] user → tool_result: call_003 → [frequency tiers for 50]
[10] assistant → text: "## Frequency Analysis of 50 Inactive Customers..."
[11] user → "yes export to excel"
[12] assistant → tool_use: export_excel(filename, csv)
[13] user → tool_result: call_004 → { url, filename }
[14] assistant → text: "Your Excel file is ready. Download here..."
[15] user → "has Acme Corp ever complained about delivery issues"
[16] assistant → tool_use: search_customer_history(query, customer_name)
[17] user → tool_result: call_005 → [5 relevant ticket chunks]
[18] assistant → text: "## Acme Corp — Delivery Complaint History..."
18 messages. 9 API calls. 4 user turns. 5 autonomous tool decisions by Claude.
Every Decision — Who Made It and Why¶
| Decision | Who | Why |
|---|---|---|
| Assume 6 months for "x" | Claude | System prompt: assume and state |
| Fetch activity summary after getting IDs | Claude | Needed richer data for useful report |
| Batch all 50 IDs in one call | Claude | Tool schema had array param + description said so |
| Define "frequent" as 2+ orders/month | Claude | Undefined by user, applied judgement |
| Use 12 months for frequency period | Claude | Reasonable default, stated it |
| Suggest export at end of Turn 2 | Claude | Saw export tool, connected it to context |
| Construct full CSV from context history | Claude | All data was already in message history — no re-fetching |
| Sort CSV by lifetime value descending | Claude | Applied business judgement unprompted |
| Use RAG tool for delivery complaint question | Claude | System prompt: unstructured history = search tool |
| Form the semantic search query | Claude | Translated user's question into search terms |
| Cross-reference delivery history with inactivity | Claude | Both were in context, made the connection |
| You ran all SQL queries | Your app | Claude never touches your DB |
| You ran the vector search | Your app | Claude passed a query string, you did the search |
| You generated the xlsx | Your app | Claude passed a CSV string, you built the file |
| You stored the file | Your app | Claude received the URL back |
| You saved session history | Your app | Claude has no memory between calls |