Skip to content

In n8n, copy this file and paste it onto the canvas, or use Import from File.

{
  "name": "14 - GoHighLevel to n8n Bridge (starter)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ghl/lead",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "m14-webhook",
      "name": "GHL Lead Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        200
      ],
      "webhookId": "course-14-ghl-lead"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "s1",
              "name": "ghl_base_url",
              "value": "https://services.leadconnectorhq.com",
              "type": "string"
            },
            {
              "id": "s2",
              "name": "ghl_api_version",
              "value": "2021-07-28",
              "type": "string"
            },
            {
              "id": "s3",
              "name": "hot_threshold",
              "value": 70,
              "type": "number"
            },
            {
              "id": "s4",
              "name": "warm_threshold",
              "value": 40,
              "type": "number"
            },
            {
              "id": "s5",
              "name": "hot_tag",
              "value": "ai-hot",
              "type": "string"
            },
            {
              "id": "s6",
              "name": "warm_tag",
              "value": "ai-warm",
              "type": "string"
            },
            {
              "id": "s7",
              "name": "cold_tag",
              "value": "ai-cold",
              "type": "string"
            },
            {
              "id": "s8",
              "name": "offer",
              "value": "We are a digital marketing agency for local service businesses (dentists, contractors, clinics). We sell websites, Google Ads management and lead-follow-up automation, from $1,500/month. Ideal client: an established business with steady revenue that wants more booked jobs within the next 1–3 months.",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "m14-settings",
      "name": "Settings (edit me)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "// Accept both GoHighLevel outbound shapes and return one clean lead:\n//  - \"Custom Webhook\" action: the body you define (recommended, see lesson Step 1)\n//  - \"Webhook\" action: standard contact fields, your extra fields inside customData\nconst req = $input.first().json;\nconst b = req.body ?? {};\nconst cd = b.customData ?? {};\nconst pick = (...keys) => {\n  for (const k of keys) {\n    const v = cd[k] ?? b[k] ?? b.contact?.[k];\n    if (v !== undefined && v !== null && String(v).trim() !== '') return String(v).trim();\n  }\n  return '';\n};\n\nconst first = pick('first_name', 'firstName');\nconst last = pick('last_name', 'lastName');\nconst tags = [].concat(b.tags ?? cd.tags ?? []).flatMap((t) => String(t).split(',')).map((t) => t.trim()).filter(Boolean);\n\nreturn [{\n  json: {\n    contact_id: pick('contact_id', 'contactId', 'id'),\n    location_id: pick('location_id', 'locationId') || b.location?.id || '',\n    name: pick('full_name', 'fullName', 'name') || [first, last].filter(Boolean).join(' '),\n    first_name: first,\n    email: pick('email').toLowerCase(),\n    phone: pick('phone').replace(/[^\\d+]/g, ''),\n    source: pick('source', 'contact_source', 'lead_source').toLowerCase(),\n    message: pick('message', 'notes', 'details', 'how_can_we_help').slice(0, 3000),\n    company: pick('company_name', 'companyName', 'company'),\n    website: pick('website'),\n    tags,\n    dry_run: req.query?.dry_run === 'true',\n  },\n}];\n"
      },
      "id": "m14-normalize",
      "name": "Normalize GHL Payload",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        200
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "c1",
              "leftValue": "={{ $json.contact_id }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "m14-has-id",
      "name": "Has Contact ID?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        660,
        200
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { error: \"contact_id is required: in GoHighLevel, map the Contact ID merge field into the webhook body\" } }}",
        "options": {
          "responseCode": 400
        }
      },
      "id": "m14-400",
      "name": "Respond Missing ID",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        880,
        380
      ]
    },
    {
      "parameters": {
        "jsCode": "// Free, instant scoring from the data we already have (no AI, no API calls).\n// Each signal adds points; the AI step later refines this.\nconst FREE_MAIL = ['gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com', 'icloud.com', 'aol.com', 'proton.me', 'protonmail.com', 'live.com', 'msn.com'];\nconst HIGH_INTENT_SOURCES = ['website form', 'referral', 'google ads', 'booking', 'quote request', 'contact form'];\n\nreturn $input.all().map(({ json: lead }) => {\n  const signals = [];\n  let score = 0;\n  const add = (points, why) => { score += points; signals.push(`+${points} ${why}`); };\n\n  const domain = lead.email.split('@')[1] ?? '';\n  const businessEmail = domain !== '' && !FREE_MAIL.includes(domain);\n  const text = `${lead.message} ${lead.company}`.toLowerCase();\n\n  if (businessEmail) add(25, `business email (${domain})`);\n  if (lead.phone.length >= 7) add(15, 'phone number');\n  if (lead.company || lead.website) add(10, 'company or website given');\n  if (lead.message.length >= 40) add(15, 'detailed message');\n  if (HIGH_INTENT_SOURCES.includes(lead.source)) add(10, `high-intent source (${lead.source})`);\n  if (/\\$\\s?\\d|budget|quote|pricing|price|cost|invest/.test(text)) add(15, 'mentions budget or pricing');\n  if (/asap|urgent|this week|today|tomorrow|right away|immediately/.test(text)) add(10, 'urgency');\n\n  return {\n    json: {\n      ...lead,\n      company_domain: businessEmail ? domain : '',\n      rule_score: Math.min(score, 100),\n      rule_signals: signals,\n    },\n  };\n});\n"
      },
      "id": "m14-rules",
      "name": "Rule Score",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        120
      ]
    },
    {
      "parameters": {
        "content": "## Your task\n1. Send a test lead with `?dry_run=true` and read **Rule Score**'s output: score + signals.\n2. Add **Dry Run?** → **AI Qualify** (Information Extractor + OpenAI) scoring fit against the offer in Settings.\n3. **Combine Scores**: 40% rules + 60% AI → tier (hot/warm/cold), tag and a CRM note.\n4. Unless dry run: **HTTP Request** POST `/contacts/{id}/tags` and `/contacts/{id}/notes` on the LeadConnector API (Header Auth credential + `Version: 2021-07-28`).\n5. Respond with the result; alert sales on hot leads.\n\nTest: `./test/test.sh`",
        "height": 260,
        "width": 420
      },
      "id": "starter-task-note",
      "name": "Your Task",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -340
      ]
    }
  ],
  "connections": {
    "GHL Lead Webhook": {
      "main": [
        [
          {
            "node": "Settings (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings (edit me)": {
      "main": [
        [
          {
            "node": "Normalize GHL Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize GHL Payload": {
      "main": [
        [
          {
            "node": "Has Contact ID?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Contact ID?": {
      "main": [
        [
          {
            "node": "Rule Score",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Missing ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rule Score": {
      "main": [
        []
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  }
}