Skip to content

None of these have been run against live Google, Slack or WhatsApp accounts yet.

Exercise 1

Replace the No Operation node with Google Sheets Append Row (sheet Leads, columns received_at, wa_id, name, service, zip, preferred_time, chat):

Column Value
received_at {{ $now.toISO() }}
wa_id {{ $json.wa_id }}
name {{ $json.lead.name }}
service {{ $json.lead.service }}
zip {{ $json.lead.zip }}
preferred_time {{ $json.lead.preferred_time }}
chat https://wa.me/{{ $json.wa_id }}

It runs once per booking: new_lead is true only on the turn that completes the lead (lead_captured_at is set once), and retried webhooks are duplicate, so they never reach this branch.

Exercise 2

Replace the No Operation node with:

HTTP Request GET http://localhost:5678/webhook/wa/conversation?wa_id={{ $json.wa_id }}
→ Slack Send Message (channel #whatsapp):
{{ $('Needs Staff?').item.json.action === 'emergency' ? '🚨 URGENT safety message' : $('Needs Staff?').item.json.action === 'silent' ? '💬 New message (you have this chat)' : '🙋 Customer wants a person' }} from {{ $json.name || $json.wa_id }}
{{ $json.history.slice(-4).map(h => (h.role === 'user' ? '👤 ' : '🤖 ') + h.text).join('\n') }}
Reply in WhatsApp Business, then hand back: POST /webhook/wa/handoff/release {"wa_id":"{{ $json.wa_id }}"}

Exercise 3

  1. Add Schedule Trigger Nudge Every Hour and a fifth Route rule {{ $('Nudge Every Hour').isExecuted }} → output nudge.
  2. Code Find Stuck Bookings:
    const db = $getWorkflowStaticData('global');
    const now = new Date();
    const hoursAgo = (iso) => (now - new Date(iso)) / 3600e3;
    const out = [];
    for (const c of Object.values(db.convos ?? {})) {
      if (!c.awaiting || c.opted_out || c.nudged_at || c.lead_captured_at) continue;
      if (c.handoff_until && c.handoff_until > now.toISOString()) continue;
      if (hoursAgo(c.last_inbound_at) < 2 || c.window_open_until < now.toISOString()) continue;
      c.nudged_at = now.toISOString();
      out.push({ json: { wa_id: c.wa_id, reply: `Hi${c.lead.name ? ' ' + c.lead.name.split(' ')[0] : ''}, still want us to book that ${c.lead.service ?? 'visit'}? Just reply here and we'll pick up where we left off.` } });
    }
    return out;
    
  3. → WhatsApp Send (same settings as the reply node, On Error → Continue).
  4. In apply.js, clear c.nudged_at whenever the customer writes again, so a later stall can be nudged once more.

Marking nudged_at before the send means a failed send isn't retried; move it after the send (as in module 19's drip) if you want retries.

Stretch

For type === 'audio': WhatsApp Media → Download (media id {{ $json.audio.id }}, which you'd add to parse.js) → OpenAI Audio → Transcribe → set text to the transcript and continue into Conversation State. Keep the "please type" reply as the fallback when transcription fails.