Module 19 · business · intermediate
Solutions 19 · Real Estate Lead Nurture
Exercise 1
Replace the No Operation node with Twilio SMS → Send (to {{ $json.agent.phone }}) or Gmail to {{ $json.agent.email }}:
🏠 New {{ $json.lead.intent }} lead ({{ $json.lead.source }}){{ $json.is_duplicate ? ' (repeat inquiry)' : '' }}
{{ $json.lead.name }} · {{ $json.lead.phone || 'no phone' }} · {{ $json.lead.email || 'no email' }}
Property: {{ $json.asked_listing ? $json.asked_listing.address : 'not specified' }}
Timeframe: {{ $json.lead.timeframe || 'unknown' }} · Pre-approved: {{ $json.lead.pre_approved ? 'yes' : 'no' }}
Instant reply: SMS {{ $json.sent.sms ? '✓' : '✗' }} · Email {{ $json.sent.email ? '✓' : '✗' }}
Exercise 2
Email replies: a new workflow with a Gmail Trigger on the brokerage inbox (query in:inbox -from:me newer_than:1d, Simplify on) → HTTP Request POST http://localhost:5678/webhook/re/lead/stop, JSON { "email": "{{ $json.From.match(/<(.+)>/)?.[1] ?? $json.From }}", "reason": "replied by email" }. A 404 for non-leads is fine; set On Error → Continue.
SMS replies: in Twilio, set the number's A message comes in webhook to a new n8n Webhook (POST, path re/sms-inbound). Twilio posts form fields From and Body.
Webhook → HTTP Request POST /webhook/re/lead/stop
body: { "phone": "{{ $json.body.From }}",
"reason": "{{ /^\s*(stop|unsubscribe|cancel|end|quit)\s*$/i.test($json.body.Body) ? 'opt-out' : 'replied by SMS' }}" }
→ Respond to Webhook with empty TwiML: <Response></Response> (Content-Type text/xml)
Twilio also handles standard STOP keywords at the carrier level for US numbers; recording the opt-out yourself keeps your own system in sync. Neither flow has been run against live Gmail/Twilio yet.
Exercise 3
- Create a sheet
Listingswith columnsid, address, city, price, beds, baths, type, status, listed_at, url. - In the new-lead lane, add Google Sheets Get Row(s) (Always Output Data on) between
Has Contact?andMatch Listings. - In
match.js, replaceconst L = s.listings;with:
(the current item is now a sheet row, so read the lead from the IF node).const L = $('Get Listings').all().map(i => i.json).filter(r => r.id) .map(r => ({ ...r, price: Number(r.price), beds: Number(r.beds), baths: Number(r.baths) })); const lead = $('Has Contact?').first().json; - Do the same in
due.jsfor the listings email (read the sheet in the drip lane too).
Stretch
In due.js, emit a second item for step checkin when the lead has a phone: { ...sameFields, channel: 'sms', text: '…Reply STOP to opt out.' }. Route by channel with a Switch: email → Gmail, sms → Twilio. In record-drip.js, pair results per channel and mark the step sent only when the email succeeded. Never text a lead whose stop reason is opt-out.