{
  "name": "AI Lead Qualifier",
  "nodes": [
    {
      "parameters": {
        "content": "## AI lead qualifier\n\nScores every inbound lead against your ideal client rubric and writes the first reply.\n\n**Setup**\n1. Send leads to the **New Lead** webhook URL (any JSON: name, email, phone, message, budget, etc).\n2. Edit the rubric in **Rubric** with your business, ideal client and service area.\n3. Connect OpenAI (the two **Score Lead** nodes) and Google Sheets. Tab **Qualified Leads**: email, name, score, tier, reasoning, first_reply, tokens, cost_usd, scored_at.\n4. Paste a Slack incoming webhook into **Alert Sales**. Change the hot threshold (18) in **Hot?** if you like.",
        "height": 400,
        "width": 440
      },
      "id": "f18e1e03-5777-4020-9dd9-08f7951e7e20",
      "name": "Note 0",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -460,
        -240
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "qualify-lead",
        "options": {}
      },
      "id": "ae27f1ec-3ad0-43d9-9d13-f45674a937b0",
      "name": "New Lead",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "webhookId": "4026330c-6416-407e-b91b-cd9b9c9f3481"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "bd655cd1-7aad-43b6-a16c-34aa14426910",
              "name": "rubric",
              "value": "Score the lead 0-5 on each: budget, timeline, service fit, location, authority (decision maker). Total 0-25. Tiers: 18+ hot, 12-17 warm, 6-11 cold, under 6 or obvious spam disqualified. Our business: YOUR ONE-LINE DESCRIPTION. Ideal client: YOUR IDEAL CLIENT. Service area: YOUR AREA. first_reply: a friendly, specific first reply under 320 characters that moves them toward booking a call.",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "ba4b19d7-0e1f-41c7-a479-27061db3df50",
      "name": "Rubric",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'gpt-4.1-mini', temperature: 0.2, response_format: { type: 'json_schema', json_schema: { name: 'lead_score', strict: true, schema: {\"type\": \"object\", \"additionalProperties\": false, \"required\": [\"score\", \"tier\", \"reasoning\", \"first_reply\"], \"properties\": {\"score\": {\"type\": \"integer\"}, \"tier\": {\"type\": \"string\", \"enum\": [\"hot\", \"warm\", \"cold\", \"disqualified\"]}, \"reasoning\": {\"type\": \"string\"}, \"first_reply\": {\"type\": \"string\"}}} } }, messages: [ { role: 'system', content: $('Rubric').item.json.rubric }, { role: 'user', content: 'Lead: ' + JSON.stringify($('New Lead').item.json.body) } ] }) }}",
        "options": {}
      },
      "id": "fb5c59fd-8bf9-46e1-b893-aef704c127d1",
      "name": "Score Lead",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "// Check the model's JSON. Passes {valid, error, ...result} on.\nconst PRICE_IN = 0.40 / 1e6, PRICE_OUT = 1.60 / 1e6;  // gpt-4.1-mini USD per token, update if you change model\nlet r, error = '';\ntry {\n  r = JSON.parse($json.choices[0].message.content);\n  if (!Number.isInteger(r.score) || r.score < 0 || r.score > 25) error = 'score must be an integer 0-25';\n  else if (!['hot','warm','cold','disqualified'].includes(r.tier)) error = 'tier is invalid';\n  else if (!r.first_reply || r.first_reply.length > 320) error = 'first_reply must be 1-320 characters';\n} catch (e) { error = 'response was not valid JSON: ' + e.message; r = {}; }\nconst u = $json.usage || {};\nreturn [{ json: { ...r, valid: !error, error, tokens: (u.total_tokens || 0), cost_usd: +((u.prompt_tokens || 0) * PRICE_IN + (u.completion_tokens || 0) * PRICE_OUT).toFixed(5) } }];",
        "mode": "runOnceForEachItem"
      },
      "id": "ee49d69b-d00a-4058-96f5-0fae2ba5ff75",
      "name": "Validate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "925a6265-b705-4dab-91e2-d509ca242f59",
              "leftValue": "={{ $json.valid }}",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "rightValue": ""
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "149cfa82-fb57-4651-958c-dadbd21f0e63",
      "name": "Valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        880,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'gpt-4.1-mini', temperature: 0.2, response_format: { type: 'json_schema', json_schema: { name: 'lead_score', strict: true, schema: {\"type\": \"object\", \"additionalProperties\": false, \"required\": [\"score\", \"tier\", \"reasoning\", \"first_reply\"], \"properties\": {\"score\": {\"type\": \"integer\"}, \"tier\": {\"type\": \"string\", \"enum\": [\"hot\", \"warm\", \"cold\", \"disqualified\"]}, \"reasoning\": {\"type\": \"string\"}, \"first_reply\": {\"type\": \"string\"}}} } }, messages: [ { role: 'system', content: $('Rubric').item.json.rubric }, { role: 'user', content: 'Lead: ' + JSON.stringify($('New Lead').item.json.body) + '\\n\\nYour last answer failed validation: ' + $('Validate').item.json.error + '. Fix it.' } ] }) }}",
        "options": {}
      },
      "id": "fc6ab47e-513b-4679-96e1-465498067bf8",
      "name": "Score Lead (Retry)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        160
      ]
    },
    {
      "parameters": {
        "jsCode": "// Check the model's JSON. Passes {valid, error, ...result} on.\nconst PRICE_IN = 0.40 / 1e6, PRICE_OUT = 1.60 / 1e6;  // gpt-4.1-mini USD per token, update if you change model\nlet r, error = '';\ntry {\n  r = JSON.parse($json.choices[0].message.content);\n  if (!Number.isInteger(r.score) || r.score < 0 || r.score > 25) error = 'score must be an integer 0-25';\n  else if (!['hot','warm','cold','disqualified'].includes(r.tier)) error = 'tier is invalid';\n  else if (!r.first_reply || r.first_reply.length > 320) error = 'first_reply must be 1-320 characters';\n} catch (e) { error = 'response was not valid JSON: ' + e.message; r = {}; }\nconst u = $json.usage || {};\nreturn [{ json: { ...r, valid: !error, error, tokens: (u.total_tokens || 0), cost_usd: +((u.prompt_tokens || 0) * PRICE_IN + (u.completion_tokens || 0) * PRICE_OUT).toFixed(5) } }];\n",
        "mode": "runOnceForEachItem"
      },
      "id": "40a61eb4-72ab-46b7-b406-541595c8b6ba",
      "name": "Validate Retry",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        160
      ]
    },
    {
      "parameters": {
        "jsCode": "// After one retry, park it for a human.\nif ($json.valid) return [{ json: $json }];\nreturn [{ json: { ...$json, score: 0, tier: 'needs review', reasoning: 'Model output failed validation twice: ' + $json.error, first_reply: '' } }];",
        "mode": "runOnceForEachItem"
      },
      "id": "2bf115d2-f0cb-4e6a-a4b2-25c755d5aedf",
      "name": "Fallback if Still Invalid",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        160
      ]
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Qualified Leads"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "email": "={{ $('New Lead').item.json.body.email }}",
            "name": "={{ $('New Lead').item.json.body.name }}",
            "score": "={{ $json.score }}",
            "tier": "={{ $json.tier }}",
            "reasoning": "={{ $json.reasoning }}",
            "first_reply": "={{ $json.first_reply }}",
            "tokens": "={{ $json.tokens }}",
            "cost_usd": "={{ $json.cost_usd }}",
            "scored_at": "={{ $now.toISO() }}"
          },
          "matchingColumns": [
            "email"
          ],
          "schema": [
            {
              "id": "email",
              "displayName": "email",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "name",
              "displayName": "name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "score",
              "displayName": "score",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "tier",
              "displayName": "tier",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "reasoning",
              "displayName": "reasoning",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "first_reply",
              "displayName": "first_reply",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "tokens",
              "displayName": "tokens",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "cost_usd",
              "displayName": "cost_usd",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "scored_at",
              "displayName": "scored_at",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ]
        },
        "options": {}
      },
      "id": "7a8463bb-4666-40e0-b84b-e252f8e85031",
      "name": "Save Score",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1760,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "a4cd3e0a-7854-45df-8723-fc7208783db6",
              "leftValue": "={{ Number($('Save Score').item.json.score) }}",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "rightValue": 18
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "394a0f00-574b-46a8-9413-66c268fe3e40",
      "name": "Hot?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1980,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "YOUR_SLACK_WEBHOOK_URL",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ text: `🔥 Hot lead (${$('Save Score').item.json.score}/25): ${$('New Lead').item.json.body.name} ${$('New Lead').item.json.body.email}\\n${$('Save Score').item.json.reasoning}\\nSuggested reply: ${$('Save Score').item.json.first_reply}` }) }}",
        "options": {}
      },
      "id": "10abdbeb-1145-4fa9-a0ed-8274ed4eb7a8",
      "name": "Alert Sales",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2200,
        -20
      ]
    }
  ],
  "connections": {
    "New Lead": {
      "main": [
        [
          {
            "node": "Rubric",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rubric": {
      "main": [
        [
          {
            "node": "Score Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Lead": {
      "main": [
        [
          {
            "node": "Validate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate": {
      "main": [
        [
          {
            "node": "Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Valid?": {
      "main": [
        [
          {
            "node": "Save Score",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Score Lead (Retry)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Lead (Retry)": {
      "main": [
        [
          {
            "node": "Validate Retry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Retry": {
      "main": [
        [
          {
            "node": "Fallback if Still Invalid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fallback if Still Invalid": {
      "main": [
        [
          {
            "node": "Save Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Score": {
      "main": [
        [
          {
            "node": "Hot?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hot?": {
      "main": [
        [
          {
            "node": "Alert Sales",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false
}