An AI dialer for real estate works by pulling a qualified pool from your CRM, placing human-sounding outbound calls, classifying outcomes, and writing dispositions back to the CRM while hard guards prevent false bookings and vendor overage. We built and shipped this for a real estate brokerage. This guide shows how it works and what tripped us up.
AI dialer for real estate: a system that calls and qualifies large lead backlogs without human dialer hours by combining a voice agent, deterministic booking guards, and CRM-synced state.
The problem it solves
Operators with big backlogs spend hours power-dialing stale leads. Lists go cold, reps burn time re-calling wrong segments, and CRMs end up with optimistic dispositions and ghost bookings. The work is simple repetition: call, qualify, disposition, and set a real appointment or stop the churn.
| Manual dialing | Automated AI dialer |
|---|---|
| Reps work through CSVs and stage filters by hand | CRM-scoped pool auto-syncs with stage and freshness rules |
| Inconsistent notes and missed dispositions | Structured outcomes and idempotent write-backs |
| False bookings slip through | Deterministic guards block and retract bad bookings |
| Vendor limits surprise you mid-campaign | Quota checks pause before overage costs |
| Calling stops when the rep is off | Scheduler runs inside quiet hours, every business day |
How the automation works
The architecture is a small service around a voice engine: a scheduler pulls a CRM-filtered lead list, calls one at a time, classifies the outcome with an AI+rules combo, writes results back, and pauses if vendor quotas or safety checks fire.
- Lead pool and filters: We sync only the right records: target stages, no recent contact, age window, and DNC/status exclusions. On each sync we also evict anything that disappeared from the CRM to avoid dialing stale leads.
- Call scheduler and agent: A loop respects office hours and concurrency caps. The voice agent runs a concise prompt with tools for confirm, reschedule, and opt-out. Silence handling and retries are tuned for live answer vs voicemail.
- Outcome classifier with guards: An AI classifier suggests a disposition. Deterministic checks then enforce business rules: no booking unless we heard a clear confirmation, no time accepted without a calendar lock, no PII-only signals.
- State ledger and CRM write-back: Every attempt and final disposition is recorded with a dedupe key. Write-backs retry on transient errors and never fan out duplicates.
- Quota and safety controls: We poll TTS/STT vendor quotas and pause before overage. A simple pause/resume endpoint lets ops freeze campaigns instantly.
Step-by-step: how to build it
1) Normalize the lead pool from your CRM
Read your target segment, apply freshness rules, then reconcile against a local ledger so removed CRM records get deactivated. The guard against stale leads mattered more than we expected.
// reconcileLeadPool.js
export function reconcileLeadPool({crmLeads, ledger}) {
const activeIds = new Set(crmLeads.map(l => l.id));
const next = new Map();
// Upsert new and existing
for (const lead of crmLeads) {
const prev = ledger.get(lead.id) || { attempts: 0, active: true };
next.set(lead.id, {
...prev,
id: lead.id,
phone: lead.phone,
stage: lead.stage,
lastContactedAt: lead.lastContactedAt || null,
active: true
});
}
// Evict anything missing from the CRM fetch
for (const [id, row] of ledger.entries()) {
if (!activeIds.has(id)) next.set(id, { ...row, active: false, evictedAt: Date.now() });
}
return next;
}Key gotcha: treat the CRM as the source of truth every sync. We shipped an evict-on-sync path to retire 640 stale leads in production and stopped accidental calls to old numbers.
2) Build a scheduler that respects quiet hours
A simple loop with a per-tenant window is safer than concurrency. We randomized within a window to avoid robotic cadence.
// scheduler.js
import { isWithinInterval } from 'date-fns';
export function shouldCallNow({tz, start, end}) {
const now = new Date().toLocaleString('en-US', { timeZone: tz });
const d = new Date(now);
const s = new Date(d); s.setHours(start.h, start.m, 0, 0);
const e = new Date(d); e.setHours(end.h, end.m, 0, 0);
return isWithinInterval(d, { start: s, end: e }) && [1,2,3,4,5].includes(d.getDay());
}
export async function runDialer(tick) {
if (!(await tick.preflight())) return;
const lead = await tick.nextLead();
if (!lead) return;
await tick.call(lead);
}Key gotcha: always store the tenant timezone and quiet hours with the campaign. Never infer from the server clock.
3) Define the voice agent and tools
Keep the prompt short and procedural. Tools are narrow: confirm, reschedule, opt-out, update notes.
{
"agent": {
"voice": "conversational_female_01",
"system": "You qualify real estate leads. Be clear, brief, and confirm before booking. Never promise exact times without a calendar lock.",
"tools": [
{"name": "confirm_booking", "args": ["time", "channel"]},
{"name": "propose_times", "args": ["options"]},
{"name": "mark_opt_out", "args": ["reason"]},
{"name": "capture_notes", "args": ["summary"]}
]
}
}Key gotcha: do not let the model do calendar math. The scheduler proposes times. The agent only confirms from a supplied list.
4) Classify outcomes, then enforce deterministic guards
We pair an AI label with hard checks that must pass for a booking.
// classify.js
export function guardBooking({aiLabel, transcript, calendarHold}) {
const saidYes = /\b(yes|that works|let's do|sounds good)\b/i.test(transcript);
const saidNo = /\b(no|not now|busy|another time)\b/i.test(transcript);
if (saidNo) return { ok: false, reason: 'declined' };
if (aiLabel !== 'booking_intent') return { ok: false, reason: 'no_intent' };
if (!saidYes) return { ok: false, reason: 'no_explicit_confirm' };
if (!calendarHold) return { ok: false, reason: 'no_calendar_lock' };
return { ok: true };
}Key gotcha: our pre-fix model labeled polite interest as a booking. The guard above ended a streak of false bookings overnight.
5) Write back to the CRM idempotently
Use a deterministic dedupe key and retry on transient failures. We also logged write errors for backfill.
// writeback.js
export async function writeDisposition({crm, leadId, disp, meta}) {
const key = `disp:${leadId}:${disp}:${meta?.ts || Date.now()}`;
if (await crm.hasKey(key)) return 'duplicate_skip';
try {
await crm.updateLead({ id: leadId, disposition: disp, notes: meta?.notes || '' });
await crm.putKey(key);
return 'ok';
} catch (e) {
await crm.logError({ key, leadId, err: String(e) });
throw e;
}
}Key gotcha: a stale API key produced 401s for hundreds of attempted write-backs. The error log made the backfill straightforward once we rotated credentials.
6) Add a vendor quota guard that can pause campaigns
Poll your TTS/STT quota, cache it, and pause safely before overage billing kicks in.
// quota.js
let cache = { until: 0, used: 0, limit: 0 };
export async function checkQuota(api) {
const now = Date.now();
if (now < cache.until) return cache;
const q = await api.getUsage(); // provider SDK call
cache = { until: now + 10 * 60 * 1000, used: q.used, limit: q.limit };
return cache;
}
export async function shouldPause(api) {
const { used, limit } = await checkQuota(api);
return limit > 0 && used / limit > 0.92; // pause with headroom
}Key gotcha: we saw 628,996 of 500,000 TTS characters used before the vendor cut us off, which incurred overage. The pause made that impossible to repeat.
7) Add a rescind path for bad bookings
If a guard triggers after the fact, have a single click to retract and notify.
// rescind.js
app.post('/api/rescind-booking', async (req, res) => {
const { leadId, bookingId, reason } = req.body;
await calendar.cancel(bookingId, { reason });
await crm.updateLead({ id: leadId, disposition: 'booking_retracted', notes: `Retracted: ${reason}` });
await notify.ops(`Retracted booking ${bookingId} for ${leadId}: ${reason}`);
res.json({ ok: true });
});Key gotcha: keep this behind auth and log every action. It is your safety net.
Where it gets complicated
- LLM optimism vs hard reality. Early on, 32 of 44 booking classifications were false. Tightening the prompt helped, but the real fix was a hard guard: no explicit yes, no calendar hold, no booking. After that change we saw no new false bookings.
- Stale leads without evict-on-sync. An upsert-only sync called a stakeholder's personal number because the CRM record had been deleted upstream. Eviction on each sync retired 640 stale rows and shrank the retry queue safely.
- Vendor overage is silent until it is expensive. Our TTS provider kept speaking past plan limits and billed overage. A 10-minute cached quota poll with a pause switch prevented repeat charges and protected deliverability pacing.
- Auth and redeploy drift. A rotated CRM key caused 401s on hundreds of write-backs. We built error buckets and a replay job so a single rotate and a click restored parity.
- Quiet hours and local time. Campaigns spanning timezones will call at the wrong times if you do not store the tenant timezone and compute windows per lead. We never infer from server time.
What this actually changes
For a real estate brokerage running this in production, the system processed 3,130 of 3,130 callable leads, placed 5,573 total calls, and produced about 12 real bookings, with no new false bookings after we shipped the guard fix. Evict-on-sync retired 640 stale leads and cleaned the pool. The value is structural: the backlog moves every day without human dialer hours.
One external benchmark explains why this matters: the odds of qualifying a lead reportedly drop by about 10 times after five minutes without a response (Harvard Business Review, The Short Life of Online Sales Leads, https://hbr.org/2011/03/the-short-life-of-online-sales-leads). A dialer that runs all day keeps you inside that window far more often than a part-time rep.
Frequently asked questions
Does this replace my power dialer?
It replaces the backlog grind and first-pass qualification. You can keep a power dialer for live rep follow-ups. The AI runs the initial call, classifies outcomes, and books only when deterministic checks pass, then hands off warm leads to humans.
Will it work with my CRM?
If your CRM exposes a way to read leads and write dispositions, yes. We design the sync so the CRM remains the source of truth, and we keep a local ledger to avoid duplicates and to evict stale records safely.
How do you prevent false bookings?
Two layers: an AI labeler for intent, then rule-based guards that require an explicit verbal yes and a confirmed calendar hold. If either is missing, it cannot book. We also include a one-click rescind path to undo mistakes safely.
Can it run in real time?
The scheduler can poll continuously within quiet hours and call new leads within minutes. We batch the pool for safety and idempotency, but new records can enter the queue on the next short tick and get worked quickly.
What does this cost monthly?
Run cost is driven by volume: minutes of audio and number of calls. We add a quota guard that pauses before provider overage. Implementation is a one-time build with light ongoing hosting and monitoring.
How long does it take to launch?
A baseline deployment typically ships in weeks, not months. The gating items are CRM access, calendar rules, and approval of the guardrails. We run a short shadow mode before going live.
If you have a real estate backlog and want it worked every day without adding callers, we have shipped this exact system and hardened it in production. See our related post on a CRM-scoped build for brokerages: Automate Follow Up Boss AI Calling. If you prefer voice over text for first contact, start with our AI voice agents overview. When you are ready to scope your pool and guardrails, book a 15-minute call.
Want us to build this for you?
15-minute discovery call. No pitch. We tell you what to automate first.
Book a Discovery Call