Building a Lead Qualification Chat Agent with OpenAI
Build a production lead qualification chat agent using OpenAI's Agents SDK with BANT scoring, structured outputs, CRM integration tools, and intelligent conversation routing.
Why Automate Lead Qualification?
Sales teams waste up to 67% of their time on leads that will never convert. A lead qualification chat agent automates the initial screening process, asks the right discovery questions, scores leads using a proven framework, and routes qualified prospects to the right sales rep — all in real time.
In this post, we will build a complete lead qualification chat agent using OpenAI's Agents SDK. The agent uses the BANT framework (Budget, Authority, Need, Timeline), produces structured qualification scores, integrates with a CRM, and routes conversations based on qualification results.
The BANT Qualification Framework
BANT is one of the most widely used sales qualification methodologies:
flowchart TD
START["Building a Lead Qualification Chat Agent with Ope…"] --> A
A["Why Automate Lead Qualification?"]
A --> B
B["The BANT Qualification Framework"]
B --> C
C["Defining Structured Output Models"]
C --> D
D["CRM Integration Tools"]
D --> E
E["Building the Qualification Agent"]
E --> F
F["Running the Agent in a Chat Loop"]
F --> G
G["Scoring Logic Enhancements"]
G --> H
H["Conversation Routing Based on Qualifica…"]
H --> DONE["Key Takeaways"]
style START fill:#4f46e5,stroke:#4338ca,color:#fff
style DONE fill:#059669,stroke:#047857,color:#fff
- Budget: Does the prospect have the budget for your solution?
- Authority: Is the person a decision-maker or influencer?
- Need: Does the prospect have a genuine pain point you solve?
- Timeline: Is there urgency or a defined implementation window?
Each dimension gets scored 0-25, for a total qualification score of 0-100. Leads scoring 70+ are "sales-qualified," 40-69 are "marketing-qualified," and below 40 are "nurture."
Defining Structured Output Models
First, we define Pydantic models that enforce structured qualification data:
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
from pydantic import BaseModel, Field
from enum import Enum
from typing import Optional
class LeadTier(str, Enum):
SALES_QUALIFIED = "sales_qualified"
MARKETING_QUALIFIED = "marketing_qualified"
NURTURE = "nurture"
DISQUALIFIED = "disqualified"
class BANTScore(BaseModel):
budget_score: int = Field(ge=0, le=25, description="Budget fit score 0-25")
budget_reasoning: str = Field(description="Why this budget score was assigned")
authority_score: int = Field(ge=0, le=25, description="Authority score 0-25")
authority_reasoning: str = Field(description="Role and decision power assessment")
need_score: int = Field(ge=0, le=25, description="Need/pain score 0-25")
need_reasoning: str = Field(description="Problem-solution fit assessment")
timeline_score: int = Field(ge=0, le=25, description="Timeline urgency 0-25")
timeline_reasoning: str = Field(description="Implementation timeline assessment")
@property
def total_score(self) -> int:
return (self.budget_score + self.authority_score +
self.need_score + self.timeline_score)
@property
def tier(self) -> LeadTier:
if self.total_score >= 70:
return LeadTier.SALES_QUALIFIED
elif self.total_score >= 40:
return LeadTier.MARKETING_QUALIFIED
else:
return LeadTier.NURTURE
class QualifiedLead(BaseModel):
contact_name: str
company: str
email: Optional[str] = None
role: Optional[str] = None
company_size: Optional[str] = None
bant: BANTScore
summary: str = Field(description="Two-sentence qualification summary")
next_action: str = Field(description="Recommended next step for sales team")
CRM Integration Tools
The agent needs tools to look up existing contacts, create new leads, and update qualification data:
from agents import function_tool
import httpx
CRM_BASE_URL = "https://api.yourcrm.com/v1"
CRM_API_KEY = "your-crm-api-key"
@function_tool
async def lookup_crm_contact(email: str) -> str:
"""Search the CRM for an existing contact by email address.
Returns contact details and deal history if found."""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{CRM_BASE_URL}/contacts/search",
params={"email": email},
headers={"Authorization": f"Bearer {CRM_API_KEY}"},
)
if resp.status_code == 200:
data = resp.json()
if data.get("results"):
contact = data["results"][0]
return (
f"Existing contact found: {contact['name']} at "
f"{contact['company']}, {contact.get('deal_count', 0)} "
f"previous deals, lifetime value ${contact.get('ltv', 0)}"
)
return "No existing contact found in CRM."
@function_tool
async def create_crm_lead(
name: str,
company: str,
email: str,
score: int,
tier: str,
notes: str,
) -> str:
"""Create a new lead in the CRM with qualification data."""
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{CRM_BASE_URL}/leads",
headers={"Authorization": f"Bearer {CRM_API_KEY}"},
json={
"name": name,
"company": company,
"email": email,
"lead_score": score,
"lead_tier": tier,
"qualification_notes": notes,
"source": "chat_agent",
},
)
if resp.status_code == 201:
lead_id = resp.json()["id"]
return f"Lead created successfully with ID: {lead_id}"
return f"Failed to create lead: {resp.text}"
@function_tool
async def schedule_sales_meeting(
lead_email: str,
rep_email: str,
topic: str,
) -> str:
"""Schedule a discovery call between a qualified lead and sales rep."""
async with httpx.AsyncClient() as client:
resp = await client.post(
f"{CRM_BASE_URL}/meetings",
headers={"Authorization": f"Bearer {CRM_API_KEY}"},
json={
"attendees": [lead_email, rep_email],
"topic": topic,
"duration_minutes": 30,
"type": "discovery_call",
},
)
if resp.status_code == 201:
link = resp.json().get("booking_link", "")
return f"Meeting scheduled. Booking link: {link}"
return "Could not schedule meeting. Please try again."
Building the Qualification Agent
Now we wire everything together with the Agents SDK. The key is using output_type for structured qualification and handoffs for routing:
flowchart TD
CENTER(("Core Concepts"))
CENTER --> N0["Budget: Does the prospect have the budg…"]
CENTER --> N1["Authority: Is the person a decision-mak…"]
CENTER --> N2["Need: Does the prospect have a genuine …"]
CENTER --> N3["Timeline: Is there urgency or a defined…"]
CENTER --> N4["Rate limiting — Prevent abuse by limiti…"]
CENTER --> N5["Human escalation — Always provide a quo…"]
style CENTER fill:#4f46e5,stroke:#4338ca,color:#fff
from agents import Agent, Runner, handoff
# Specialist agent for high-value leads
sales_handoff_agent = Agent(
name="Sales Handoff Agent",
instructions="""You handle sales-qualified leads (score 70+).
Thank them for their time, confirm their details, and let them
know a senior account executive will reach out within 2 hours.
Use schedule_sales_meeting to book a discovery call.""",
tools=[schedule_sales_meeting],
)
# Specialist agent for mid-tier leads
nurture_agent = Agent(
name="Nurture Agent",
instructions="""You handle marketing-qualified leads (score 40-69).
Thank them, offer to send relevant case studies or whitepapers,
and add them to the appropriate email nurture sequence.""",
tools=[create_crm_lead],
)
# Main qualification agent
qualification_agent = Agent(
name="Lead Qualification Agent",
instructions="""You are a friendly, professional lead qualification
assistant for Acme Software. Your job is to have a natural
conversation that uncovers BANT qualification criteria.
CONVERSATION FLOW:
1. Greet warmly and ask what brought them to Acme
2. Understand their NEED: What problem are they solving?
3. Assess AUTHORITY: What is their role? Who else is involved?
4. Explore BUDGET: What are they currently spending? Is there
an allocated budget for a new solution?
5. Determine TIMELINE: When do they need a solution in place?
RULES:
- Never ask BANT questions directly (e.g., "What is your budget?")
- Weave questions naturally into the conversation
- If they mention an email, look them up in the CRM
- After gathering enough info, use the scoring tool
- Route to the appropriate specialist based on score
Be conversational, not interrogative. Mirror their tone.""",
tools=[lookup_crm_contact, create_crm_lead],
handoffs=[
handoff(sales_handoff_agent, tool_name_override="route_to_sales",
tool_description_override="Route sales-qualified leads (70+) to a sales rep"),
handoff(nurture_agent, tool_name_override="route_to_nurture",
tool_description_override="Route marketing-qualified leads (40-69) to nurture"),
],
output_type=QualifiedLead,
)
Running the Agent in a Chat Loop
import asyncio
from agents import Runner
async def run_qualification_chat():
print("Lead Qualification Agent ready. Type 'quit' to exit.\n")
result = None
while True:
user_input = input("Visitor: ").strip()
if user_input.lower() == "quit":
break
if result is None:
result = await Runner.run(
qualification_agent,
input=user_input,
)
else:
result = await Runner.run(
qualification_agent,
input=user_input,
context=result.context,
)
# Check if we got structured output (qualification complete)
if isinstance(result.final_output, QualifiedLead):
lead = result.final_output
print(f"\n--- QUALIFICATION COMPLETE ---")
print(f"Lead: {lead.contact_name} at {lead.company}")
print(f"Score: {lead.bant.total_score}/100 ({lead.bant.tier.value})")
print(f"Summary: {lead.summary}")
print(f"Next Action: {lead.next_action}")
break
else:
print(f"Agent: {result.final_output}\n")
asyncio.run(run_qualification_chat())
Scoring Logic Enhancements
For production, you want the agent to score incrementally as information is revealed. Add a scoring tool that the agent calls mid-conversation:
@function_tool
def score_lead_progress(
budget_evidence: str,
authority_evidence: str,
need_evidence: str,
timeline_evidence: str,
) -> str:
"""Evaluate current qualification evidence and return a preliminary
BANT score. Call this after gathering at least 2 BANT dimensions."""
scores = {}
for dimension, evidence in [
("budget", budget_evidence),
("authority", authority_evidence),
("need", need_evidence),
("timeline", timeline_evidence),
]:
if not evidence or evidence.lower() == "unknown":
scores[dimension] = 0
elif any(w in evidence.lower() for w in ["confirmed", "strong", "yes"]):
scores[dimension] = 20
elif any(w in evidence.lower() for w in ["likely", "possible", "maybe"]):
scores[dimension] = 12
else:
scores[dimension] = 5
total = sum(scores.values())
return (
f"Preliminary score: {total}/100. "
f"Budget: {scores['budget']}/25, Authority: {scores['authority']}/25, "
f"Need: {scores['need']}/25, Timeline: {scores['timeline']}/25. "
f"{'Ready to qualify' if total >= 40 else 'Need more information'}."
)
Conversation Routing Based on Qualification
The handoff pattern above handles routing automatically. When the agent determines a lead is sales-qualified, it invokes the route_to_sales handoff, which transfers the conversation to the sales_handoff_agent. The sales agent has access to the meeting scheduler and can book a discovery call immediately.
For leads that do not meet the sales threshold, the route_to_nurture handoff sends them to a gentler follow-up flow. This two-tier routing ensures that sales reps only spend time on high-quality prospects while lower-scoring leads still receive attention.
Production Considerations
When deploying this agent:
- Persist conversation state — Store each turn in a database so leads can return and continue qualification
- Track partial qualifications — Many visitors leave mid-conversation; save partial BANT data for follow-up
- Rate limiting — Prevent abuse by limiting messages per session
- Human escalation — Always provide a "talk to a human" escape hatch
- GDPR compliance — Inform visitors that conversation data is being collected and processed
This pattern scales well: the same BANT framework can be adapted for different products by changing the agent instructions, and the CRM tools work with any REST-based CRM API.
Written by
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.