Skip to content

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: internal keeps 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

  1. Code Intake (intake.js): validate, find by email or create, save the message → IF Valid? (400/404 otherwise).
  2. IF Use AI? → Basic LLM Chain Extract Answers (prompt in code/ai-prompt.txt) + OpenAI + Structured Output Parser, On Error → Continue → Merge Join AI.
  3. 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

  1. 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.
  2. IF Book? → IF Google Calendar? → Google Calendar Event → Create (start/end, summary, description with the score and conversation, attendee = lead, send updates) → Code Record Booking (record.js): booked only with an event id, then release holds. Internal mode goes straight to Record Booking.
  3. 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: timezone in 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_id is wrong (primary or the calendar's email address).
  • Every lead gets "not a fit": min_budget too high, or budgets written in words ("eight grand") need the AI.