Every lead read in seconds.
Every action approved by a person.
An AI agent reviews each used and damaged car lead against the company's rules, written in plain English and stored as data. It recommends the next step with its reasoning, and an employee approves, edits or rejects every action.
Three problems, and what the design does about each
Today every lead is read and processed by hand in a legacy CRM, against rules only engineering can change.
Manual review
1,000+ leads a day, each read by a person before anything happens.
Rigid rules
Business rules sit in code or in people's heads; a change means a ticket and a release.
Scale
2,000 leads on a peak day. At that volume the people approving become the bottleneck.
The LLM reasons. Deterministic code enforces.
The agent sits in the main path of every lead, not in a side panel. Four decisions make it safe to put it there.
Rules as data
Immutable versions with a live pointer. Every recommendation records the exact rule versions it used.
Stage-aware context
Only the live rules for the lead's stage, in priority order, plus facts from deterministic tools:
- Pricing quote and its inputs
- VIN decode vs. the seller's year, make and model
- Possible duplicates in the window
- Latest messages, redacted, with no contact details
Structured, explainable output
A JSON schema, validated with zod. Rule IDs must exist.
{
"action": "counter_offer",
"params": { "amount": 4075 },
"reasoning": "Outside 3%…",
"rule_version_ids": ["R-06 v2"],
"confidence": 0.86,
"checked": [ … ]
}
Human approval
The agent can only write a pending recommendation. It cannot act.
- Approve, edit or reject, with a reason code
- The diff between recommended and approved is stored
- The action runs under the approver's identity
- Employees can propose their own action, through the same guardrails
Where AI enables, empowers, decides, or acts
Each action type has a level, stored as configuration, and shown in the app with the evidence behind it.
Enables
The agent surfaces what it found and steps back. Anything unsure goes to a person.
Empowers
Money moves only with a person. Employees approve up to $50k; above that, a manager.
Decides
Low-risk process steps. One click, or bulk approval above 0.9 confidence.
Acts
Not assignable today. The settings schema rejects it. An action type becomes a candidate only with evidence:
One API, one worker, one database
One seller reply, through every module
One damaged 2016 Civic, from intake to a rule change
A damaged car arrives
No VIN. The agent cites R‑04 and asks for it. Maria approves; the text reaches the seller.
First offer from the formula
VIN decoded and checked. Quote $3,750 with its breakdown. Maria edits to $4,000 with a reason.
The seller pushes back
$4,150 is within 5%: the agent says accept. Maria rejects and counters $4,075 herself.
Deal, documents, audit
Accepted. The title shows another name: proof of ownership. Full history. $50k route on a Porsche.
A rule changed live
Priya sets R‑06 from 5% to 3%, tests on sample leads, publishes. Same counter: the agent now does what Maria did.
A TypeScript monorepo with hard edges between modules
Structure
Data model
Everything is an event. The audit trail, metrics and experiment results are queries over the same tables.
- Append-only tables enforced by database triggers
- Lead status is derived from pending items, never stored
- Each run stores prompt version, model, context hash, rule versions
Tests that mean something
- "Never runs without an approval"
- "Blocks when the approver lost the authority"
- "Turns invalid output into a could-not-assess escalation"
No prompt, rule edit or experiment can weaken the hard limits
Guardrails in code
Locked rules: admin-only, every stage, never overridden by a variant.
Bad or uncertain output
Audit, privacy, cost
- Audit trail: every event, agent run, LLM call and decision, by whom and when; never updated or deleted
- Privacy: contact details never reach the model; messages are redacted first
- Cost: tokens, cost and latency per call; $0.50 budget per lead with a warning; rate limit with backoff
- Free CI: a deterministic mock LLM makes tests and the demo repeatable
Off the legacy CRM in stages, with evidence at each step
The agent reviews, nobody acts
Leads copied from the legacy CRM. Measure agreement with what employees actually did.
One team, one stage
For example New lead. The legacy CRM stays the system of record, synced both ways.
Move stage by stage
Until the legacy CRM is read-only, then retire it. No big-bang switch.
Scaling path
Demo · Docker Compose on one Droplet, behind Cloudflare. Pulumi.
Early production · managed Postgres, Valkey queue, workers scaled separately, SSO.
Larger · Kubernetes, workers autoscaled on queue depth, a warehouse.
The limit is LLM calls and the human queue, not compute. Stateless services and env config keep each move to infrastructure code.
Trade-offs I chose
| Choice | Cost | Next |
|---|---|---|
| Outbox, not a queue | More database load at volume | Valkey behind the same interface |
| Mock LLM by default | Proves the flow, not reasoning quality | Measure real models; model A/B first |
| Human approves all | Inbox is the bottleneck | Bulk approve, then earned autonomy |
| Dashboards in the app | A new chart needs an engineer | BI tool on the same SQL views |