{
  "name": "AI Lead Qualification & Speed-to-Lead Engine (AI SDR)",
  "nodes": [
    {
      "parameters": {
        "content": "# 🎯 AI Lead Qualification & Speed-to-Lead Engine\n**Problem:** inbound leads wait hours for a reply, reps waste time on bad-fit leads, and follow-ups die after one email.\n**Solution:** an AI SDR that researches, scores and replies to every lead within about a minute, routes hot leads to a rep instantly, and runs a thread-aware follow-up sequence that stops the moment someone replies.\n\n**What it does**\n• Validates + de-dupes submissions, silently drops bots (honeypot, disposable domains, link spam)\n• Enriches with Hunter (deliverability), Apollo (person + firmographics) and the company website\n• Claude research agent (live web search) + Claude scorer with a strict JSON schema\n• Hybrid scoring: LLM judgement + deterministic rules the model cannot override\n• Concurrency-safe round-robin rep assignment, HubSpot contact + deal, Slack alert with talking points\n• Personalised first-touch email in seconds; follow-ups replied in-thread, business days only\n• Reply classifier: interested → alert rep, question → AI draft for review, not now → snooze, unsubscribe → suppress\n\n**Stack:** n8n · Claude (Anthropic) · Hunter · Apollo · SerpAPI · HubSpot · Gmail · Slack · Postgres",
        "height": 800,
        "width": 860,
        "color": 1
      },
      "id": "08810d6b-c757-5a4b-bf90-3451850b7c86",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -980,
        -250
      ]
    },
    {
      "parameters": {
        "content": "## 🛠️ Setup\n1. **Postgres** → select on all Postgres nodes → ▶ **Run Once: Create Tables** (adds 3 demo reps; put real Slack user IDs / HubSpot owner IDs in `sales_reps`).\n2. **Header Auth** credential for the intake webhook (e.g. `X-API-Key: <long random>`), send it from your form backend.\n3. **Hunter**: Query Auth credential, name `api_key`. **Apollo**: Header Auth, name `x-api-key`.\n4. **HubSpot**: private-app token (scopes: crm.objects.contacts.write, crm.objects.deals.write).\n5. **Anthropic**, **SerpAPI**, **Gmail OAuth2**, **Slack** credentials.\n6. Edit **Lead Engine Config** (product pitch, ICP, thresholds, booking link, cadences).\n\n**Test it:**\n`curl -X POST https://your-n8n/webhook/lead-intake -H \"X-API-Key: ...\" -H \"Content-Type: application/json\" -d '{\"email\":\"dana@acme-saas.com\",\"name\":\"Dana Kim\",\"company\":\"Acme SaaS\",\"jobTitle\":\"VP Revenue Operations\",\"employees\":\"201-500\",\"timeline\":\"this quarter\",\"message\":\"Looking for a demo, our forecasts live in spreadsheets.\"}'`",
        "height": 700,
        "width": 860,
        "color": 3
      },
      "id": "c2a00d56-adeb-58c3-8a26-5baf7bd8887a",
      "name": "Setup Guide",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -980,
        600
      ]
    },
    {
      "parameters": {
        "content": "## 📥 Three triggers → one config → one router",
        "height": 2240,
        "width": 740,
        "color": 7
      },
      "id": "ed4f01b8-d7ea-5aa1-bbbf-ca42c3417777",
      "name": "Frame: Entry",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -70,
        50
      ]
    },
    {
      "parameters": {
        "content": "## ① Intake → enrichment → AI qualification (replies to the form in milliseconds, works in the background)\nBots get a fake 202 and are dropped. Every enrichment call fails soft. Claude researches the company on the web, then a second Claude pass scores it against the ICP with a strict JSON schema and writes the first email.",
        "height": 800,
        "width": 3620,
        "color": 5
      },
      "id": "ea97c34d-5abb-5c2f-a85e-cc91e7e66dfd",
      "name": "Frame: Intake",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        730,
        -250
      ]
    },
    {
      "parameters": {
        "content": "## ② Hybrid score → route → act\nHot: round-robin rep, HubSpot contact + deal, Slack alert with talking points. Warm: nurture sequence. Cold: CRM only. Disqualified: audit log.",
        "height": 620,
        "width": 2944,
        "color": 6
      },
      "id": "18a1f7a1-d660-5467-ab34-fe283606845a",
      "name": "Frame: Routing",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        730,
        570
      ]
    },
    {
      "parameters": {
        "content": "## ③ Follow-up sequencer (every 15 min, business days)\nRows are claimed with SKIP LOCKED so overlapping runs never double-send. Claude writes each follow-up fresh; it is sent as a reply in the original thread.",
        "height": 620,
        "width": 1228,
        "color": 4
      },
      "id": "cc0135cd-829d-558f-b07b-600e5f972d45",
      "name": "Frame: Sequencer",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        730,
        1190
      ]
    },
    {
      "parameters": {
        "content": "## ④ Reply intelligence\nEvery inbound reply is matched to its lead and classified by Claude. The sequence stops the moment a human answers.",
        "height": 800,
        "width": 1956,
        "color": 2
      },
      "id": "2b4476bf-b674-522c-8560-f9b7d2c0146b",
      "name": "Frame: Replies",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        730,
        1810
      ]
    },
    {
      "parameters": {
        "content": "## 🧱 Run once",
        "height": 260,
        "width": 520,
        "color": 7
      },
      "id": "40967d74-6114-5c10-9e5b-468dbe4aa01e",
      "name": "Frame: Setup",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -70,
        2630
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "lead-intake",
        "authentication": "headerAuth",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "efc74617-e0c1-50d4-86bc-a3bff5a4187b",
      "name": "Lead Intake Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        200
      ],
      "webhookId": "0efd5b97-15c2-58df-9162-c203e37f98fd"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      },
      "id": "c69e867a-5f0e-507f-807f-3ead76dc54a9",
      "name": "Every 15 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        1460
      ]
    },
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "simple": false,
        "filters": {
          "q": "in:inbox -from:me -category:promotions newer_than:1d"
        },
        "options": {}
      },
      "id": "2d0f2739-ff37-5c49-a1eb-b379e1182ac7",
      "name": "Gmail: Lead Replies",
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        2140
      ]
    },
    {
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "8ecc67cd-232b-57df-838f-21e10f5f0688",
              "name": "companyName",
              "value": "Northwind Analytics",
              "type": "string"
            },
            {
              "id": "3da396d1-8804-590e-aa13-5b572e09b936",
              "name": "productPitch",
              "value": "Northwind is a revenue analytics platform that unifies CRM, billing and product usage data so B2B SaaS teams can forecast accurately, spot churn early and stop living in spreadsheets.",
              "type": "string"
            },
            {
              "id": "ecbd653c-f858-5420-a630-66dfdd52d94d",
              "name": "icp",
              "value": "B2B SaaS companies with 50 to 2,000 employees; buyers are RevOps, Finance or Data leaders (Director and above); typical stack includes Salesforce or HubSpot plus Stripe or Chargebee; strong triggers are recent funding, a new RevOps hire or messy spreadsheet forecasting.",
              "type": "string"
            },
            {
              "id": "0b0795c9-251f-5de0-b82a-2fd63f00be33",
              "name": "bookingLink",
              "value": "https://cal.com/northwind/demo",
              "type": "string"
            },
            {
              "id": "66aee7dc-b949-5ed4-8a46-9c652d8b0610",
              "name": "senderName",
              "value": "Alex at Northwind",
              "type": "string"
            },
            {
              "id": "cdf14e48-8238-5bbd-b766-5bb7d684ee79",
              "name": "hotThreshold",
              "value": 75,
              "type": "number"
            },
            {
              "id": "09bfb370-fe13-5eb6-bf74-cf4cbe8e9ebc",
              "name": "warmThreshold",
              "value": 45,
              "type": "number"
            },
            {
              "id": "76930fce-c17e-5a14-b0bd-44a9291b88ac",
              "name": "hotCadence",
              "value": "1,3,6",
              "type": "string"
            },
            {
              "id": "bf1ad2c2-4ca6-59b6-8cb6-37821283c93a",
              "name": "warmCadence",
              "value": "3,7,14",
              "type": "string"
            },
            {
              "id": "3239112f-a65e-563d-b6fb-d500549d77d8",
              "name": "timezone",
              "value": "America/New_York",
              "type": "string"
            },
            {
              "id": "d637ba58-d2b4-59bf-a96c-7c6f3d83aad9",
              "name": "salesSlackChannelId",
              "value": "C0123456789",
              "type": "string"
            },
            {
              "id": "e1dcead0-b732-5181-a786-20fad60d1906",
              "name": "hubspotPortalId",
              "value": "12345678",
              "type": "string"
            },
            {
              "id": "77e60e49-8d5f-5bab-ba82-fe2f2f1e924e",
              "name": "hubspotPipeline",
              "value": "default",
              "type": "string"
            },
            {
              "id": "06e2d75e-8a93-50a5-9f0b-4e689b2fdb74",
              "name": "hubspotDealStage",
              "value": "qualifiedtobuy",
              "type": "string"
            },
            {
              "id": "9590504e-1095-5088-a3dc-1c1b456c54a9",
              "name": "event",
              "value": "={{ $('Lead Intake Webhook').isExecuted ? 'lead' : ($('Every 15 Minutes').isExecuted ? 'followups' : 'reply') }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "e387aab4-c640-58c3-9a64-16000aa806ee",
      "name": "Lead Engine Config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        260,
        1000
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "59a0de5f-34f9-5f51-bfdc-5704dc4b383c",
                    "leftValue": "={{ $json.event }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "lead"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "New Lead"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "79579d92-8217-55ee-8318-77a560630fc4",
                    "leftValue": "={{ $json.event }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "followups"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Follow-up Run"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "ac1d3f7d-58fd-5c86-a87a-f72fea7fc1b2",
                    "leftValue": "={{ $json.event }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "reply"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Inbound Reply"
            }
          ]
        },
        "options": {}
      },
      "id": "d3b2f6e8-6bc0-549d-85f0-ba9606b9f192",
      "name": "Route by Trigger",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        520,
        1000
      ]
    },
    {
      "parameters": {
        "jsCode": "// Validate, normalise and pre-score the raw form submission (deterministic, no API calls)\nconst cfg = $('Lead Engine Config').first().json;\nconst b = cfg.body || {};\nconst clean = (v, max = 500) => (v == null ? '' : String(v)).trim().slice(0, max);\n\nconst email = clean(b.email, 254).toLowerCase();\nconst fullName = clean(b.name, 160);\nconst firstName = clean(b.firstName || b.first_name || fullName.split(' ')[0], 80);\nconst lastName = clean(b.lastName || b.last_name || fullName.split(' ').slice(1).join(' '), 80);\nconst errors = [];\nif (!/^[^\\s@]+@[^\\s@]+\\.[a-z]{2,}$/i.test(email)) errors.push('email: invalid format');\nif (!firstName) errors.push('firstName: required');\n\nconst domain = email.split('@')[1] || '';\nconst FREE = new Set(['gmail.com', 'googlemail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'live.com', 'msn.com',\n  'icloud.com', 'me.com', 'aol.com', 'proton.me', 'protonmail.com', 'gmx.com', 'mail.com', 'yandex.com', 'zoho.com']);\nconst DISPOSABLE = new Set(['mailinator.com', 'guerrillamail.com', '10minutemail.com', 'tempmail.com', 'temp-mail.org',\n  'yopmail.com', 'trashmail.com', 'sharklasers.com', 'getnada.com', 'dispostable.com', 'maildrop.cc']);\nconst freeEmail = FREE.has(domain);\nconst disposable = DISPOSABLE.has(domain);\nconst website = clean(b.website, 200).replace(/^https?:\\/\\//i, '').replace(/\\/.*$/, '').toLowerCase();\nconst companyDomain = !freeEmail && !disposable ? domain : (website || null);\n\nconst message = clean(b.message, 3000);\nconst title = clean(b.jobTitle || b.job_title || b.title, 120);\nconst digits = clean(b.phone, 40).replace(/[^\\d+]/g, '');\nconst phone = digits.startsWith('+') ? digits : digits.length === 10 ? `+1${digits}` : digits.length === 11 && digits.startsWith('1') ? `+${digits}` : null;\n\nconst spamReasons = [];\nif (clean(b.hp || b.honeypot)) spamReasons.push('honeypot field filled');\nif ((message.match(/https?:\\/\\//g) || []).length > 2) spamReasons.push('too many links');\nif (disposable) spamReasons.push('disposable email domain');\nif (/\\b(seo services|guest post|backlinks|casino|crypto signals|loan offer)\\b/i.test(message)) spamReasons.push('spam keywords');\nif (/^(test|asdf|qwerty)[^@]*@/i.test(email)) spamReasons.push('test address');\n\nconst seniority = /\\b(ceo|cfo|coo|cro|cto|founder|co-founder|owner|president|chief)\\b/i.test(title) ? 'c_level'\n  : /\\b(vp|vice president|head of)\\b/i.test(title) ? 'vp'\n  : /\\bdirector\\b/i.test(title) ? 'director'\n  : /\\b(manager|lead)\\b/i.test(title) ? 'manager'\n  : title ? 'individual' : 'unknown';\nconst employees = clean(b.employees || b.companySize || b.company_size, 40);\nconst empMin = Number((employees.match(/\\d+/) || [0])[0]);\nconst timeline = clean(b.timeline, 60).toLowerCase();\nconst budget = clean(b.budget, 60);\nconst demoRequest = /\\b(demo|pricing|quote|trial|talk to sales|call)\\b/i.test(`${message} ${clean(b.formType || b.form_type)}`);\n\nlet ruleScore = { c_level: 12, vp: 11, director: 9, manager: 6, individual: 2, unknown: 0 }[seniority];\nruleScore += empMin >= 200 ? 10 : empMin >= 50 ? 8 : empMin >= 11 ? 4 : 0;\nruleScore += /asap|now|this month|immediately|30 days/.test(timeline) ? 8 : /quarter|90 days|3 months/.test(timeline) ? 5 : 0;\nruleScore += budget ? 4 : 0;\nruleScore += demoRequest ? 6 : 0;\nruleScore = Math.min(ruleScore, 40);\n\nreturn [{\n  json: {\n    status: errors.length ? 'invalid' : spamReasons.length ? 'spam' : 'valid',\n    errors,\n    spamReasons,\n    lead: {\n      email, firstName, lastName: lastName || null, company: clean(b.company, 120) || null,\n      domain: companyDomain, website: website || null, title: title || null, phone, message,\n      employees: employees || null, budget: budget || null, timeline: timeline || null,\n      source: clean(b.source || b.utm_source, 80) || 'website',\n      utm: { source: clean(b.utm_source, 80), medium: clean(b.utm_medium, 80), campaign: clean(b.utm_campaign, 120) },\n    },\n    signals: { freeEmail, disposable, seniority, empMin, demoRequest, ruleScore },\n  },\n}];"
      },
      "id": "9519a88c-4a59-585f-beb3-6ac701a06c9e",
      "name": "Normalize & Validate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        780,
        200
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "07849f59-ea96-511d-acde-89c1f5a001dd",
                    "leftValue": "={{ $json.status }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "valid"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Valid"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "5ef2f6a2-7813-5df4-a84f-b2ed1d9e267e",
                    "leftValue": "={{ $json.status }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "spam"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Spam"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "4c0ceaa7-4db8-52e1-bbd2-88e3bb23aa8c",
                    "leftValue": "={{ $json.status }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "invalid"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "Invalid"
            }
          ]
        },
        "options": {}
      },
      "id": "bb60c50d-d231-576a-b1d2-e31951b15262",
      "name": "Triage Submission",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1040,
        200
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ status: 'accepted', message: 'Thanks! We will be in touch shortly.' }) }}",
        "options": {
          "responseCode": 202
        }
      },
      "id": "89536e70-79ff-51e9-a3b8-e29e3fb1ad4f",
      "name": "Respond: 202 Accepted",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1300,
        80
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ status: 'accepted', message: 'Thanks! We will be in touch shortly.' }) }}",
        "options": {
          "responseCode": 202
        }
      },
      "id": "f70048ee-b898-58d3-acf5-162679656d6d",
      "name": "Respond: 202 (Silent Drop)",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1300,
        260
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ status: 'invalid', errors: $json.errors }) }}",
        "options": {
          "responseCode": 422
        }
      },
      "id": "1a29b1f0-9662-5c8e-bd42-6519d05b3e93",
      "name": "Respond: 422 Invalid",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1300,
        420
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO lead_events (lead_id, event, detail)\nVALUES (NULL, 'spam_blocked', $1::jsonb);",
        "options": {
          "queryReplacement": "={{ [ JSON.stringify({ email: $json.lead.email, reasons: $json.spamReasons }) ] }}"
        }
      },
      "id": "84a03730-ac10-5cbf-9786-cb5fe3491a5e",
      "name": "Log Spam",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1560,
        260
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO leads (email, first_name, last_name, company, domain, website, job_title, phone,\n                   message, employees, budget, timeline, source, utm, free_email)\nVALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14::jsonb, $15)\nON CONFLICT (email) DO UPDATE SET\n  first_name  = coalesce(EXCLUDED.first_name, leads.first_name),\n  last_name   = coalesce(EXCLUDED.last_name, leads.last_name),\n  company     = coalesce(EXCLUDED.company, leads.company),\n  domain      = coalesce(EXCLUDED.domain, leads.domain),\n  website     = coalesce(EXCLUDED.website, leads.website),\n  job_title   = coalesce(EXCLUDED.job_title, leads.job_title),\n  phone       = coalesce(EXCLUDED.phone, leads.phone),\n  message     = EXCLUDED.message,\n  submissions = leads.submissions + 1,\n  updated_at  = now()\nRETURNING id, submissions, status AS previous_status, owner_id,\n          (xmax = 0) AS is_new,\n          EXISTS (SELECT 1 FROM suppression_list s WHERE s.email = leads.email) AS suppressed,\n          (SELECT r.slack_user_id FROM sales_reps r WHERE r.id = leads.owner_id) AS owner_slack_id;",
        "options": {
          "queryReplacement": "={{ [ $('Normalize & Validate').first().json.lead.email, $('Normalize & Validate').first().json.lead.firstName, $('Normalize & Validate').first().json.lead.lastName, $('Normalize & Validate').first().json.lead.company, $('Normalize & Validate').first().json.lead.domain, $('Normalize & Validate').first().json.lead.website, $('Normalize & Validate').first().json.lead.title, $('Normalize & Validate').first().json.lead.phone, $('Normalize & Validate').first().json.lead.message, $('Normalize & Validate').first().json.lead.employees, $('Normalize & Validate').first().json.lead.budget, $('Normalize & Validate').first().json.lead.timeline, $('Normalize & Validate').first().json.lead.source, JSON.stringify($('Normalize & Validate').first().json.lead.utm), $('Normalize & Validate').first().json.signals.freeEmail ] }}"
        }
      },
      "id": "24080f78-9483-5ee2-ae4d-4159a4930d30",
      "name": "Upsert Lead",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1560,
        80
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "441e0969-a569-5593-bc83-0ece69421744",
              "leftValue": "={{ !$json.is_new && ['hot_assigned', 'replied_interested', 'meeting_booked', 'customer'].includes($json.previous_status) }}",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "rightValue": ""
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "515b7cbf-bedf-5cfc-999d-26a4cc7deb47",
      "name": "Already Being Worked?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1820,
        80
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Lead Engine Config').first().json.salesSlackChannelId }}"
        },
        "text": "=:repeat: *{{ $('Normalize & Validate').first().json.lead.firstName }} {{ $('Normalize & Validate').first().json.lead.lastName || '' }}* ({{ $('Normalize & Validate').first().json.lead.company || $('Normalize & Validate').first().json.lead.email }}) submitted the form again {{ $json.owner_slack_id ? '<@' + $json.owner_slack_id + '>' : '' }}\n> {{ ($('Normalize & Validate').first().json.lead.message || '(no message)').slice(0, 400) }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "f37b4722-5397-5609-bd59-4a8664501998",
      "name": "Slack: Re-engagement Ping",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        2080,
        -80
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "https://api.hunter.io/v2/email-verifier",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpQueryAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "email",
              "value": "={{ $('Normalize & Validate').first().json.lead.email }}"
            }
          ]
        },
        "options": {
          "timeout": 15000
        }
      },
      "id": "d5b879f1-e131-5980-b329-c883c6cf1c6d",
      "name": "Verify Email (Hunter)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2080,
        80
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.apollo.io/api/v1/people/match",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ email: $('Normalize & Validate').first().json.lead.email, first_name: $('Normalize & Validate').first().json.lead.firstName, last_name: $('Normalize & Validate').first().json.lead.lastName, domain: $('Normalize & Validate').first().json.lead.domain, reveal_personal_emails: false }) }}",
        "options": {
          "timeout": 15000
        }
      },
      "id": "be3f19d9-e0b6-5ff7-938f-80af87222d29",
      "name": "Enrich Person & Company (Apollo)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2340,
        80
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "f55a501d-03d4-5b5e-bae3-09a7d378973a",
              "leftValue": "={{ $('Normalize & Validate').first().json.lead.domain }}",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "rightValue": ""
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "26bd4ddd-93a3-5b79-968d-0ba939e5025f",
      "name": "Has Company Domain?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2600,
        80
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://{{ $('Normalize & Validate').first().json.lead.domain }}",
        "options": {
          "timeout": 10000,
          "allowUnauthorizedCerts": true,
          "redirect": {
            "redirect": {
              "followRedirects": true,
              "maxRedirects": 5
            }
          },
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "id": "d781a9a1-856b-5c3f-af0f-35509391aec7",
      "name": "Fetch Company Website",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2860,
        0
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Merge every enrichment source into one compact research dossier. Missing sources degrade gracefully.\nconst n = $('Normalize & Validate').first().json;\nconst up = $('Upsert Lead').first().json;\nconst safe = (name) => { try { return $(name).first().json; } catch (e) { return null; } };\n\nconst hunter = safe('Verify Email (Hunter)')?.data || null;\nconst person = safe('Enrich Person & Company (Apollo)')?.person || null;\nconst org = person?.organization || null;\nconst html = String(safe('Fetch Company Website')?.data || '');\n\nconst pageTitle = (html.match(/<title[^>]*>([^<]*)<\\/title>/i) || [])[1] || '';\nconst metaDesc = (html.match(/<meta[^>]+name=[\"']description[\"'][^>]*content=[\"']([^\"']*)/i) || [])[1] || '';\nconst siteText = html\n  .replace(/<(script|style|noscript|svg)[\\s\\S]*?<\\/\\1>/gi, ' ')\n  .replace(/<[^>]+>/g, ' ')\n  .replace(/&[a-z#0-9]+;/gi, ' ')\n  .replace(/\\s+/g, ' ')\n  .trim()\n  .slice(0, 2500);\n\nconst L = n.lead;\nconst facts = {\n  emailStatus: hunter?.status || 'unknown',\n  emailScore: hunter?.score ?? null,\n  title: person?.title || L.title,\n  seniority: person?.seniority || n.signals.seniority,\n  linkedin: person?.linkedin_url || null,\n  location: [person?.city, person?.state, person?.country].filter(Boolean).join(', ') || null,\n  company: org?.name || L.company,\n  industry: org?.industry || null,\n  employees: org?.estimated_num_employees || L.employees,\n  revenue: org?.annual_revenue_printed || null,\n  funding: [org?.latest_funding_stage, org?.total_funding_printed].filter(Boolean).join(', ') || null,\n  founded: org?.founded_year || null,\n  tech: (org?.technology_names || []).slice(0, 25),\n  companyDescription: org?.short_description || metaDesc || null,\n};\n\nconst dossier = [\n  `Lead: ${L.firstName} ${L.lastName || ''} <${L.email}> (${n.signals.freeEmail ? 'personal email' : 'work email'}, deliverability: ${facts.emailStatus})`,\n  `Title: ${facts.title || 'unknown'} | seniority: ${facts.seniority} | location: ${facts.location || 'unknown'}`,\n  `Company: ${facts.company || 'unknown'} | domain: ${L.domain || 'none'} | industry: ${facts.industry || 'unknown'}`,\n  `Size: ${facts.employees || 'unknown'} employees | revenue: ${facts.revenue || 'unknown'} | funding: ${facts.funding || 'unknown'} | founded: ${facts.founded || 'unknown'}`,\n  `Tech stack: ${facts.tech.join(', ') || 'unknown'}`,\n  `Form: timeline=${L.timeline || 'n/a'}, budget=${L.budget || 'n/a'}, source=${L.source}, demo request=${n.signals.demoRequest}`,\n  `Their message: \"${L.message || '(empty)'}\"`,\n  `Website: ${pageTitle} ${metaDesc ? '- ' + metaDesc : ''}`,\n  siteText ? `Website text excerpt: ${siteText}` : 'Website: not available',\n  up.is_new ? 'CRM: first time we see this person.' : `CRM: returning lead (${up.submissions} submissions, previous status ${up.previous_status}).`,\n].join('\\n');\n\nreturn [{ json: { leadId: up.id, dossier, facts, emailStatus: facts.emailStatus } }];"
      },
      "id": "25cac034-17e2-5fd5-ac48-795c7ad3acc1",
      "name": "Build Research Dossier",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3120,
        80
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.dossier }}",
        "options": {
          "systemMessage": "=You are a B2B sales researcher preparing a brief for {{ $('Lead Engine Config').first().json.companyName }}'s sales team.\nOur product: {{ $('Lead Engine Config').first().json.productPitch }}\n\nUse the web search tool at most twice, only for the lead's company (recent news, funding, hiring, product launches, tech stack hints). If the lead uses a personal email and no company is known, do not search: say that research was not possible.\n\nReturn 5 to 8 short bullet points of verified facts, each ending with its source (website, dossier or search). Finish with one line starting \"Likely priority:\" that connects their situation to our product. Never guess numbers.",
          "maxIterations": 5
        }
      },
      "id": "de08c9e8-9358-5878-828a-8679d0e9d885",
      "name": "AI Research Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 2,
      "position": [
        3458,
        80
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "id",
          "value": "claude-opus-5"
        },
        "options": {
          "maxTokensToSample": 1500
        }
      },
      "id": "46301ce2-29c8-5a35-8b84-b0af78ba25f5",
      "name": "Claude · Researcher",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.3,
      "position": [
        3354,
        300
      ]
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "69a46d7f-6d39-5bd5-ae20-94ad6ec58ec9",
      "name": "Web Search (SerpAPI)",
      "type": "@n8n/n8n-nodes-langchain.toolSerpApi",
      "typeVersion": 1,
      "position": [
        3562,
        300
      ]
    },
    {
      "parameters": {
        "text": "=## Our product\n{{ $('Lead Engine Config').first().json.productPitch }}\n\n## Ideal customer profile\n{{ $('Lead Engine Config').first().json.icp }}\n\n## Booking link\n{{ $('Lead Engine Config').first().json.bookingLink }}\n\n## Lead dossier\n{{ $('Build Research Dossier').first().json.dossier }}\n\n## External research\n{{ $json.output || 'No external research available.' }}",
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"fit_score\": { \"type\": \"integer\", \"description\": \"0-100 match to the ideal customer profile\" },\n    \"intent_score\": { \"type\": \"integer\", \"description\": \"0-100 buying intent\" },\n    \"persona\": { \"type\": \"string\", \"description\": \"e.g. Economic buyer, Champion, Evaluator, Researcher\" },\n    \"company_summary\": { \"type\": \"string\", \"description\": \"One or two sentences on what the company does and its size\" },\n    \"pain_points\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n    \"disqualifiers\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } },\n    \"recommended_action\": { \"type\": \"string\", \"enum\": [\"fast_track_ae\", \"nurture_sequence\", \"newsletter_only\", \"disqualify\"] },\n    \"reasoning\": { \"type\": \"string\", \"description\": \"Why these scores, in 2-3 sentences\" },\n    \"email_subject\": { \"type\": \"string\" },\n    \"email_body_html\": { \"type\": \"string\" },\n    \"ae_talking_points\": { \"type\": \"array\", \"items\": { \"type\": \"string\" } }\n  },\n  \"required\": [\"fit_score\", \"intent_score\", \"persona\", \"company_summary\", \"pain_points\", \"disqualifiers\", \"recommended_action\", \"reasoning\", \"email_subject\", \"email_body_html\", \"ae_talking_points\"]\n}",
        "options": {
          "systemPromptTemplate": "You are a senior SDR manager who qualifies inbound leads and writes the first reply.\nScore strictly against the ideal customer profile. fit_score measures company and persona match; intent_score measures buying intent from the message, timeline, budget and seniority.\nDisqualify students, job seekers, vendors pitching us, competitors and obvious non-buyers, and list the reason in disqualifiers.\nWrite email_body_html as a reply from a helpful human: 90 to 140 words, 2 or 3 <p> paragraphs, reference one specific detail from the dossier or research, no hype, no invented facts, and end with a single call to action that includes the booking link. Do not include a greeting line with the subject or a signature.\nae_talking_points are 3 to 5 crisp bullets for the account executive's first call."
        }
      },
      "id": "a266247c-86bd-5279-9a4e-0bd0d42f2912",
      "name": "AI Qualification Scorer",
      "type": "@n8n/n8n-nodes-langchain.informationExtractor",
      "typeVersion": 1,
      "position": [
        3848,
        80
      ]
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "id",
          "value": "claude-opus-5"
        },
        "options": {
          "maxTokensToSample": 3000
        }
      },
      "id": "712f2552-84af-5609-9b9a-195469f856bb",
      "name": "Claude · Scorer",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.3,
      "position": [
        3848,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Hybrid scoring: LLM judgement + deterministic rules the model cannot override\nconst cfg = $('Lead Engine Config').first().json;\nconst n = $('Normalize & Validate').first().json;\nconst d = $('Build Research Dossier').first().json;\nconst up = $('Upsert Lead').first().json;\nconst ai = $input.first().json.output || {};\n\nconst clamp = (x) => Math.max(0, Math.min(100, Math.round(Number(x) || 0)));\nconst fit = clamp(ai.fit_score);\nconst intent = clamp(ai.intent_score);\nconst rule = clamp((n.signals.ruleScore / 40) * 100);\nlet score = Math.round(0.5 * fit + 0.3 * intent + 0.2 * rule);\nconst adjustments = [];\n\nif (n.signals.freeEmail && !n.lead.company) {\n  score = Math.min(score, cfg.hotThreshold - 1);\n  adjustments.push('capped: personal email and no company');\n}\nif (['c_level', 'vp', 'director'].includes(n.signals.seniority) && n.signals.demoRequest && n.signals.empMin >= 50) {\n  score = Math.max(score, cfg.hotThreshold);\n  adjustments.push('floored: senior buyer asking for a demo at a 50+ person company');\n}\n\nlet tier = score >= cfg.hotThreshold ? 'hot' : score >= cfg.warmThreshold ? 'warm' : 'cold';\nif (ai.recommended_action === 'disqualify' && (ai.disqualifiers || []).length) {\n  tier = 'disqualified';\n  adjustments.push(`AI disqualified: ${ai.disqualifiers.join(', ')}`);\n}\nif (d.emailStatus === 'invalid') {\n  tier = 'disqualified';\n  adjustments.push('email address is undeliverable');\n}\nconst canEmail = !up.suppressed && tier !== 'disqualified';\nif (up.suppressed) adjustments.push('address is on the suppression list: no emails');\n\nreturn [{\n  json: {\n    leadId: up.id,\n    tier, score, fit, intent, rule, adjustments, canEmail,\n    persona: ai.persona || null,\n    companySummary: ai.company_summary || '',\n    painPoints: ai.pain_points || [],\n    disqualifiers: ai.disqualifiers || [],\n    reasoning: ai.reasoning || '',\n    emailSubject: ai.email_subject || `Re: your note to ${cfg.companyName}`,\n    emailBodyHtml: ai.email_body_html || '',\n    talkingPoints: ai.ae_talking_points || [],\n  },\n}];"
      },
      "id": "2bca47d6-b2af-5a9d-8037-14fc6be7c1e0",
      "name": "Apply Scoring Guardrails",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4160,
        80
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE leads SET\n  tier = $2, score = $3, fit_score = $4, intent_score = $5, rule_score = $6,\n  analysis = $7::jsonb, enrichment = $8::jsonb,\n  status = CASE $2 WHEN 'disqualified' THEN 'disqualified' WHEN 'cold' THEN 'newsletter' ELSE 'qualified' END,\n  qualified_at = now(), updated_at = now()\nWHERE id = $1\nRETURNING id, status;",
        "options": {
          "queryReplacement": "={{ [ $json.leadId, $json.tier, $json.score, $json.fit, $json.intent, $json.rule, JSON.stringify($json), JSON.stringify($('Build Research Dossier').first().json.facts) ] }}"
        }
      },
      "id": "95f174d3-061d-59ec-ad0a-38619301539b",
      "name": "Save Qualification",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        780,
        840
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "84d4af2d-4075-544e-ba75-30ded604cacf",
                    "leftValue": "={{ $('Apply Scoring Guardrails').first().json.tier }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "hot"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "🔥 Hot"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "c0f137f9-b70e-5103-a0e0-03b99dddf9f2",
                    "leftValue": "={{ $('Apply Scoring Guardrails').first().json.tier }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "warm"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "🌤 Warm"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "945779ee-d4d3-5dbc-aba2-54df4f95cf5c",
                    "leftValue": "={{ $('Apply Scoring Guardrails').first().json.tier }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "cold"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "❄️ Cold"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "loose",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "e6c00188-74a6-5c77-aa2d-7e33a74ffc8f",
                    "leftValue": "={{ $('Apply Scoring Guardrails').first().json.tier }}",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "rightValue": "disqualified"
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "⛔ Disqualified"
            }
          ]
        },
        "options": {}
      },
      "id": "69a5a0e9-c24e-5c2e-a331-f1bc70783cbb",
      "name": "Route by Tier",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        1040,
        840
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "-- Fair round-robin that is safe under concurrent webhooks (row lock + SKIP LOCKED)\nUPDATE sales_reps\nSET last_assigned_at = now(), assigned_count = assigned_count + 1\nWHERE id = (\n  SELECT id FROM sales_reps\n  WHERE active\n  ORDER BY last_assigned_at NULLS FIRST, id\n  LIMIT 1\n  FOR UPDATE SKIP LOCKED\n)\nRETURNING id AS owner_id, name AS owner_name, email AS owner_email, slack_user_id, hubspot_owner_id;",
        "options": {}
      },
      "id": "b873940d-00b3-50e0-bc41-cecb1c426d71",
      "name": "Assign Rep (Round-Robin)",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1352,
        740
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "mode": "manual",
        "assignments": {
          "assignments": [
            {
              "id": "aa9bd25e-29e9-5df6-9cfe-18e471d7458d",
              "name": "owner_name",
              "value": "={{ $('Lead Engine Config').first().json.senderName }}",
              "type": "string"
            },
            {
              "id": "d60caa7a-1363-5712-867e-61932767aa44",
              "name": "slack_user_id",
              "value": "",
              "type": "string"
            },
            {
              "id": "0843ee12-4e7b-5e34-93c5-76c9102e3209",
              "name": "hubspot_owner_id",
              "value": "",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": false,
        "options": {}
      },
      "id": "c38420c0-947b-518e-8e58-ad2611a26b53",
      "name": "Assign Marketing Owner",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1352,
        900
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO lead_events (lead_id, event, detail)\nVALUES ($1, 'disqualified', $2::jsonb);",
        "options": {
          "queryReplacement": "={{ [ $('Apply Scoring Guardrails').first().json.leadId, JSON.stringify({ adjustments: $('Apply Scoring Guardrails').first().json.adjustments, disqualifiers: $('Apply Scoring Guardrails').first().json.disqualifiers, reasoning: $('Apply Scoring Guardrails').first().json.reasoning }) ] }}"
        }
      },
      "id": "8d5b280c-e560-5ecc-9ac5-54615f7e8850",
      "name": "Log Disqualification",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1352,
        1060
      ]
    },
    {
      "parameters": {
        "jsCode": "// Build CRM + email payloads for whichever owner this tier was routed to\nconst cfg = $('Lead Engine Config').first().json;\nconst L = $('Normalize & Validate').first().json.lead;\nconst T = $('Apply Scoring Guardrails').first().json;\nconst owner = $input.first().json;\nconst hot = T.tier === 'hot';\n\nconst props = {\n  email: L.email, firstname: L.firstName, lastname: L.lastName, company: L.company, jobtitle: L.title,\n  phone: L.phone, website: L.website,\n  lifecyclestage: hot ? 'salesqualifiedlead' : T.tier === 'warm' ? 'marketingqualifiedlead' : 'lead',\n  hs_lead_status: 'NEW',\n  hubspot_owner_id: owner.hubspot_owner_id,\n};\nconst properties = Object.fromEntries(Object.entries(props).filter(([, v]) => v != null && v !== ''));\n\nconst signature = `<p>${owner.owner_name || cfg.senderName}<br>${cfg.companyName}</p>`;\nconst optOut = '<p style=\"color:#888;font-size:12px\">Not relevant? Just reply \"unsubscribe\" and I will not email again.</p>';\n\nreturn [{\n  json: {\n    tier: T.tier,\n    sendEmail: T.canEmail && T.tier !== 'cold',\n    email: L.email,\n    senderName: owner.owner_name || cfg.senderName,\n    ownerId: owner.owner_id ?? null,\n    ownerName: owner.owner_name || null,\n    ownerSlackId: owner.slack_user_id || null,\n    subject: T.emailSubject,\n    emailHtml: `${T.emailBodyHtml}${signature}${optOut}`,\n    status: hot ? 'hot_assigned' : 'nurturing',\n    cadenceDays: hot ? cfg.hotCadence.split(',').map(Number) : cfg.warmCadence.split(',').map(Number),\n    hubspotContact: { inputs: [{ idProperty: 'email', id: L.email, properties }] },\n    hubspotDeal: {\n      properties: Object.fromEntries(Object.entries({\n        dealname: `${L.company || L.domain || L.email} - inbound`,\n        pipeline: cfg.hubspotPipeline,\n        dealstage: cfg.hubspotDealStage,\n        hubspot_owner_id: owner.hubspot_owner_id,\n        description: `AI score ${T.score}/100. ${T.companySummary}`.slice(0, 1000),\n      }).filter(([, v]) => v != null && v !== '')),\n    },\n  },\n}];"
      },
      "id": "2b232530-0d49-5326-9947-6faab3a1db05",
      "name": "Prepare Outreach",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1664,
        819
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/contacts/batch/upsert",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.hubspotContact) }}",
        "options": {
          "timeout": 15000
        }
      },
      "id": "dead583f-7ab7-5759-bd38-e4f8b6cd5af2",
      "name": "HubSpot: Upsert Contact",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1924,
        819
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "c6ccb1dd-6622-538b-b7a6-8df2593813e3",
              "leftValue": "={{ $('Prepare Outreach').first().json.sendEmail }}",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "rightValue": ""
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "ba54956a-ad3c-582a-b232-feeea87ad7f3",
      "name": "Send Email?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2184,
        819
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "send",
        "sendTo": "={{ $('Prepare Outreach').first().json.email }}",
        "subject": "={{ $('Prepare Outreach').first().json.subject }}",
        "emailType": "html",
        "message": "={{ $('Prepare Outreach').first().json.emailHtml }}",
        "options": {
          "appendAttribution": false,
          "senderName": "={{ $('Prepare Outreach').first().json.senderName }}"
        }
      },
      "id": "c84bf386-5b14-5ce2-b98d-c27a885957f5",
      "name": "Gmail: First-Touch Email",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        2444,
        740
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH lead AS (\n  UPDATE leads\n  SET status = $2, owner_id = $3, gmail_thread_id = $4, gmail_message_id = $5,\n      hubspot_contact_id = $6, first_touch_at = now(), last_touch_at = now(), updated_at = now()\n  WHERE id = $1\n  RETURNING id\n)\nINSERT INTO sequence_steps (lead_id, step_number, send_at)\nSELECT lead.id, s.n, now() + make_interval(days => s.days)\nFROM lead, unnest($7::int[]) WITH ORDINALITY AS s(days, n)\nRETURNING step_number, send_at;",
        "options": {
          "queryReplacement": "={{ [ $('Apply Scoring Guardrails').first().json.leadId, $('Prepare Outreach').first().json.status, $('Prepare Outreach').first().json.ownerId, $json.threadId, $json.id, $('HubSpot: Upsert Contact').first().json.results?.[0]?.id ?? null, $('Prepare Outreach').first().json.cadenceDays ] }}"
        }
      },
      "id": "05cf9e90-3420-539d-9f06-19c645d2919d",
      "name": "Enroll in Sequence",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        2704,
        740
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "f0c86183-7134-5a62-8148-1f97c28b6dab",
              "leftValue": "={{ $('Prepare Outreach').first().json.tier }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "rightValue": "hot"
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "65e45eda-c487-5eaf-b013-a7019a9c9b43",
      "name": "Hot Lead?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2964,
        819
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/deals",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "hubspotAppToken",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ ...$('Prepare Outreach').first().json.hubspotDeal, associations: $('HubSpot: Upsert Contact').first().json.results?.[0]?.id ? [{ to: { id: $('HubSpot: Upsert Contact').first().json.results?.[0]?.id }, types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: 3 }] }] : [] }) }}",
        "options": {
          "timeout": 15000
        }
      },
      "id": "bc86a25c-8925-5221-b273-358a4f5f11e2",
      "name": "HubSpot: Create Deal",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3224,
        819
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Lead Engine Config').first().json.salesSlackChannelId }}"
        },
        "text": "=:fire: *Hot inbound lead*, score {{ $('Apply Scoring Guardrails').first().json.score }}/100 for {{ $('Prepare Outreach').first().json.ownerSlackId ? '<@' + $('Prepare Outreach').first().json.ownerSlackId + '>' : ($('Prepare Outreach').first().json.ownerName || 'the team') }}\n*{{ $('Normalize & Validate').first().json.lead.firstName }} {{ $('Normalize & Validate').first().json.lead.lastName || '' }}*, {{ $('Normalize & Validate').first().json.lead.title || 'title unknown' }} at *{{ $('Normalize & Validate').first().json.lead.company || $('Normalize & Validate').first().json.lead.domain }}*\n{{ $('Apply Scoring Guardrails').first().json.companySummary }}\n*Why hot:* fit {{ $('Apply Scoring Guardrails').first().json.fit }}, intent {{ $('Apply Scoring Guardrails').first().json.intent }}, rules {{ $('Apply Scoring Guardrails').first().json.rule }}{{ $('Apply Scoring Guardrails').first().json.adjustments.length ? ' (' + $('Apply Scoring Guardrails').first().json.adjustments.join('; ') + ')' : '' }}\n*Talking points:*\n{{ $('Apply Scoring Guardrails').first().json.talkingPoints.map(p => '• ' + p).join('\\n') }}\n*They wrote:* \"{{ ($('Normalize & Validate').first().json.lead.message || '').slice(0, 300) }}\"\n{{ $('Prepare Outreach').first().json.sendEmail ? ':envelope: First-touch email already sent.' : ':warning: Email suppressed, call instead.' }} <https://app.hubspot.com/contacts/{{ $('Lead Engine Config').first().json.hubspotPortalId }}/record/0-3/{{ $json.id }}|Open deal in HubSpot>",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "0235ec1d-fc60-5383-bbf9-f5bbfe078d9a",
      "name": "Slack: Hot Lead Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        3484,
        819
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "-- Claim due follow-ups atomically so overlapping runs never double-send\nWITH due AS (\n  SELECT s.id\n  FROM sequence_steps s\n  JOIN leads l ON l.id = s.lead_id\n  WHERE s.status = 'scheduled'\n    AND s.send_at <= now()\n    AND l.status IN ('hot_assigned', 'nurturing')\n    AND extract(isodow FROM now() AT TIME ZONE $1) < 6          -- business days only\n    AND NOT EXISTS (SELECT 1 FROM suppression_list x WHERE x.email = l.email)\n  ORDER BY s.send_at\n  LIMIT 25\n  FOR UPDATE OF s SKIP LOCKED\n)\nUPDATE sequence_steps s\nSET status = 'sending', updated_at = now()\nFROM due, leads l\nWHERE s.id = due.id AND l.id = s.lead_id\nRETURNING s.id AS step_id, s.step_number, l.id AS lead_id, l.email, l.first_name, l.company,\n          l.job_title, l.tier, l.gmail_message_id, l.analysis,\n          (SELECT count(*) FROM sequence_steps x WHERE x.lead_id = l.id)::int AS total_steps;",
        "options": {
          "queryReplacement": "={{ [ $('Lead Engine Config').first().json.timezone ] }}"
        }
      },
      "id": "655a14b1-97ee-5084-8a31-4b44c8076055",
      "name": "Claim Due Follow-ups",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        780,
        1460
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Write follow-up step {{ $json.step_number }} of {{ $json.total_steps }} to {{ $json.first_name }} ({{ $json.job_title || 'unknown title' }} at {{ $json.company || 'their company' }}).\nPain points we identified: {{ ($json.analysis?.painPoints || []).join('; ') || 'unknown' }}\nCompany: {{ $json.analysis?.companySummary || 'unknown' }}\nOur product: {{ $('Lead Engine Config').first().json.productPitch }}\nBooking link: {{ $('Lead Engine Config').first().json.bookingLink }}",
        "messages": {
          "messageValues": [
            {
              "message": "You write short follow-up emails in an existing thread for a B2B SaaS company. Each follow-up must add something new instead of \"just checking in\":\n- step 1: a concrete insight or tip tied to one of the lead's pain points\n- step 2: a brief, believable customer outcome for a similar company (no invented metrics, keep it qualitative)\n- step 3 or later: a polite break-up email that makes it easy to say \"not now\"\nRules: 50 to 90 words, plain and human, 1 to 2 <p> paragraphs of HTML, one soft call to action, no subject line, no signature. Output only the HTML."
            }
          ]
        }
      },
      "id": "fdf57147-855d-5cae-9989-f2e21ac48476",
      "name": "AI Follow-up Writer",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.5,
      "position": [
        1118,
        1460
      ]
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "id",
          "value": "claude-opus-5"
        },
        "options": {
          "maxTokensToSample": 800
        }
      },
      "id": "542fee61-1502-59b0-930d-6aef40fbcd21",
      "name": "Claude · Follow-ups",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.3,
      "position": [
        1118,
        1680
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "reply",
        "messageId": "={{ $('Claim Due Follow-ups').item.json.gmail_message_id }}",
        "emailType": "html",
        "message": "={{ $json.text }}",
        "options": {
          "appendAttribution": false
        }
      },
      "id": "9cd25759-9d67-57fe-bcfd-427a6e6d2ccb",
      "name": "Gmail: Reply in Thread",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1456,
        1460
      ],
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE sequence_steps SET status = 'sent', sent_at = now(), body = $2, updated_at = now()\nWHERE id = $1;\nUPDATE leads\nSET last_touch_at = now(),\n    status = CASE WHEN $3 >= $4 THEN 'sequence_completed' ELSE status END\nWHERE id = $5;",
        "options": {
          "queryReplacement": "={{ [ $('Claim Due Follow-ups').item.json.step_id, $('AI Follow-up Writer').item.json.text, $('Claim Due Follow-ups').item.json.step_number, $('Claim Due Follow-ups').item.json.total_steps, $('Claim Due Follow-ups').item.json.lead_id ] }}"
        }
      },
      "id": "d789dc98-ad74-51f0-93ba-7e02ed34b784",
      "name": "Mark Step Sent",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1768,
        1380
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE sequence_steps SET status = 'failed', error = $2, updated_at = now()\nWHERE id = $1;",
        "options": {
          "queryReplacement": "={{ [ $('Claim Due Follow-ups').item.json.step_id, $json.error?.message || JSON.stringify($json.error || 'send failed') ] }}"
        }
      },
      "id": "da925998-8841-53a1-8f24-11a89df152a8",
      "name": "Mark Step Failed",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1768,
        1540
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT l.id, l.email, l.first_name, l.company, l.status, l.tier, l.analysis,\n       r.name AS owner_name, r.slack_user_id AS owner_slack_id\nFROM leads l\nLEFT JOIN sales_reps r ON r.id = l.owner_id\nWHERE l.gmail_thread_id = $1 OR l.email = lower($2)\nORDER BY (l.gmail_thread_id = $1) DESC NULLS LAST\nLIMIT 1;",
        "options": {
          "queryReplacement": "={{ [ $json.threadId, $json.from?.value?.[0]?.address || '' ] }}"
        }
      },
      "id": "d5c787d7-16ed-5ffe-b7b1-de7c1e10dbbd",
      "name": "Match Reply to Lead",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        780,
        2140
      ]
    },
    {
      "parameters": {
        "inputText": "={{ String($('Gmail: Lead Replies').item.json.text || '').split(/\\r?\\nOn .{0,200}wrote:/)[0].slice(0, 2000) }}",
        "categories": {
          "categories": [
            {
              "category": "Interested",
              "description": "Wants to talk, book a call or demo, asks for times, or says yes."
            },
            {
              "category": "Question",
              "description": "Asks about pricing, features, integrations, security or fit before committing."
            },
            {
              "category": "Not Now",
              "description": "Timing is wrong, out-of-office or auto-reply, asks to reconnect later, or is busy."
            },
            {
              "category": "Unsubscribe",
              "description": "Asks to stop emailing, remove them, is not interested at all, or is hostile."
            }
          ]
        },
        "options": {}
      },
      "id": "89593a96-5db5-5e26-b233-5722b886e008",
      "name": "Classify Reply Intent",
      "type": "@n8n/n8n-nodes-langchain.textClassifier",
      "typeVersion": 1,
      "position": [
        1118,
        2140
      ]
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "id",
          "value": "claude-opus-5"
        },
        "options": {
          "maxTokensToSample": 300
        }
      },
      "id": "e8923897-e5e0-537c-9308-2ab96d7a543c",
      "name": "Claude · Classifier",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.3,
      "position": [
        1118,
        2360
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH l AS (\n  UPDATE leads SET status = 'replied_interested', last_reply_at = now(), last_reply_intent = 'interested', updated_at = now()\n  WHERE id = $1 RETURNING id\n), stop AS (\n  UPDATE sequence_steps SET status = 'cancelled', updated_at = now()\n  WHERE lead_id = $1 AND status = 'scheduled'\n)\nINSERT INTO lead_events (lead_id, event, detail)\nSELECT id, 'reply_interested', $2::jsonb FROM l\nRETURNING 'Interested' AS category;",
        "options": {
          "queryReplacement": "={{ [ $('Match Reply to Lead').item.json.id, JSON.stringify({ snippet: String($('Gmail: Lead Replies').item.json.text || '').split(/\\r?\\nOn .{0,200}wrote:/)[0].slice(0, 2000).slice(0, 500), subject: $('Gmail: Lead Replies').item.json.subject }) ] }}"
        }
      },
      "id": "918d2180-2c80-5f55-bcdd-221989c0c15d",
      "name": "Mark Interested",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1508,
        1980
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=Prospect reply to our email:\n\"\"\"{{ String($('Gmail: Lead Replies').item.json.text || '').split(/\\r?\\nOn .{0,200}wrote:/)[0].slice(0, 2000) }}\"\"\"\nLead: {{ $json.first_name }} at {{ $json.company || 'unknown company' }}. Company summary: {{ $json.analysis?.companySummary || 'unknown' }}\nOur product: {{ $('Lead Engine Config').first().json.productPitch }}\nBooking link: {{ $('Lead Engine Config').first().json.bookingLink }}",
        "messages": {
          "messageValues": [
            {
              "message": "You help a sales rep answer a prospect's question by email. Draft a clear, friendly answer in HTML <p> paragraphs (under 150 words) using only the product description and lead context provided. If the answer needs facts you do not have (exact pricing, security certifications, roadmap), say the rep will confirm, and suggest a short call with the booking link. No signature."
            }
          ]
        }
      },
      "id": "93d8708a-6879-5eff-8f85-993fe85779a7",
      "name": "AI Draft Answer",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.5,
      "position": [
        1508,
        2140
      ]
    },
    {
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "id",
          "value": "claude-opus-5"
        },
        "options": {
          "maxTokensToSample": 800
        }
      },
      "id": "5bb29f00-68e4-5523-97b8-cfdc16832744",
      "name": "Claude · Drafts",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "typeVersion": 1.3,
      "position": [
        1508,
        2360
      ]
    },
    {
      "parameters": {
        "resource": "draft",
        "operation": "create",
        "subject": "=Re: {{ $('Gmail: Lead Replies').item.json.subject }}",
        "emailType": "html",
        "message": "={{ $json.text }}",
        "options": {
          "threadId": "={{ $('Gmail: Lead Replies').item.json.threadId }}",
          "sendTo": "={{ $('Match Reply to Lead').item.json.email }}"
        }
      },
      "id": "55a018de-f87f-559c-8f6e-4eb4464333fe",
      "name": "Gmail: Draft Reply for Rep",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1872,
        2140
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH l AS (\n  UPDATE leads SET last_reply_at = now(), last_reply_intent = 'question', updated_at = now()\n  WHERE id = $1 RETURNING id\n), hold AS (\n  UPDATE sequence_steps SET send_at = send_at + interval '3 days', updated_at = now()\n  WHERE lead_id = $1 AND status = 'scheduled'\n)\nINSERT INTO lead_events (lead_id, event, detail)\nSELECT id, 'reply_question', $2::jsonb FROM l\nRETURNING 'Question' AS category;",
        "options": {
          "queryReplacement": "={{ [ $('Match Reply to Lead').item.json.id, JSON.stringify({ snippet: String($('Gmail: Lead Replies').item.json.text || '').split(/\\r?\\nOn .{0,200}wrote:/)[0].slice(0, 2000).slice(0, 500), subject: $('Gmail: Lead Replies').item.json.subject }) ] }}"
        }
      },
      "id": "971aeacf-15ac-552d-ac31-f0c706de0529",
      "name": "Pause Sequence",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        2184,
        2140
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH l AS (\n  UPDATE leads SET last_reply_at = now(), last_reply_intent = 'not_now', updated_at = now()\n  WHERE id = $1 RETURNING id\n), snooze AS (\n  UPDATE sequence_steps SET send_at = send_at + interval '30 days', updated_at = now()\n  WHERE lead_id = $1 AND status = 'scheduled'\n)\nINSERT INTO lead_events (lead_id, event, detail)\nSELECT id, 'reply_not_now', $2::jsonb FROM l;",
        "options": {
          "queryReplacement": "={{ [ $('Match Reply to Lead').item.json.id, JSON.stringify({ snippet: String($('Gmail: Lead Replies').item.json.text || '').split(/\\r?\\nOn .{0,200}wrote:/)[0].slice(0, 2000).slice(0, 500), subject: $('Gmail: Lead Replies').item.json.subject }) ] }}"
        }
      },
      "id": "bea9388d-7a6f-531e-ae67-5dd9ce89bb4f",
      "name": "Snooze 30 Days",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1872,
        2320
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "-- Honour opt-outs immediately (CAN-SPAM / GDPR): stop the sequence and suppress the address\nWITH l AS (\n  UPDATE leads SET status = 'unsubscribed', last_reply_at = now(), last_reply_intent = 'unsubscribe', updated_at = now()\n  WHERE id = $1 RETURNING id, email\n), stop AS (\n  UPDATE sequence_steps SET status = 'cancelled', updated_at = now()\n  WHERE lead_id = $1 AND status = 'scheduled'\n), sup AS (\n  INSERT INTO suppression_list (email, reason) SELECT email, 'replied_unsubscribe' FROM l\n  ON CONFLICT (email) DO NOTHING\n)\nINSERT INTO lead_events (lead_id, event, detail)\nSELECT id, 'unsubscribed', $2::jsonb FROM l;",
        "options": {
          "queryReplacement": "={{ [ $('Match Reply to Lead').item.json.id, JSON.stringify({ snippet: String($('Gmail: Lead Replies').item.json.text || '').split(/\\r?\\nOn .{0,200}wrote:/)[0].slice(0, 2000).slice(0, 500), subject: $('Gmail: Lead Replies').item.json.subject }) ] }}"
        }
      },
      "id": "1e653bcb-1d89-57c0-9bc6-18eeaa46f6c3",
      "name": "Unsubscribe & Suppress",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1872,
        2480
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Lead Engine Config').first().json.salesSlackChannelId }}"
        },
        "text": "=:incoming_envelope: *{{ $('Match Reply to Lead').item.json.first_name }} from {{ $('Match Reply to Lead').item.json.company || 'unknown company' }} replied*: {{ $json.category }} {{ $('Match Reply to Lead').item.json.owner_slack_id ? '<@' + $('Match Reply to Lead').item.json.owner_slack_id + '>' : '' }}\n> {{ String($('Gmail: Lead Replies').item.json.text || '').split(/\\r?\\nOn .{0,200}wrote:/)[0].slice(0, 2000).slice(0, 400) }}\n{{ $json.category === 'Question' ? ':memo: An AI-drafted answer is waiting in Gmail drafts for your review.' : ':calendar: Sequence stopped. Reply personally while they are warm.' }}",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "id": "665b26e9-d76c-5e6b-9590-bb6bc99237bb",
      "name": "Slack: Reply Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        2496,
        2060
      ]
    },
    {
      "parameters": {},
      "id": "80d0c78e-3935-5c77-9ffa-a428c0bb9f84",
      "name": "Run Once: Create Tables",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        2760
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "-- Lead engine schema + demo sales reps. Safe to re-run.\nCREATE TABLE IF NOT EXISTS sales_reps (\n  id               bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  name             text NOT NULL,\n  email            text UNIQUE NOT NULL,\n  slack_user_id    text,\n  hubspot_owner_id text,\n  active           boolean NOT NULL DEFAULT true,\n  assigned_count   int NOT NULL DEFAULT 0,\n  last_assigned_at timestamptz\n);\n\nCREATE TABLE IF NOT EXISTS leads (\n  id                 bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  email              text UNIQUE NOT NULL,\n  first_name         text,\n  last_name          text,\n  company            text,\n  domain             text,\n  website            text,\n  job_title          text,\n  phone              text,\n  message            text,\n  employees          text,\n  budget             text,\n  timeline           text,\n  source             text,\n  utm                jsonb,\n  free_email         boolean,\n  submissions        int NOT NULL DEFAULT 1,\n  status             text NOT NULL DEFAULT 'new',\n  tier               text,\n  score              int,\n  fit_score          int,\n  intent_score       int,\n  rule_score         int,\n  analysis           jsonb,\n  enrichment         jsonb,\n  owner_id           bigint REFERENCES sales_reps(id),\n  hubspot_contact_id text,\n  gmail_thread_id    text,\n  gmail_message_id   text,\n  first_touch_at     timestamptz,\n  last_touch_at      timestamptz,\n  last_reply_at      timestamptz,\n  last_reply_intent  text,\n  qualified_at       timestamptz,\n  created_at         timestamptz NOT NULL DEFAULT now(),\n  updated_at         timestamptz NOT NULL DEFAULT now()\n);\nCREATE INDEX IF NOT EXISTS leads_domain_idx ON leads (domain);\nCREATE INDEX IF NOT EXISTS leads_thread_idx ON leads (gmail_thread_id);\n\nCREATE TABLE IF NOT EXISTS sequence_steps (\n  id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  lead_id     bigint NOT NULL REFERENCES leads(id) ON DELETE CASCADE,\n  step_number int NOT NULL,\n  send_at     timestamptz NOT NULL,\n  status      text NOT NULL DEFAULT 'scheduled',  -- scheduled | sending | sent | failed | cancelled\n  body        text,\n  error       text,\n  sent_at     timestamptz,\n  updated_at  timestamptz NOT NULL DEFAULT now()\n);\nCREATE INDEX IF NOT EXISTS sequence_due_idx ON sequence_steps (status, send_at);\n\nCREATE TABLE IF NOT EXISTS suppression_list (\n  email      text PRIMARY KEY,\n  reason     text,\n  created_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE TABLE IF NOT EXISTS lead_events (\n  id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\n  lead_id    bigint REFERENCES leads(id) ON DELETE CASCADE,\n  event      text NOT NULL,\n  detail     jsonb,\n  created_at timestamptz NOT NULL DEFAULT now()\n);\n\nINSERT INTO sales_reps (name, email, slack_user_id, hubspot_owner_id) VALUES\n  ('Priya Shah',   'priya@example.com',  'U01AAAAAAA', NULL),\n  ('Marcus Reed',  'marcus@example.com', 'U01BBBBBBB', NULL),\n  ('Elena Novak',  'elena@example.com',  'U01CCCCCCC', NULL)\nON CONFLICT (email) DO NOTHING;\n",
        "options": {}
      },
      "id": "40e26b35-7398-55e6-9a03-c0a5a6fbd4ba",
      "name": "Create Schema & Demo Reps",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        260,
        2760
      ]
    }
  ],
  "pinData": {},
  "connections": {
    "Lead Intake Webhook": {
      "main": [
        [
          {
            "node": "Lead Engine Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every 15 Minutes": {
      "main": [
        [
          {
            "node": "Lead Engine Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail: Lead Replies": {
      "main": [
        [
          {
            "node": "Lead Engine Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lead Engine Config": {
      "main": [
        [
          {
            "node": "Route by Trigger",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude · Researcher": {
      "ai_languageModel": [
        [
          {
            "node": "AI Research Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Web Search (SerpAPI)": {
      "ai_tool": [
        [
          {
            "node": "AI Research Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Claude · Scorer": {
      "ai_languageModel": [
        [
          {
            "node": "AI Qualification Scorer",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Route by Trigger": {
      "main": [
        [
          {
            "node": "Normalize & Validate",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Claim Due Follow-ups",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Match Reply to Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize & Validate": {
      "main": [
        [
          {
            "node": "Triage Submission",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Triage Submission": {
      "main": [
        [
          {
            "node": "Respond: 202 Accepted",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond: 202 (Silent Drop)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond: 422 Invalid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respond: 202 (Silent Drop)": {
      "main": [
        [
          {
            "node": "Log Spam",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respond: 202 Accepted": {
      "main": [
        [
          {
            "node": "Upsert Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert Lead": {
      "main": [
        [
          {
            "node": "Already Being Worked?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Already Being Worked?": {
      "main": [
        [
          {
            "node": "Slack: Re-engagement Ping",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Verify Email (Hunter)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Email (Hunter)": {
      "main": [
        [
          {
            "node": "Enrich Person & Company (Apollo)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Enrich Person & Company (Apollo)": {
      "main": [
        [
          {
            "node": "Has Company Domain?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Company Domain?": {
      "main": [
        [
          {
            "node": "Fetch Company Website",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Research Dossier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Company Website": {
      "main": [
        [
          {
            "node": "Build Research Dossier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Research Dossier": {
      "main": [
        [
          {
            "node": "AI Research Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Research Agent": {
      "main": [
        [
          {
            "node": "AI Qualification Scorer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Qualification Scorer": {
      "main": [
        [
          {
            "node": "Apply Scoring Guardrails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apply Scoring Guardrails": {
      "main": [
        [
          {
            "node": "Save Qualification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Qualification": {
      "main": [
        [
          {
            "node": "Route by Tier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Tier": {
      "main": [
        [
          {
            "node": "Assign Rep (Round-Robin)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Assign Marketing Owner",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Assign Marketing Owner",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Disqualification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Assign Rep (Round-Robin)": {
      "main": [
        [
          {
            "node": "Prepare Outreach",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Assign Marketing Owner": {
      "main": [
        [
          {
            "node": "Prepare Outreach",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Outreach": {
      "main": [
        [
          {
            "node": "HubSpot: Upsert Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HubSpot: Upsert Contact": {
      "main": [
        [
          {
            "node": "Send Email?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Email?": {
      "main": [
        [
          {
            "node": "Gmail: First-Touch Email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Hot Lead?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail: First-Touch Email": {
      "main": [
        [
          {
            "node": "Enroll in Sequence",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Enroll in Sequence": {
      "main": [
        [
          {
            "node": "Hot Lead?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hot Lead?": {
      "main": [
        [
          {
            "node": "HubSpot: Create Deal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HubSpot: Create Deal": {
      "main": [
        [
          {
            "node": "Slack: Hot Lead Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude · Follow-ups": {
      "ai_languageModel": [
        [
          {
            "node": "AI Follow-up Writer",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Claim Due Follow-ups": {
      "main": [
        [
          {
            "node": "AI Follow-up Writer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Follow-up Writer": {
      "main": [
        [
          {
            "node": "Gmail: Reply in Thread",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail: Reply in Thread": {
      "main": [
        [
          {
            "node": "Mark Step Sent",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark Step Failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude · Classifier": {
      "ai_languageModel": [
        [
          {
            "node": "Classify Reply Intent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Claude · Drafts": {
      "ai_languageModel": [
        [
          {
            "node": "AI Draft Answer",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Match Reply to Lead": {
      "main": [
        [
          {
            "node": "Classify Reply Intent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Reply Intent": {
      "main": [
        [
          {
            "node": "Mark Interested",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "AI Draft Answer",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Snooze 30 Days",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Unsubscribe & Suppress",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Draft Answer": {
      "main": [
        [
          {
            "node": "Gmail: Draft Reply for Rep",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gmail: Draft Reply for Rep": {
      "main": [
        [
          {
            "node": "Pause Sequence",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pause Sequence": {
      "main": [
        [
          {
            "node": "Slack: Reply Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Interested": {
      "main": [
        [
          {
            "node": "Slack: Reply Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Once: Create Tables": {
      "main": [
        [
          {
            "node": "Create Schema & Demo Reps",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all"
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}