Skip to content

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": {}
}