{
  "nodes": [
    {
      "parameters": {
        "content": "## Where the scoring lives\n\n- Every point value sits at the top of the Score the lead node as plain numbers. Change them there and the rest of the flow stays put.\n- 70 and up is hot, 40 to 69 is warm, under 40 is cold. The rep rotation only turns when a lead comes out hot.",
        "height": 220,
        "width": 700,
        "color": 4
      },
      "id": "sticky-scoring",
      "name": "Where the scoring lives",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        140,
        -340
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "demo/score-lead",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "wf-webhook",
      "name": "A new lead comes in from the website",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// ------------------------------------------------------------\n// THE SCORING RULES LIVE HERE. Change a number, save, done.\n// Points add up. 70 and up is hot, 40 to 69 is warm, under 40 is cold.\nconst SERVICE_POINTS = { install: 30, replacement: 30, repair: 18, question: 5 };\nconst TIMELINE_POINTS = { thisWeek: 25, thisMonth: 15 };\nconst BUDGET_BANDS = [\n  { atLeast: 10000, points: 20 },\n  { atLeast: 5000, points: 15 },\n  { atLeast: 1000, points: 8 }\n];\nconst SOURCE_POINTS = { referral: 15, facebook: 8, website: 5 };\nconst HAS_PHONE_POINTS = 5;\nconst HAS_EMAIL_POINTS = 5;\nconst HOT_AT = 70;\nconst WARM_AT = 40;\n// ------------------------------------------------------------\n\n// The form can post its fields at the top level or wrapped under data. Read both.\nconst raw = $input.first().json;\nconst body = raw.body || raw;\nconst fields = body.data || body;\n\nconst pick = (...keys) => {\n  for (const k of keys) {\n    const v = fields[k] ?? body[k];\n    if (v !== undefined && v !== null && String(v).trim() !== '') return String(v).trim();\n  }\n  return '';\n};\n\nconst fullName = pick('name', 'full_name', 'fullName') || 'New lead';\nlet phone = pick('phone', 'phone_number', 'mobile').replace(/[^0-9]/g, '');\nif (phone.length === 10) phone = '1' + phone;\nif (phone) phone = '+' + phone;\nconst email = pick('email', 'email_address').toLowerCase();\nconst service = pick('service', 'service_type', 'job_type');\nconst budget = pick('budget', 'budget_range');\nconst timeline = pick('timeline', 'when', 'timeframe');\nconst message = pick('message', 'notes', 'comments');\nconst source = pick('source', 'utm_source', 'lead_source') || 'website';\n\nlet score = 0;\nconst notes = [];\n\nconst serviceKey = Object.keys(SERVICE_POINTS).find(k => service.toLowerCase().includes(k));\nif (serviceKey) {\n  score += SERVICE_POINTS[serviceKey];\n  notes.push(serviceKey + ' work earned ' + SERVICE_POINTS[serviceKey]);\n}\n\nconst t = timeline.toLowerCase();\nif (t.includes('week') || t.includes('today') || t.includes('asap') || t.includes('now')) {\n  score += TIMELINE_POINTS.thisWeek;\n  notes.push('wants it this week earned ' + TIMELINE_POINTS.thisWeek);\n} else if (t.includes('month')) {\n  score += TIMELINE_POINTS.thisMonth;\n  notes.push('wants it this month earned ' + TIMELINE_POINTS.thisMonth);\n}\n\nconst budgetAmount = Number(String(budget).replace(/[^0-9.]/g, '')) || 0;\nconst bBand = BUDGET_BANDS.find(b => budgetAmount >= b.atLeast);\nif (bBand) {\n  score += bBand.points;\n  notes.push('budget around ' + budgetAmount + ' earned ' + bBand.points);\n}\n\nconst sourceKey = Object.keys(SOURCE_POINTS).find(k => source.toLowerCase().includes(k));\nif (sourceKey) {\n  score += SOURCE_POINTS[sourceKey];\n  notes.push('came from ' + sourceKey + ' earned ' + SOURCE_POINTS[sourceKey]);\n}\n\nif (phone) { score += HAS_PHONE_POINTS; notes.push('left a phone number earned ' + HAS_PHONE_POINTS); }\nif (email) { score += HAS_EMAIL_POINTS; notes.push('left an email earned ' + HAS_EMAIL_POINTS); }\n\nscore = Math.min(100, score);\nconst band = score >= HOT_AT ? 'hot' : score >= WARM_AT ? 'warm' : 'cold';\n\nreturn [{\n  json: {\n    fullName,\n    phone,\n    email,\n    service,\n    budget,\n    budgetAmount,\n    timeline,\n    message,\n    source,\n    score,\n    band,\n    scoreNotes: notes.join(', ') || 'nothing on the form earned points',\n    receivedAt: new Date().toISOString()\n  }\n}];"
      },
      "id": "wf-score",
      "name": "Score the lead",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "resource": "text",
        "operation": "message",
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "claude-haiku-4-5"
        },
        "messages": {
          "values": [
            {
              "role": "user",
              "content": "=You write one line for the sales reps at Blue Summit Home Services, an HVAC and plumbing company in Denver.\n\nA lead was just scored by fixed rules, not by you. Your only job is to say why it scored the way it did so the rep gets it at a glance.\n\nThe lead:\nName: {{ $json.fullName }}\nAsked about: {{ $json.service || 'not said' }}\nWhen they want it: {{ $json.timeline || 'not said' }}\nBudget they gave: {{ $json.budget || 'not said' }}\nCame from: {{ $json.source }}\nWhat they wrote: {{ $json.message || 'nothing' }}\nScore: {{ $json.score }} out of 100, which makes them {{ $json.band }}\nWhat earned points: {{ $json.scoreNotes }}\n\nWrite one plain sentence for the rep. Rules:\n- Write like one person texting another. Under 25 words.\n- No corporate phrases, no emojis, no em dashes, no exclamation points, no lists of three.\n- Say what they want, how soon, and what pushed the score where it landed.\n\nReply with the sentence only. No quotes, no labels, nothing else."
            }
          ]
        },
        "options": {}
      },
      "id": "wf-claude",
      "name": "Have Claude explain the score in one line",
      "type": "@n8n/n8n-nodes-langchain.anthropic",
      "typeVersion": 1,
      "position": [
        440,
        0
      ],
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// The reps rotate in this order. Add or remove one here and the rotation adjusts.\nconst REPS = [\n  { name: 'Alex Rivera', phone: '+15550142201', email: 'alex@blue-summit.example.com' },\n  { name: 'Sam Porter', phone: '+15550142202', email: 'sam@blue-summit.example.com' },\n  { name: 'Jamie Lee', phone: '+15550142203', email: 'jamie@blue-summit.example.com' }\n];\n\nconst lead = $('Score the lead').first().json;\nconst reply = $input.first().json || {};\nconst text = reply.content?.[0]?.text || reply.text || reply.output || '';\nlet whyLine = String(text).trim().replace(/^\"+|\"+$/g, '');\nif (!whyLine) {\n  whyLine = lead.fullName + ' scored ' + lead.score + '. ' + (lead.service || 'No service named') + ', ' + (lead.timeline || 'no timing given') + '.';\n}\n\n// The workflow remembers whose turn it is between runs, so hot leads\n// go around the team evenly. Only a hot lead takes a turn.\nlet rep = null;\nif (lead.band === 'hot') {\n  const memory = $getWorkflowStaticData('global');\n  const turn = Number.isInteger(memory.nextRepTurn) ? memory.nextRepTurn : 0;\n  rep = REPS[turn % REPS.length];\n  memory.nextRepTurn = (turn + 1) % REPS.length;\n}\n\nreturn [{\n  json: {\n    ...lead,\n    whyLine,\n    repName: rep ? rep.name : '',\n    repFirstName: rep ? rep.name.split(' ')[0] : '',\n    repPhone: rep ? rep.phone : '',\n    repEmail: rep ? rep.email : '',\n    repTag: rep ? 'assigned-' + rep.name.split(' ')[0].toLowerCase() : '',\n    assignedTo: rep ? rep.name : 'unassigned'\n  }\n}];"
      },
      "id": "wf-pick-rep",
      "name": "Pick the next rep in line",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        0
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "band-hot",
                    "leftValue": "={{ $json.band }}",
                    "rightValue": "hot",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "hot"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "band-warm",
                    "leftValue": "={{ $json.band }}",
                    "rightValue": "warm",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "warm"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "band-cold",
                    "leftValue": "={{ $json.band }}",
                    "rightValue": "cold",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "cold"
            }
          ]
        },
        "options": {}
      },
      "id": "wf-switch",
      "name": "Hot, warm or cold",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        880,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://services.leadconnectorhq.com/contacts/upsert",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Version",
              "value": "2021-07-28"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ locationId: 'bsHomeSvcDemo01', name: $json.fullName, phone: $json.phone, email: $json.email, source: $json.source, tags: ['hot-lead', $json.repTag] }) }}",
        "options": {}
      },
      "id": "wf-ghl-hot",
      "name": "Save the hot lead in GoHighLevel",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1120,
        -240
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://services.leadconnectorhq.com/contacts/{{ $json.contact.id }}/tasks",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Version",
              "value": "2021-07-28"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ title: 'Call this lead now', body: $('Pick the next rep in line').first().json.whyLine + ' Their number is ' + ($('Pick the next rep in line').first().json.phone || 'on the contact record') + '.', dueDate: new Date(Date.now() + 15 * 60 * 1000).toISOString(), completed: false }) }}",
        "options": {}
      },
      "id": "wf-ghl-task",
      "name": "Give the rep a task due in 15 minutes",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1340,
        -240
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "#sales-team",
          "mode": "name"
        },
        "text": "=Hot lead. {{ $('Pick the next rep in line').first().json.repName }} has it.\n\n{{ $('Pick the next rep in line').first().json.whyLine }}\n\n{{ $('Pick the next rep in line').first().json.fullName }} / {{ $('Pick the next rep in line').first().json.phone || 'no phone' }} / {{ $('Pick the next rep in line').first().json.email || 'no email' }}\nAsked about: {{ $('Pick the next rep in line').first().json.service || 'not said' }}\nWhen: {{ $('Pick the next rep in line').first().json.timeline || 'not said' }}\nBudget: {{ $('Pick the next rep in line').first().json.budget || 'not said' }}\nScore: {{ $('Pick the next rep in line').first().json.score }} out of 100\n\n{{ $('Pick the next rep in line').first().json.repFirstName }} got a text and has a call task due in 15 minutes.",
        "otherOptions": {}
      },
      "id": "wf-slack-hot",
      "name": "Tell the sales team who has it",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1560,
        -240
      ],
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "sms",
        "operation": "send",
        "from": "+15550149000",
        "to": "={{ $('Pick the next rep in line').first().json.repPhone }}",
        "message": "=Hey {{ $('Pick the next rep in line').first().json.repFirstName }}, you are up. {{ $('Pick the next rep in line').first().json.whyLine }} Call {{ $('Pick the next rep in line').first().json.fullName }} at {{ $('Pick the next rep in line').first().json.phone || 'the number on the contact' }}. Task is due in 15 minutes.",
        "options": {}
      },
      "id": "wf-twilio-rep",
      "name": "Text the rep their new lead",
      "type": "n8n-nodes-base.twilio",
      "typeVersion": 1,
      "position": [
        1780,
        -240
      ],
      "credentials": {
        "twilioApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://services.leadconnectorhq.com/contacts/upsert",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Version",
              "value": "2021-07-28"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ locationId: 'bsHomeSvcDemo01', name: $json.fullName, phone: $json.phone, email: $json.email, source: $json.source, tags: ['warm-lead', 'nurture'] }) }}",
        "options": {}
      },
      "id": "wf-ghl-warm",
      "name": "Save the warm lead in GoHighLevel",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1120,
        40
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "#sales-team",
          "mode": "name"
        },
        "text": "=Warm lead, no rush.\n\n{{ $('Pick the next rep in line').first().json.whyLine }}\n\n{{ $('Pick the next rep in line').first().json.fullName }} / {{ $('Pick the next rep in line').first().json.phone || 'no phone' }} / {{ $('Pick the next rep in line').first().json.email || 'no email' }}\nScore: {{ $('Pick the next rep in line').first().json.score }} out of 100\n\nThey are saved in GoHighLevel on the nurture list. Nobody is assigned.",
        "otherOptions": {}
      },
      "id": "wf-slack-warm",
      "name": "Leave a note for the sales team",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1340,
        40
      ],
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://services.leadconnectorhq.com/contacts/upsert",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Version",
              "value": "2021-07-28"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ locationId: 'bsHomeSvcDemo01', name: $json.fullName, phone: $json.phone, email: $json.email, source: $json.source, tags: ['cold-lead'] }) }}",
        "options": {}
      },
      "id": "wf-ghl-cold",
      "name": "Save the cold lead in GoHighLevel",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1120,
        280
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "authentication": "airtableTokenApi",
        "resource": "record",
        "operation": "create",
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appBlueSummit001"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblLeads"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Name": "={{ $('Pick the next rep in line').first().json.fullName }}",
            "Score": "={{ $('Pick the next rep in line').first().json.score }}",
            "Band": "={{ $('Pick the next rep in line').first().json.band }}",
            "Assigned rep": "={{ $('Pick the next rep in line').first().json.assignedTo }}",
            "Source": "={{ $('Pick the next rep in line').first().json.source }}"
          },
          "matchingColumns": [],
          "schema": []
        },
        "options": {}
      },
      "id": "wf-airtable-log",
      "name": "Log the lead in Airtable",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.1,
      "position": [
        2020,
        0
      ],
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ score: $('Pick the next rep in line').first().json.score, band: $('Pick the next rep in line').first().json.band, assigned_to: $('Pick the next rep in line').first().json.assignedTo }) }}",
        "options": {}
      },
      "id": "wf-respond",
      "name": "Answer the website form",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        2240,
        0
      ]
    }
  ],
  "connections": {
    "A new lead comes in from the website": {
      "main": [
        [
          {
            "node": "Score the lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score the lead": {
      "main": [
        [
          {
            "node": "Have Claude explain the score in one line",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Have Claude explain the score in one line": {
      "main": [
        [
          {
            "node": "Pick the next rep in line",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick the next rep in line": {
      "main": [
        [
          {
            "node": "Hot, warm or cold",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hot, warm or cold": {
      "main": [
        [
          {
            "node": "Save the hot lead in GoHighLevel",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Save the warm lead in GoHighLevel",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Save the cold lead in GoHighLevel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save the hot lead in GoHighLevel": {
      "main": [
        [
          {
            "node": "Give the rep a task due in 15 minutes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Give the rep a task due in 15 minutes": {
      "main": [
        [
          {
            "node": "Tell the sales team who has it",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tell the sales team who has it": {
      "main": [
        [
          {
            "node": "Text the rep their new lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text the rep their new lead": {
      "main": [
        [
          {
            "node": "Log the lead in Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save the warm lead in GoHighLevel": {
      "main": [
        [
          {
            "node": "Leave a note for the sales team",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Leave a note for the sales team": {
      "main": [
        [
          {
            "node": "Log the lead in Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save the cold lead in GoHighLevel": {
      "main": [
        [
          {
            "node": "Log the lead in Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log the lead in Airtable": {
      "main": [
        [
          {
            "node": "Answer the website form",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}