Skip to content

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

  1. Create a sheet Listings with columns id, address, city, price, beds, baths, type, status, listed_at, url.
  2. In the new-lead lane, add Google Sheets Get Row(s) (Always Output Data on) between Has Contact? and Match Listings.
  3. In match.js, replace const L = s.listings; with:
    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;
    
    (the current item is now a sheet row, so read the lead from the IF node).
  4. Do the same in due.js for 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.