Introduction
This pattern implements a Sales Lead Qualification & Routing Agent. It ingests inbound leads (web forms, chat, events), normalizes and scores them, checks ICP fit and readiness, and routes to the right owner (AE/SDR/CSM/Partner) with a tracked handoff. It never claims success unless the downstream CRM actions return receipts (lead created/updated, task assigned, email sent), enabling full auditability and fast feedback loops.
The Use Case
Go-to-market teams often drown in raw leads with inconsistent fields and poor signal. The agent standardizes company/person data, de-duplicates against CRM, enriches from approved sources, applies scoring and rules (territory, segment, product interest), and then creates/updates CRM objects plus a next-step task. For uncertain or low-signal cases, it asks for one clarification (e.g., company size or use case) before proceeding. All factual statements cite policy claims (routing policies, SLOs, ICP criteria).
Prompt Contract (agent interface)
# file: contracts/lead_router_v1.yaml
role: "LeadRouterAgent"
scope: >
Normalize, enrich, score, dedup, and route inbound leads to the correct owner with a next action.
Ask once if critical fields are missing (email, full_name, company, country, interest).
Propose tool calls; never assert success without a CRM receipt.
output:
type: object
required: [summary, decision, lead, routing, citations, next_steps, tool_proposals]
properties:
summary: {type: string, maxWords: 80}
decision: {type: string, enum: ["route","need_more_info","reject","hold"]}
lead:
type: object
required: [email, full_name, company, country, interest, score]
properties:
email: {type: string}
full_name: {type: string}
company: {type: string}
country: {type: string}
interest: {type: string}
score: {type: integer}
routing:
type: object
required: [owner_type, owner_id, queue, sla_minutes]
properties:
owner_type: {type: string, enum: ["AE","SDR","Partner","CSM"]}
owner_id: {type: string}
queue: {type: string}
sla_minutes: {type: integer}
citations: {type: array, items: {type: string}}
next_steps: {type: array, items: {type: string}, maxItems: 6}
tool_proposals:
type: array
items:
type: object
required: [name, args, preconditions, idempotency_key]
properties:
name: {type: string, enum: [Normalize, Enrich, ScoreLead, CheckDuplicate, RouteLead, CreateTask, SendHandoffEmail]}
args: {type: object}
preconditions: {type: string}
idempotency_key: {type: string}
policy_id: "gtm_routing_policy.v7"
citation_rule: "1–2 minimal-span claim_ids per factual sentence"
decoding:
narrative: {top_p: 0.9, temperature: 0.7}
bullets: {top_p: 0.82, temperature: 0.45}
Example claims (context given to the model)
[
{"claim_id":"policy:icp:employee_min","text":"ICP companies have >= 100 employees.","effective_date":"2025-05-01","source_id":"doc:gtm_routing_policy_v7","span":">= 100 employees"},
{"claim_id":"policy:routing:territory","text":"Leads route by country to regional SDR queues within 15 minutes.","effective_date":"2025-05-01","source_id":"doc:gtm_routing_policy_v7","span":"regional SDR queues within 15 minutes"},
{"claim_id":"policy:routing:dedup","text":"If a matching contact exists, update and assign to the current account owner.","effective_date":"2025-05-01","source_id":"doc:gtm_routing_policy_v7","span":"assign to the current account owner"}
]
Tool Interfaces (typed, with receipts)
# tools.py
from pydantic import BaseModel, EmailStr
from typing import Optional, Dict
from datetime import datetime
class NormalizeArgs(BaseModel):
email: EmailStr
full_name: str
company: str
country: str
interest: str
class EnrichArgs(BaseModel):
email: EmailStr
company: str
class ScoreLeadArgs(BaseModel):
signals: Dict[str, int] # e.g., {"employees": 200, "site_visits": 5, "use_case_match": 1}
class CheckDuplicateArgs(BaseModel):
email: EmailStr
class RouteLeadArgs(BaseModel):
email: EmailStr
country: str
score: int
class CreateTaskArgs(BaseModel):
owner_id: str
lead_email: EmailStr
summary: str
due_minutes: int
class SendHandoffEmailArgs(BaseModel):
owner_id: str
lead_email: EmailStr
template_id: str
class ToolReceipt(BaseModel):
tool: str
ok: bool
ref: str
message: str = ""
data: Optional[Dict] = None
# adapters.py (demo logic)
from tools import *
from datetime import datetime
CRM = {"contacts": {"[email protected]": {"owner_id":"AE-44", "account_id":"ACC-9"}}}
ACCOUNTS = {"ACME INC": {"employees": 420, "segment":"MM"}}
TERRITORY = {"US":"SDR-US-1","CA":"SDR-CA-2","DE":"SDR-EMEA-3"}
def normalize(a: NormalizeArgs) -> ToolReceipt:
email = a.email.lower().strip()
company = a.company.upper().strip()
country = a.country.upper().strip()
return ToolReceipt(tool="Normalize", ok=True, ref=f"norm-{email}",
data={"email":email,"full_name":a.full_name.strip(),"company":company,"country":country,"interest":a.interest.strip()})
def enrich(a: EnrichArgs) -> ToolReceipt:
employees = ACCOUNTS.get(a.company.upper(), {}).get("employees", 50)
return ToolReceipt(tool="Enrich", ok=True, ref=f"enrich-{a.email}", data={"employees": employees, "site_visits": 3})
def score_lead(a: ScoreLeadArgs) -> ToolReceipt:
score = min(100, a.signals.get("employees",0)//3 + a.signals.get("site_visits",0)*5 + a.signals.get("use_case_match",0)*30)
return ToolReceipt(tool="ScoreLead", ok=True, ref="score", data={"score": score})
def check_duplicate(a: CheckDuplicateArgs) -> ToolReceipt:
if a.email in CRM["contacts"]:
owner_id = CRM["contacts"][a.email]["owner_id"]
return ToolReceipt(tool="CheckDuplicate", ok=True, ref="dup-found", data={"duplicate": True, "owner_id": owner_id})
return ToolReceipt(tool="CheckDuplicate", ok=True, ref="dup-none", data={"duplicate": False})
def route_lead(a: RouteLeadArgs) -> ToolReceipt:
owner = TERRITORY.get(a.country, "SDR-ROW-0")
queue = f"queue-{owner}"
sla = 15 if a.country in TERRITORY else 30
return ToolReceipt(tool="RouteLead", ok=True, ref=f"route-{a.email}", data={"owner_id": owner, "queue": queue, "sla_minutes": sla})
def create_task(a: CreateTaskArgs) -> ToolReceipt:
return ToolReceipt(tool="CreateTask", ok=True, ref=f"task-{a.owner_id}", data={"due_minutes": a.due_minutes})
def send_handoff_email(a: SendHandoffEmailArgs) -> ToolReceipt:
return ToolReceipt(tool="SendHandoffEmail", ok=True, ref=f"mail-{a.owner_id}", data={"template_id": a.template_id})

Join the conversation! Your thoughts help the community grow.