AI Lead Qualifier and Booking Agent
Module 25 · ai · intermediate
starter.json
In n8n, copy this file and paste it onto the canvas, or use Import from File.
{
"name": "25 · AI Lead Qualifier and Booking Agent (starter)",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "lq/lead",
"responseMode": "responseNode",
"options": {}
},
"id": "m25-new-lead-api",
"name": "New Lead API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
0
],
"webhookId": "course-25-lead"
},
{
"parameters": {
"httpMethod": "POST",
"path": "lq/reply",
"responseMode": "responseNode",
"options": {}
},
"id": "m25-reply-api",
"name": "Reply API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
180
],
"webhookId": "course-25-reply"
},
{
"parameters": {
"httpMethod": "POST",
"path": "lq/book",
"responseMode": "responseNode",
"options": {}
},
"id": "m25-book-api",
"name": "Book API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
360
],
"webhookId": "course-25-book"
},
{
"parameters": {
"httpMethod": "GET",
"path": "lq/lead/status",
"responseMode": "responseNode",
"options": {}
},
"id": "m25-lead-status-api",
"name": "Lead Status API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
600
],
"webhookId": "course-25-status"
},
{
"parameters": {
"httpMethod": "POST",
"path": "lq/cancel",
"responseMode": "responseNode",
"options": {}
},
"id": "m25-cancel-api",
"name": "Cancel API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
780
],
"webhookId": "course-25-cancel"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "s1",
"name": "business_name",
"value": "Northstar Automation",
"type": "string"
},
{
"id": "s2",
"name": "owner_name",
"value": "Jordan",
"type": "string"
},
{
"id": "s3",
"name": "owner_email",
"value": "jordan@northstar.example",
"type": "string"
},
{
"id": "s4",
"name": "services",
"value": "={{ [{\"name\": \"AI chatbot\",\"keywords\": [\"chatbot\",\"chat bot\",\"ai assistant\",\"support bot\",\"whatsapp bot\"]},{\"name\": \"CRM integration\",\"keywords\": [\"crm\",\"hubspot\",\"salesforce\",\"pipedrive\",\"gohighlevel\",\"integrat\"]},{\"name\": \"reporting dashboard\",\"keywords\": [\"report\",\"dashboard\",\"kpi\",\"analytics\"]},{\"name\": \"workflow automation\",\"keywords\": [\"automat\",\"workflow\",\"zapier\",\"make.com\",\"n8n\",\"manual\"]}] }}",
"type": "array"
},
{
"id": "s5",
"name": "min_budget",
"value": 3000,
"type": "number"
},
{
"id": "s6",
"name": "max_timeline_weeks",
"value": 12,
"type": "number"
},
{
"id": "s7",
"name": "resources_url",
"value": "https://northstar.example/free-automation-guides",
"type": "string"
},
{
"id": "s8",
"name": "calendar_mode",
"value": "google",
"type": "string"
},
{
"id": "s9",
"name": "calendar_id",
"value": "primary",
"type": "string"
},
{
"id": "s10",
"name": "timezone",
"value": "America/Chicago",
"type": "string"
},
{
"id": "s11",
"name": "hours",
"value": "={{ {\"mon\": {\"open\": 9,\"close\": 17},\"tue\": {\"open\": 9,\"close\": 17},\"wed\": {\"open\": 9,\"close\": 17},\"thu\": {\"open\": 9,\"close\": 17},\"fri\": {\"open\": 9,\"close\": 17} } }}",
"type": "object"
},
{
"id": "s12",
"name": "meeting_minutes",
"value": 30,
"type": "number"
},
{
"id": "s13",
"name": "buffer_minutes",
"value": 15,
"type": "number"
},
{
"id": "s14",
"name": "min_notice_hours",
"value": 4,
"type": "number"
},
{
"id": "s15",
"name": "horizon_days",
"value": 7,
"type": "number"
},
{
"id": "s16",
"name": "hold_minutes",
"value": 30,
"type": "number"
},
{
"id": "s17",
"name": "use_ai",
"value": true,
"type": "boolean"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "m25-settings-edit-me",
"name": "Settings (edit me)",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
240,
300
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "r1",
"leftValue": "={{ $('New Lead API').isExecuted || $('Reply API').isExecuted || $('Book API').isExecuted }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "message"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "r2",
"leftValue": "={{ $('Lead Status API').isExecuted }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "status"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "r3",
"leftValue": "={{ $('Cancel API').isExecuted }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "cancel"
}
]
},
"options": {}
},
"id": "m25-route",
"name": "Route",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
460,
300
]
},
{
"parameters": {
"jsCode": "// Find or create the lead and add the incoming message. One lead per email address.\nconst s = $('Settings (edit me)').first().json;\nconst which = $('New Lead API').isExecuted ? 'lead' : $('Reply API').isExecuted ? 'reply' : 'book';\nconst req = $(which === 'lead' ? 'New Lead API' : which === 'reply' ? 'Reply API' : 'Book API').first().json;\nconst b = req.body ?? {};\nconst db = $getWorkflowStaticData('global');\ndb.leads ??= {}; db.holds ??= {}; db.bookings ??= {};\nconst now = b.now || new Date().toISOString(); // tests inject the clock\n\nconst fail = (code, error) => [{ json: { ok: false, code, error } }];\nlet lead;\nif (which === 'lead') {\n const email = String(b.email ?? '').trim().toLowerCase();\n if (!/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(email)) return fail(400, 'a valid email is required');\n lead = Object.values(db.leads).find((l) => l.email === email);\n if (!lead) {\n const id = 'L-' + Math.random().toString(36).slice(2, 8).toUpperCase();\n lead = db.leads[id] = { id, email, name: String(b.name ?? '').trim(), company: String(b.company ?? '').trim(), phone: String(b.phone ?? ''),\n source: String(b.source ?? 'website'), channel: b.channel === 'email' ? 'email' : 'chat', created_at: now,\n answers: {}, asked: null, status: 'qualifying', score: 0, reasons: [], messages: [], offered: [], booking: null };\n }\n // Structured answers from a form count as answers already given\n for (const k of ['need', 'budget', 'timeline', 'decision_maker']) if (b[k] !== undefined && b[k] !== '') lead.answers[k] = b[k];\n} else {\n lead = db.leads[String(b.lead_id ?? '')];\n if (!lead) return fail(404, 'unknown lead_id');\n}\nconst text = String(which === 'book' ? '' : b.message ?? b.text ?? '').trim().slice(0, 2000);\nif (text) lead.messages.push({ from: 'lead', text, at: now });\nlead.updated_at = now;\n\nreturn [{ json: {\n ok: true, which, lead_id: lead.id, text, slot_id: which === 'book' ? String(b.slot_id ?? '') : null, now,\n calendar_mode: b.calendar_mode ?? s.calendar_mode, busy_override: Array.isArray(b.busy) ? b.busy : null,\n asked: lead.asked, answers: lead.answers, offered: lead.offered, status: lead.status,\n use_ai: s.use_ai && b.ai !== false && Boolean(text) && ['qualifying', 'offered'].includes(lead.status),\n} }];\n"
},
"id": "m25-intake",
"name": "Intake",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
700,
60
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.ok }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "m25-valid",
"name": "Valid?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
920,
60
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { ok: false, error: $json.error } }}",
"options": {
"responseCode": "={{ $json.code }}"
}
},
"id": "m25-respond-error",
"name": "Respond Error",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1140,
240
]
},
{
"parameters": {
"jsCode": "// One lead's record (for checking the flow and debugging).\nconst q = $('Lead Status API').first().json.query ?? {};\nconst db = $getWorkflowStaticData('global');\nconst lead = (db.leads ?? {})[String(q.lead_id ?? '')] ?? Object.values(db.leads ?? {}).find((l) => l.email === String(q.email ?? '').toLowerCase());\nreturn [{ json: lead ? { found: true, ...lead, holds: Object.entries(db.holds ?? {}).filter(([, h]) => h.lead_id === lead.id).map(([k]) => k) } : { found: false } }];\n"
},
"id": "m25-lead-status",
"name": "Lead Status",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
700,
600
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}",
"options": {
"responseCode": "={{ $json.found ? 200 : 404 }}"
}
},
"id": "m25-respond-status",
"name": "Respond Status",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
920,
600
]
},
{
"parameters": {
"content": "## Your task\nLeads are created/found and messages saved (try lq/lead, then lq/lead/status).\n1. **Extract** answers: Basic LLM Chain + OpenAI + structured output (need, budget, timeline_weeks, decision_maker, slot_choice, wants_human), On Error: Continue; rules as fallback; **Merge**.\n2. **Busy times**: Google Calendar → Get Many (On Error: Continue), or busy from the request / internal calendar.\n3. **Decide** (Code): score with reasons, next question, not a fit / nurture / handoff, 3 slots (hours, buffer, notice, one per day), holds, slot choice.\n4. **Book**: Google Calendar → Create (or internal), confirm only with an event id; email the owner and the lead.\n5. Cancel endpoint.\n\nTest: `./test/test.sh`",
"height": 260,
"width": 420
},
"id": "starter-task-note",
"name": "Your Task",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
0,
-340
]
}
],
"connections": {
"New Lead API": {
"main": [
[
{
"node": "Settings (edit me)",
"type": "main",
"index": 0
}
]
]
},
"Reply API": {
"main": [
[
{
"node": "Settings (edit me)",
"type": "main",
"index": 0
}
]
]
},
"Book API": {
"main": [
[
{
"node": "Settings (edit me)",
"type": "main",
"index": 0
}
]
]
},
"Lead Status API": {
"main": [
[
{
"node": "Settings (edit me)",
"type": "main",
"index": 0
}
]
]
},
"Cancel API": {
"main": [
[
{
"node": "Settings (edit me)",
"type": "main",
"index": 0
}
]
]
},
"Settings (edit me)": {
"main": [
[
{
"node": "Route",
"type": "main",
"index": 0
}
]
]
},
"Route": {
"main": [
[
{
"node": "Intake",
"type": "main",
"index": 0
}
],
[
{
"node": "Lead Status",
"type": "main",
"index": 0
}
],
[]
]
},
"Intake": {
"main": [
[
{
"node": "Valid?",
"type": "main",
"index": 0
}
]
]
},
"Valid?": {
"main": [
[],
[
{
"node": "Respond Error",
"type": "main",
"index": 0
}
]
]
},
"Lead Status": {
"main": [
[
{
"node": "Respond Status",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"pinData": {}
}