By Sagar Shankaran, Founder of CallSphere
Learn how to build an AI agent for HVAC service companies that walks technicians and customers through diagnostic trees, books appointments, looks up parts, and generates quotes automatically.
Key takeaways
HVAC service companies handle hundreds of calls daily — from emergency no-heat situations to routine filter replacements. Each call requires triaging the problem, checking technician availability, looking up compatible parts, and generating accurate quotes. An AI agent can handle this entire workflow, reducing dispatcher workload by 60-70% while ensuring consistent, accurate service.
The key challenge is building a diagnostic engine that mirrors how experienced HVAC technicians think. A furnace that will not ignite could be a dirty flame sensor, a faulty ignitor, a gas valve issue, or a control board failure. The agent must ask the right questions in the right order to narrow down the problem before dispatching a technician with the correct parts.
HVAC diagnostics follow well-established decision trees. We model these as structured data that the agent traverses based on customer responses.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent for home services in your browser — 60 seconds, no signup.
flowchart LR
CALLER(["Homeowner"])
subgraph TEL["Telephony"]
SIP["Twilio SIP and PSTN"]
end
subgraph BRAIN["Field Service AI Agent"]
STT["Streaming STT<br/>Deepgram or Whisper"]
NLU{"Intent and<br/>Entity Extraction"}
TOOLS["Tool Calls"]
TTS["Streaming TTS<br/>ElevenLabs or Rime"]
end
subgraph DATA["Live Data Plane"]
CRM[("CRM and Notes")]
CAL[("Calendar and<br/>Schedule")]
KB[("Knowledge Base<br/>and Policies")]
end
subgraph OUT["Outcomes"]
O1(["Service appointment booked"])
O2(["Quote sent via SMS"])
O3(["Tech dispatched today"])
end
CALLER --> SIP --> STT --> NLU
NLU -->|Lookup| TOOLS
TOOLS <--> CRM
TOOLS <--> CAL
TOOLS <--> KB
NLU --> TTS --> SIP --> CALLER
NLU -->|Resolved| O1
NLU -->|Schedule| O2
NLU -->|Escalate| O3
style CALLER fill:#f1f5f9,stroke:#64748b,color:#0f172a
style NLU fill:#4f46e5,stroke:#4338ca,color:#fff
style O1 fill:#059669,stroke:#047857,color:#fff
style O2 fill:#0ea5e9,stroke:#0369a1,color:#fff
style O3 fill:#f59e0b,stroke:#d97706,color:#1f2937
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class DiagnosticNode:
node_id: str
question: str
options: dict[str, str] # answer -> next_node_id
diagnosis: Optional[str] = None
required_parts: list[str] = field(default_factory=list)
urgency: str = "standard" # standard, same_day, emergency
FURNACE_DIAGNOSTIC_TREE = {
"start": DiagnosticNode(
node_id="start",
question="Is the furnace producing any heat at all?",
options={"no_heat": "check_thermostat", "weak_heat": "check_filter"},
),
"check_thermostat": DiagnosticNode(
node_id="check_thermostat",
question="Is your thermostat set to HEAT mode and set above current room temperature?",
options={"yes": "check_ignition", "no": "thermostat_fix"},
),
"thermostat_fix": DiagnosticNode(
node_id="thermostat_fix",
question=None,
options={},
diagnosis="Thermostat misconfiguration. Adjust settings.",
urgency="standard",
),
"check_ignition": DiagnosticNode(
node_id="check_ignition",
question="Do you hear the furnace clicking or attempting to start?",
options={"yes": "flame_sensor_issue", "no": "control_board_issue"},
),
"flame_sensor_issue": DiagnosticNode(
node_id="flame_sensor_issue",
question=None,
options={},
diagnosis="Likely dirty or failed flame sensor. Technician visit required.",
required_parts=["flame_sensor", "ignitor_backup"],
urgency="same_day",
),
}
Each diagnosis maps to specific parts. The agent needs to check inventory and pricing in real time.
from datetime import datetime
class PartsInventory:
def __init__(self, db_connection):
self.db = db_connection
async def lookup_parts(
self, part_codes: list[str], equipment_model: str
) -> list[dict]:
query = """
SELECT p.part_number, p.description, p.price,
i.quantity_on_hand, p.supplier_lead_days,
c.model_numbers
FROM parts p
JOIN inventory i ON p.part_id = i.part_id
JOIN compatibility c ON p.part_id = c.part_id
WHERE p.category_code = ANY($1)
AND $2 = ANY(c.model_numbers)
ORDER BY i.quantity_on_hand DESC
"""
rows = await self.db.fetch(query, part_codes, equipment_model)
return [
{
"part_number": r["part_number"],
"description": r["description"],
"price": float(r["price"]),
"in_stock": r["quantity_on_hand"] > 0,
"available_date": (
datetime.now().strftime("%Y-%m-%d")
if r["quantity_on_hand"] > 0
else f"{r['supplier_lead_days']} business days"
),
}
for r in rows
]
async def generate_quote(
self, parts: list[dict], labor_hours: float, urgency: str
) -> dict:
parts_total = sum(p["price"] for p in parts)
labor_rate = {"standard": 95, "same_day": 135, "emergency": 185}
labor_cost = labor_hours * labor_rate.get(urgency, 95)
return {
"parts_total": round(parts_total, 2),
"labor_estimate": round(labor_cost, 2),
"total_estimate": round(parts_total + labor_cost, 2),
"urgency": urgency,
"valid_until": "48 hours",
}
The agent checks technician availability and books appointments based on urgency and skill requirements.
from datetime import datetime, timedelta
class HVACScheduler:
def __init__(self, calendar_service):
self.calendar = calendar_service
async def find_available_slots(
self, urgency: str, skill_required: str, zip_code: str
) -> list[dict]:
if urgency == "emergency":
window_start = datetime.now()
window_end = window_start + timedelta(hours=4)
elif urgency == "same_day":
window_start = datetime.now()
window_end = window_start.replace(hour=18, minute=0)
else:
window_start = datetime.now() + timedelta(days=1)
window_end = window_start + timedelta(days=5)
technicians = await self.calendar.get_qualified_techs(
skill=skill_required, service_area=zip_code
)
slots = []
for tech in technicians:
available = await self.calendar.get_open_slots(
tech_id=tech["id"],
start=window_start,
end=window_end,
)
for slot in available:
slots.append({
"technician": tech["name"],
"date": slot["date"],
"time_window": slot["window"],
"estimated_arrival": slot["eta"],
})
return sorted(slots, key=lambda s: s["date"])
The complete agent orchestrates diagnostics, parts lookup, quoting, and scheduling into a single conversational flow.
from agents import Agent, Runner, function_tool
@function_tool
async def diagnose_hvac_issue(symptom: str, responses: dict) -> dict:
"""Walk through the HVAC diagnostic tree based on customer symptoms."""
node = FURNACE_DIAGNOSTIC_TREE.get("start")
for answer in responses.values():
next_id = node.options.get(answer)
if next_id:
node = FURNACE_DIAGNOSTIC_TREE.get(next_id, node)
if node.diagnosis:
return {
"diagnosis": node.diagnosis,
"required_parts": node.required_parts,
"urgency": node.urgency,
}
return {"next_question": node.question, "options": list(node.options.keys())}
hvac_agent = Agent(
name="HVAC Service Agent",
instructions="""You are an HVAC service agent. Walk customers through
diagnostic questions to identify their issue. Once diagnosed, look up
required parts, generate a quote, and offer available appointment slots.
Always confirm the equipment model before quoting parts.""",
tools=[diagnose_hvac_issue],
)
Gas leaks and carbon monoxide situations bypass the diagnostic tree entirely. The agent is configured with keyword detection for terms like "gas smell," "CO alarm," or "carbon monoxide." When detected, it immediately instructs the customer to leave the building, call 911, and then dispatches an emergency technician without going through the standard flow.
Still reading? Stop comparing — try CallSphere live.
See the home services AI agent handle a real call — complete, industry-specific, and live in your browser. No signup.
Yes. You create separate diagnostic trees for furnaces, air conditioners, heat pumps, and boilers, then use the equipment type identified early in the conversation to select the correct tree. The DiagnosticNode structure is generic enough to model any branching diagnostic flow.
The quotes are based on real parts pricing from your inventory database and standardized labor times for each repair type. Accuracy typically reaches 85-90% compared to final invoices. The agent presents quotes as estimates and flags when on-site inspection may change the scope.
#HVAC #FieldServiceAI #Troubleshooting #Scheduling #PartsManagement #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
American HVAC and home services companies lose emergency jobs to voicemail after hours. See how CallSphere AI voice and chat agents capture and dispatch every call 24/7.
HVAC, heating, plumbing, and snow-removal companies in Calgary, Winnipeg, and Toronto lose thousands to missed after-hours calls each winter. Here is how AI voice agents fix it.
How we built a fault-tolerant HVAC emergency triage and tech-dispatch platform on Kubernetes — three-tier CQRS, 11 micro-agents on the OpenAI Agents SDK + LangGraph, NATS JetStream, DTMF/SMS/WebSocket acceptance, circuit breakers, and an evaluation pipeline that catches regressions before they wake a tech at 3 AM.
Spring 2026 AC season starts now. With the voice AI market at $47.5B by 2034, HVAC shops without after-hours voice agents will lose to those that have them.
HVAC companies miss 40–60% of inbound. Build a 4-agent dispatch (intake, scheduling, parts, emergency) that integrates with ServiceTitan in 600 lines.
Sacramento businesses lose millions every year to missed calls during legislative session rushes, 107-degree HVAC surges, and Bay Area buyer waves. Here is how CallSphere's AI voice agent captures every call in English, Spanish, Hmong, Russian, and Ukrainian, 24/7.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco