Module 25 · ai · intermediate
Lesson 25 · AI Lead Qualifier and Booking Agent
Goal
Inbound leads wait hours for a reply, then days of back-and-forth to find a meeting time. Build an agent that asks the qualifying questions immediately and books good leads straight into your calendar.
Prerequisites
- Modules 15 (AI with a fallback), 19 (conversation state, injected clock), 22 (Merge before shared steps)
- For the live part: OpenAI, Google Calendar and Gmail credentials
Concepts
- AI reads, code decides. The model only extracts facts from a message (need, budget, timeline, decision maker, which slot). Code decides what to say next, the score, and whether to book. Replies come from templates, so the agent never promises something odd, and everything still works when AI is off: rules extract the same facts from common phrasings ("around $8k", "next month", "I'm the founder").
- One question at a time. The lead record remembers what was asked last, which also helps the rules ("yes" means something only after the decision-maker question).
- Score with reasons. Need 30, budget ≥ minimum 30, timeline within 12 weeks 20, decision maker 20, each with a human-readable reason for the owner's email.
- Polite exits. Budget too low → not a fit with free resources. Too far out → nurture with a follow-up date. "Can I talk to a person?" → handoff and an owner alert.
- Finding slots. Walk each day in the business's timezone in 30-minute steps within working hours; skip anything before now + notice or overlapping a busy block ± buffer; take the first free slot per day until there are three.
- Holds. Offered slots are held for 30 minutes and count as busy (with buffer) for other leads, so two people can't book the same time, or back-to-back without a gap.
- Never invent availability. If Google Calendar can't be read, the agent offers no times and the owner schedules by hand.
- Internal calendar.
calendar_mode: internalkeeps bookings in n8n. It's useful for businesses without Google Calendar, and it makes the whole flow testable without credentials.
Build it
Step 1: Endpoints, Settings, Route
Webhooks New Lead API (POST lq/lead), Reply API (POST lq/reply), Book API (POST lq/book), Lead Status API (GET lq/lead/status), Cancel API (POST lq/cancel) → Edit Fields Settings (edit me): business/owner, services (name + keywords, most specific first), min_budget, max_timeline_weeks, resources_url, calendar_mode, calendar_id, timezone, hours, meeting_minutes, buffer_minutes, min_notice_hours, horizon_days, hold_minutes, use_ai → Switch Route.
Step 2: Intake and extraction
- Code
Intake(intake.js): validate, find by email or create, save the message → IFValid?(400/404 otherwise). - IF
Use AI?→ Basic LLM ChainExtract Answers(prompt incode/ai-prompt.txt) + OpenAI + Structured Output Parser, On Error → Continue → MergeJoin AI. - Code
Pick Extraction(pick.js): rules first, AI values on top when present.
Step 3: Busy times
IF Need Calendar? (Google mode and no busy in the request) → Google Calendar Event → Get Many (time range now → horizon, single events, On Error → Continue, Always Output Data) → Merge Join Calendar → Code Busy Times (busy.js).
Step 4: Decide and book
- Code
Decide Next Step(decide.js): merge answers (frozen once slots are offered), score, next question or outcome, slot finding and holds, slot choice by number, day/time words or slot id. - IF
Book?→ IFGoogle Calendar?→ Google Calendar Event → Create (start/end, summary, description with the score and conversation, attendee = lead, send updates) → CodeRecord Booking(record.js): booked only with an event id, then release holds. Internal mode goes straight toRecord Booking. - Merge
Join Outcome→ Respond; Gmail to the owner on bookings/handoffs/manual scheduling; Gmail confirmation to the lead for internal bookings (Google sends its own invite).
Step 5: Status and cancel
Code Lead Status (status.js) and Cancel Booking (cancel.js) → respond.
Publish (static data only persists for production executions).
Test it
./test/test.sh # 48 assertions, internal calendar, cleans up after itself
WITH_CREDS=1 CAL=1 ./test/test.sh # AI extraction + real Google Calendar busy times
Try a conversation:
curl -X POST http://localhost:5678/webhook/lq/lead -H 'Content-Type: application/json' \
-d '{"name":"Priya","email":"priya@example.com","message":"We want our CRM synced with HubSpot","calendar_mode":"internal"}'
# then POST lq/reply with {"lead_id":"L-…","message":"around $8k","calendar_mode":"internal"} …
Common problems
- Slots in the wrong hours:
timezonein Settings must be the business's zone; Google returns times in UTC. - Two leads booked back to back: make sure holds and bookings are treated as busy with the buffer (they are in
decide.js). - "Calendar parameter's value is invalid": the Google Calendar credential is missing or
calendar_idis wrong (primaryor the calendar's email address). - Every lead gets "not a fit":
min_budgettoo high, or budgets written in words ("eight grand") need the AI.