---
title: "Building a Lead Qualification Chat Agent with OpenAI"
description: "Build a production lead qualification chat agent using OpenAI's Agents SDK with BANT scoring, structured outputs, CRM integration tools, and intelligent conversation routing."
canonical: https://callsphere.ai/blog/building-lead-qualification-chat-agent-openai
category: "Learn Agentic AI"
tags: ["OpenAI", "Lead Qualification", "Chat", "Sales", "CRM"]
author: "CallSphere Team"
published: 2026-03-14T00:00:00.000Z
updated: 2026-05-07T00:57:33.897Z
---

# 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:

```mermaid
flowchart LR
    LEAD(["Inbound lead"])
    AGENT["AI voice or chat
qualifier"]
    BANT["BANT capture
budget, authority,
need, timing"]
    SCORE{"Lead score
and routing rules"}
    HOT(["Hot — book
AE meeting"])
    WARM(["Warm — SDR
sequence"])
    NURT(["Nurture — drip
and content"])
    CRM[("CRM and SLA timer")]
    LEAD --> AGENT --> BANT --> SCORE
    SCORE -->|Hot| HOT --> CRM
    SCORE -->|Warm| WARM --> CRM
    SCORE -->|Cold| NURT --> CRM
    style AGENT fill:#4f46e5,stroke:#4338ca,color:#fff
    style HOT fill:#059669,stroke:#047857,color:#fff
    style WARM fill:#0ea5e9,stroke:#0369a1,color:#fff
    style NURT fill:#f59e0b,stroke:#d97706,color:#1f2937
```

- **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:

```python
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:

```python
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:

```python
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

```python
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:

```python
@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:

1. **Persist conversation state** — Store each turn in a database so leads can return and continue qualification
2. **Track partial qualifications** — Many visitors leave mid-conversation; save partial BANT data for follow-up
3. **Rate limiting** — Prevent abuse by limiting messages per session
4. **Human escalation** — Always provide a "talk to a human" escape hatch
5. **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.

---

Source: https://callsphere.ai/blog/building-lead-qualification-chat-agent-openai
