Real Estate Lead Nurture
Module 19 · business · intermediate
starter.json
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"
}
}