Skip to content

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

{
  "name": "19 - Real Estate Lead Nurture (starter)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "re/lead",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "m19-lead-api",
      "name": "New Lead API",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        0
      ],
      "webhookId": "course-19-lead"
    },
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "simple": false,
        "filters": {
          "q": "from:(zillow.com OR realtor.com OR homes.com OR redfin.com) newer_than:1d",
          "readStatus": "unread"
        },
        "options": {}
      },
      "id": "m19-gmail",
      "name": "Portal Lead Emails",
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.3,
      "position": [
        0,
        180
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 1
            }
          ]
        }
      },
      "id": "m19-drip-schedule",
      "name": "Drip Every Hour",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        420
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "re/drip/run",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "m19-drip-api",
      "name": "Drip Run API",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        600
      ],
      "webhookId": "course-19-drip"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "re/lead/stop",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "m19-stop-api",
      "name": "Stop Lead API",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        780
      ],
      "webhookId": "course-19-stop"
    },
    {
      "parameters": {
        "path": "re/lead/status",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "m19-status-api",
      "name": "Lead Status API",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        960
      ],
      "webhookId": "course-19-status"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "s1",
              "name": "brokerage",
              "value": "Hill Country Realty",
              "type": "string"
            },
            {
              "id": "s2",
              "name": "booking_link",
              "value": "https://cal.com/hill-country-realty/showing",
              "type": "string"
            },
            {
              "id": "s3",
              "name": "agents",
              "value": "={{ [ { name: 'Maria Lopez', email: 'maria@hillcountry.example', phone: '(512) 555-0101' }, { name: 'James Carter', email: 'james@hillcountry.example', phone: '(512) 555-0102' } ] }}",
              "type": "array"
            },
            {
              "id": "s4",
              "name": "sms_from",
              "value": "+15125550100",
              "type": "string"
            },
            {
              "id": "s5",
              "name": "sms_enabled",
              "value": true,
              "type": "boolean"
            },
            {
              "id": "s6",
              "name": "dedupe_days",
              "value": 30,
              "type": "number"
            },
            {
              "id": "s7",
              "name": "drip",
              "value": "={{ [ { step: 'checkin', day: 1 }, { step: 'listings', day: 3 }, { step: 'last_chance', day: 7 } ] }}",
              "type": "array"
            },
            {
              "id": "s8",
              "name": "listings",
              "value": "={{ [\n  { id: 'HC-1001', address: '214 Live Oak St', city: 'Austin', price: 465000, beds: 3, baths: 2, type: 'sale', status: 'active', listed_at: '2026-10-01', url: 'https://example.com/listings/HC-1001' },\n  { id: 'HC-1002', address: '88 Barton Hills Dr', city: 'Austin', price: 520000, beds: 3, baths: 2.5, type: 'sale', status: 'active', listed_at: '2026-10-05', url: 'https://example.com/listings/HC-1002' },\n  { id: 'HC-1003', address: '1910 Cedar Bend Ln', city: 'Austin', price: 430000, beds: 3, baths: 2, type: 'sale', status: 'active', listed_at: '2026-09-28', url: 'https://example.com/listings/HC-1003' },\n  { id: 'HC-1004', address: '5 Riverside Ct', city: 'Austin', price: 899000, beds: 5, baths: 4, type: 'sale', status: 'active', listed_at: '2026-10-08', url: 'https://example.com/listings/HC-1004' },\n  { id: 'HC-1005', address: '742 Maple Way', city: 'Round Rock', price: 410000, beds: 4, baths: 3, type: 'sale', status: 'active', listed_at: '2026-10-03', url: 'https://example.com/listings/HC-1005' },\n  { id: 'HC-1006', address: '300 Lamar Blvd Unit 12', city: 'Austin', price: 2400, beds: 2, baths: 2, type: 'rent', status: 'active', listed_at: '2026-10-09', url: 'https://example.com/listings/HC-1006' },\n  { id: 'HC-1007', address: '61 Pecan Grove Rd', city: 'Austin', price: 2150, beds: 2, baths: 1, type: 'rent', status: 'active', listed_at: '2026-10-02', url: 'https://example.com/listings/HC-1007' },\n  { id: 'HC-1008', address: '17 Bluebonnet Dr', city: 'Austin', price: 489000, beds: 4, baths: 3, type: 'sale', status: 'pending', listed_at: '2026-09-20', url: 'https://example.com/listings/HC-1008' }\n] }}",
              "type": "array"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "m19-settings",
      "name": "Settings (edit me)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        240,
        480
      ]
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "r1",
                    "leftValue": "={{ $('New Lead API').isExecuted || $('Portal Lead Emails').isExecuted }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "new lead"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "r2",
                    "leftValue": "={{ $('Drip Every Hour').isExecuted || $('Drip Run API').isExecuted }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "drip"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "r3",
                    "leftValue": "={{ $('Stop Lead API').isExecuted }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "stop"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "r4",
                    "leftValue": "={{ $('Lead Status API').isExecuted }}",
                    "rightValue": "",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    }
                  }
                ],
                "combinator": "and"
              },
              "renameOutput": true,
              "outputKey": "status"
            }
          ]
        },
        "options": {}
      },
      "id": "m19-route",
      "name": "Route",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        460,
        480
      ]
    },
    {
      "parameters": {
        "jsCode": "// Turn any inquiry into one clean lead: structured JSON, a pasted portal email, or the Gmail trigger.\nconst viaApi = $('New Lead API').isExecuted;\nconst src = viaApi ? $('New Lead API').first().json : $('Portal Lead Emails').first().json;\nconst b = viaApi ? src.body ?? {} : {};\n\n// Raw email text (API \"raw_email\" or Gmail trigger)\nconst from = viaApi ? String(b.from ?? '') : String(src.from?.value?.[0]?.address ?? src.from ?? '');\nconst subject = viaApi ? String(b.subject ?? '') : String(src.subject ?? '');\nconst raw = viaApi ? String(b.raw_email ?? '') : String(src.text ?? src.snippet ?? '');\nconst all = `${subject}\\n${raw}`;\nconst field = (label) => (raw.match(new RegExp(`^\\\\s*${label}\\\\s*[:\\\\-]\\\\s*(.+)$`, 'im'))?.[1] ?? '').trim();\n\nconst name = String(b.name ?? '').trim() || field('Name') || field('From');\nconst email = (String(b.email ?? '').trim() || field('Email') || (raw.match(/[\\w.+-]+@[\\w-]+\\.[\\w.-]+/)?.[0] ?? '')).toLowerCase();\nlet phone = (String(b.phone ?? '') || field('Phone') || (raw.match(/\\(?\\d{3}\\)?[\\s.-]\\d{3}[\\s.-]\\d{4}/)?.[0] ?? '')).replace(/[^\\d+]/g, '');\nif (/^\\d{10}$/.test(phone)) phone = '+1' + phone;\nif (/^1\\d{10}$/.test(phone)) phone = '+' + phone;\nconst message = String(b.message ?? '').trim() || field('Message') || field('Comments');\nconst property = String(b.property ?? '').trim() || field('Property') || field('Listing') || (all.match(/\\d{2,6} [A-Z][\\w ]+? (?:St|Street|Ave|Avenue|Rd|Road|Dr|Drive|Ln|Lane|Blvd|Ct|Way)\\b[^\\n]*/)?.[0] ?? '').trim();\nconst listingId = String(b.listing_id ?? '').trim() || field('Listing ID') || (all.match(/\\b(?:MLS|Listing)\\s*#?\\s*([A-Z0-9-]{4,})/i)?.[1] ?? '');\n\nconst text = `${message} ${subject} ${raw}`.toLowerCase();\nconst sourceHay = `${b.source ?? ''} ${from} ${subject}`.toLowerCase();\nconst source = ['zillow', 'realtor.com', 'homes.com', 'redfin', 'trulia'].find((s) => sourceHay.includes(s)) ?? (String(b.source ?? '').toLowerCase() || 'website');\nconst intent = /\\b(rent|lease|rental)\\b/.test(text) ? 'renter' : /(sell my|home value|what.?s my home worth|list my|selling)/.test(text) ? 'seller' : 'buyer';\nconst timeframe = (text.match(/\\b(asap|this week|this month|next month|30 days|60 days|90 days|3 months|6 months|this year|just looking|browsing)\\b/)?.[1]) ?? null;\n\nreturn [{\n  json: {\n    name, first_name: name.split(' ')[0] || 'there', email, phone, message, property, listing_id: listingId,\n    source, intent, timeframe, pre_approved: /pre-?approved|preapproved/.test(text),\n    valid: Boolean(email || phone),\n    dry_run: viaApi && src.query?.dry_run === 'true',\n    via_api: viaApi,\n    received_at: DateTime.now().toISO(),\n  },\n}];\n"
      },
      "id": "m19-parse",
      "name": "Parse Lead",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        700,
        80
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "v1",
              "leftValue": "={{ $json.valid }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "m19-valid",
      "name": "Has Contact?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        920,
        80
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { error: 'need an email or phone: send {name, email, phone, message, property} or {from, subject, raw_email}' } }}",
        "options": {
          "responseCode": 400
        }
      },
      "id": "m19-400",
      "name": "Respond No Contact",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1140,
        240
      ]
    },
    {
      "parameters": {
        "jsCode": "// Find the listing they asked about, then up to 3 similar ones (same type and city, price within ±20%).\nconst s = $('Settings (edit me)').first().json;\nconst lead = $input.first().json;\nconst L = s.listings;\nconst norm = (x) => String(x ?? '').toLowerCase().replace(/[^a-z0-9 ]/g, ' ').replace(/\\s+/g, ' ').trim();\n\nconst asked = L.find((l) => lead.listing_id && l.id.toLowerCase() === lead.listing_id.toLowerCase())\n  ?? L.find((l) => lead.property && norm(lead.property).includes(norm(l.address).split(' ').slice(0, 3).join(' ')));\nconst wantType = lead.intent === 'renter' ? 'rent' : 'sale';\nconst ref = asked ?? { city: (L.find((l) => norm(lead.property).includes(norm(l.city)))?.city) ?? null, price: null, type: wantType };\n\nconst similar = L\n  .filter((l) => l.status === 'active' && l.id !== asked?.id && l.type === (asked?.type ?? wantType))\n  .filter((l) => !ref.city || l.city === ref.city)\n  .filter((l) => !ref.price || Math.abs(l.price - ref.price) / ref.price <= 0.2)\n  .sort((a, b) => (ref.price ? Math.abs(a.price - ref.price) - Math.abs(b.price - ref.price) : b.listed_at.localeCompare(a.listed_at)))\n  .slice(0, 3);\n\nreturn [{ json: { ...lead, asked_listing: asked ?? null, matches: lead.intent === 'seller' ? [] : similar } }];\n"
      },
      "id": "m19-match",
      "name": "Match Listings",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1140,
        0
      ]
    },
    {
      "parameters": {
        "content": "## Your task\nParsing and listing matches are done. Try `?dry_run=true` with a lead.\n1. **Register Lead** (Code + static data): dedupe by email/phone (30 days), round-robin agent, schedule drip day 1/3/7.\n2. **Compose Replies**: SMS (≤ 320 chars, opt-out) + email from the agent; seller and duplicate variants.\n3. Dry run → respond. Else **Twilio** SMS (if phone) and **Gmail**, each *On Error: Continue* → report.\n4. Drip lane: find due steps (inject `now` via the API), send, mark sent.\n5. Stop lane: mark the lead stopped. Status lane: return the record.\n\nTest: `./test/test.sh`",
        "height": 260,
        "width": 420
      },
      "id": "starter-task-note",
      "name": "Your Task",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -340
      ]
    }
  ],
  "connections": {
    "New Lead API": {
      "main": [
        [
          {
            "node": "Settings (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Portal Lead Emails": {
      "main": [
        [
          {
            "node": "Settings (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Drip Every Hour": {
      "main": [
        [
          {
            "node": "Settings (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Drip Run API": {
      "main": [
        [
          {
            "node": "Settings (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Stop Lead API": {
      "main": [
        [
          {
            "node": "Settings (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lead Status API": {
      "main": [
        [
          {
            "node": "Settings (edit me)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings (edit me)": {
      "main": [
        [
          {
            "node": "Route",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route": {
      "main": [
        [
          {
            "node": "Parse Lead",
            "type": "main",
            "index": 0
          }
        ],
        [],
        [],
        []
      ]
    },
    "Parse Lead": {
      "main": [
        [
          {
            "node": "Has Contact?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Contact?": {
      "main": [
        [
          {
            "node": "Match Listings",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond No Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Match Listings": {
      "main": [
        []
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  }
}