Lead qualification
Inbound leads arrive from forms and imports at any hour. Millstream normalises each one, drops duplicates, enriches the company, has an agent score the fit, and puts the promising leads in a sales inbox before anything reaches the CRM.
The pipeline
flowchart LR
W(["webhook: form"]) --> N[normalise] --> D{{dedupe}} --> E[enrich company]
E --> Q["qualify (agent)"]
Q -->|fit at least 60| R[/sales review/] --> C[CRM upsert]
Q -->|other| U[nurture list]
pipeline: lead-qualification
description: Qualify inbound leads before they reach the CRM
vars: { minFit: 60 }
triggers:
inbound: { webhook: { rate: 60/min } }
streams:
leads.normalized: { retention: 180d, redact: [/email, /phone] }
steps:
normalize:
action: ./actions/normalize-lead
in: trigger.inbound
out: leads.normalized
dedupe:
action: dedupe
in: leads.normalized
out: leads.unique
config: { key: "item.emailNorm", window: 90d }
enrich:
action: lab/enrich-company@1
in: leads.unique
out: leads.enriched
rate: 30/min
qualify:
action: lab/qualify-lead@1
in: leads.enriched
concurrency: 4
out:
- { when: "item.fit >= vars.minFit", out: leads.promising }
- { when: "true", out: leads.nurture }
sales-review:
action: review
in: leads.promising
out: leads.approved
config:
inbox: sales
timeout: 4h
onTimeout: { escalate: sales-leads }
editable: [/tier, /owner]
reasons: [notOurMarket, competitor, student, existingCustomer]
crm-upsert:
action: lab/crm-upsert@1
in: leads.approved
nurture:
action: lab/nurture-export@1
in: leads.nurture
budget: { dayUsd: 20 }
The qualifying agent's manifest sets its model and limits:
name: lab/qualify-lead
version: 1.0.0
kind: agent
effects: none
runtime:
agent:
harness: api
model: claude-haiku-5-5
network: [api.anthropic.com]
maxTurns: 2
maxCostUsd: 0.05
What Millstream does for it
Intake
- Each form post is
POST /hooks/lead-qualification/inboundwith the trigger's token, rate limited to 60 a minute. - A sender that retries with the same
Idempotency-Keygets back the run it already opened. - The same address submitted twice within 90 days is filtered by
dedupewith reasonduplicate.
The agent
qualifyanswers a schema withfit,tierand its reasons; an answer that does not match is retried.- Routes in
outsend each lead to the first stream whose condition holds, and the threshold stays in the pipeline file. dayUsd: 20caps the agent spend per day, and the per-deliverymaxCostUsdcaps a single lead.
Sales
- Promising leads wait in the
salesinbox; one undecided after four hours escalates tosales-leads. - A rejection needs a reason code, so
notOurMarketandstudentbecome counts instead of free text. - The CRM action is declared
effects: externaland receives anidempotencyKeyper delivery, stable across retries.
How it learns
The insights projector reads the review decisions. When rejections are predictable from a field known before the agent runs, such as a country or a company size, the improve pipeline drafts a topology proposal: a filter placed before qualify, which saves the agent call. Its backtest must show it would have wrongly dropped at most 1% of past approvals. Recurring edits to /tier attributed to qualify become a lessons proposal for that step. Each proposal is drafted in development, validated against evidence exported from production, and accepted by a person.
Figures to watch
| Figure | What it tells you |
|---|---|
Intervention rate on sales-review |
How many leads still need a salesperson's decision |
| Rejections by reason code | Which kind of lead the agent lets through wrongly |
| Cost per approved item | What qualification costs per lead sales keeps |
| Time to decision, p50 and p95 | Whether the inbox is staffed for the volume |
waste on qualify |
Agent spend on leads rejected for a reason known upstream |