AutomationFlowsMarketing & Ads › Automate Whatsapp Lead Follow-ups and Nurturing with Wati, Airtable and Openai

Automate Whatsapp Lead Follow-ups and Nurturing with Wati, Airtable and Openai

ByJitesh Dugar @jiteshdugar on n8n.io

Maximize your conversion rates with this end-to-end automated outreach and lead nurturing system. This workflow manages the entire sales lifecycle—from instant contact enrollment via WhatsApp to AI-personalized follow-ups and comprehensive A/B performance tracking—all integrated…

Event trigger★★★★★ complexity36 nodesN8N Nodes WatiAirtableHTTP Request
Marketing & Ads Trigger: Event Nodes: 36 Complexity: ★★★★★ Added:

This workflow corresponds to n8n.io template #13723 — we link there as the canonical source.

This workflow follows the Airtable → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "0c72eca1-6a9b-4160-953e-2db5fd3a68fd",
      "name": "\ud83d\udce5 Pipeline A \u00b7 Campaign Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        512,
        640
      ],
      "parameters": {
        "color": 7,
        "width": 880,
        "height": 448,
        "content": " *(enroll path)*\n`Parse Enroll Command` *(code \u2014 extracts phone/name/company/campaignId, assigns A/B variant randomly)*.`Airtable \u2013 Create Contact` *(appends row to Contacts table)*.`WATI \u2013 Confirm Enrollment` *(confirms to the sales rep)*.`WATI \u2013 Send Welcome to Contact` *(sends first touch message to the new contact)*"
      },
      "typeVersion": 1
    },
    {
      "id": "ccc5979b-edeb-4ffb-9eaa-409e714736e3",
      "name": "\ud83d\udcca Pipeline D \u00b7 Analytics Dashboard",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        496,
        1168
      ],
      "parameters": {
        "color": 7,
        "width": 968,
        "height": 384,
        "content": "Analytics Dashboard\n**Flow:**\n*(routed from Command Router when sender types `report`)*.`Airtable \u2013 Read All FollowUps`*(lists all FollowUps records)*.`Airtable \u2013 Read All Engagement`*(lists all Engagement records)*.`Build Analytics Report`*(code \u2014 computes all stats and A/B comparison)*.`WATI \u2013 Send Analytics Report`*(sends full dashboard to the requester)*"
      },
      "typeVersion": 1
    },
    {
      "id": "c959b458-4f23-42a2-8cda-45002cce6fad",
      "name": "Wati Trigger",
      "type": "n8n-nodes-wati.watiTrigger",
      "position": [
        -480,
        1472
      ],
      "parameters": {
        "event": "messageReceived"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "37f201d3-163c-40c6-b8a5-0e669c853bb6",
      "name": "Command Router",
      "type": "n8n-nodes-base.switch",
      "position": [
        -64,
        1440
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Enroll Contact",
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "startsWith"
                    },
                    "leftValue": "={{ $json.text }}",
                    "rightValue": "enroll "
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Analytics Report",
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.text.toLowerCase().trim() }}",
                    "rightValue": "report"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Pause Contact",
              "conditions": {
                "options": {
                  "caseSensitive": false,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "startsWith"
                    },
                    "leftValue": "={{ $json.text }}",
                    "rightValue": "pause "
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "a6823936-61cb-484f-9865-fe4030a02c0d",
      "name": "Parse Enroll Command",
      "type": "n8n-nodes-base.code",
      "position": [
        544,
        816
      ],
      "parameters": {
        "jsCode": "// Parse: enroll <phone> <name> <company> <campaignId>\n// Example: enroll 919876543210 Priya Sharma Acme Corp CAMP-001\nconst text = ($json.text || '').trim();\nconst senderPhone = $json.waId || $json.from;\nconst senderName  = $json.senderName || 'Rep';\n\nconst parts = text.replace(/^enroll\\s+/i,'').trim().split(' ');\nif (parts.length < 4) {\n  return [{ json: {\n    senderPhone, senderName, valid: false,\n    errMsg: `\u26a0\ufe0f Invalid format. Use:\\n*enroll <phone> <name> <company> <campaignId>*\\nExample: *enroll 919876543210 Priya Acme CAMP-001*`\n  }}];\n}\n\nconst contactPhone = parts[0];\nconst campaignId   = parts[parts.length - 1];\nconst company      = parts.slice(2, parts.length - 1).join(' ') || 'Unknown';\nconst contactName  = parts[1];\n\n// Assign A/B variant randomly\nconst variant = Math.random() < 0.5 ? 'A' : 'B';\n\nconst now = new Date();\nconst tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1);\n\nreturn [{ json: {\n  senderPhone, senderName, valid: true,\n  contactPhone, contactName, company, campaignId, variant,\n  enrolledAt:    now.toISOString(),\n  nextFollowUp:  tomorrow.toISOString().split('T')[0],\n  confirmMsg: `\u2705 *Contact Enrolled!*\\n\\n\ud83d\udc64 *${contactName}* (${company})\\n\ud83d\udcde ${contactPhone}\\n\ud83c\udfaf Campaign: ${campaignId}\\n\ud83d\udd00 Variant: ${variant}\\n\\n\ud83d\udcc5 First follow-up: tomorrow`\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d823f64e-d2a3-47df-85cd-9024a154f3e7",
      "name": "Airtable \u2013 Create Contact",
      "type": "n8n-nodes-base.airtable",
      "position": [
        800,
        816
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": "appBOslXzqBuBTIv8",
          "cachedResultUrl": "https://airtable.com/appBOslXzqBuBTIv8",
          "cachedResultName": "Campaign Manager"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "tbl7ZtUheEOFOvPEL",
          "cachedResultUrl": "https://airtable.com/appBOslXzqBuBTIv8/tbl7ZtUheEOFOvPEL",
          "cachedResultName": "Contacts Table"
        },
        "columns": {
          "value": {
            "phone": "={{ $json.contactPhone }}",
            "status": "Active",
            "company": "={{ $json.company }}",
            "enrolledAt": "={{ $json.enrolledAt }}",
            "nextFollowUp": "={{ $json.nextFollowUp }}",
            "followUpCount": "=0"
          },
          "schema": [
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "company",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "company",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Campaign Id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Campaign Id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Variant",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Variant",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "followUpCount",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "followUpCount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "nextFollowUp",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "nextFollowUp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "enrolledAt",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "enrolledAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "create"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "1ed3b49c-4e4e-48d2-af08-86c0f85516da",
      "name": "WATI \u2013 Confirm Enrollment",
      "type": "n8n-nodes-wati.wati",
      "position": [
        1040,
        736
      ],
      "parameters": {
        "target": "={{ $('Parse Enroll Command').item.json.senderPhone }}",
        "messageText": "={{ $('Parse Enroll Command').item.json.confirmMsg }}"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "73444670-14cd-4e79-9c26-c8eb0770e60a",
      "name": "WATI \u2013 Send Welcome to Contact",
      "type": "n8n-nodes-wati.wati",
      "position": [
        1056,
        912
      ],
      "parameters": {
        "target": "={{ $('Parse Enroll Command').item.json.contactPhone }}",
        "messageText": "=\ud83d\udc4b Hi *{{ $('Parse Enroll Command').item.json.contactName }}*!\n\nWe wanted to reach out and introduce ourselves. We'll be in touch soon with something relevant to *{{ $('Parse Enroll Command').item.json.company }}*.\n\nFeel free to reply anytime \u2014 we're here to help! \ud83d\ude4c"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "3bb67842-3006-4c5d-a96c-8e1d18857696",
      "name": "Parse Pause Command",
      "type": "n8n-nodes-base.code",
      "position": [
        576,
        1712
      ],
      "parameters": {
        "jsCode": "const text  = ($json.text || '').trim();\nconst phone = text.replace(/^pause\\s+/i,'').trim();\nconst senderPhone = $json.waId || $json.from;\nreturn [{ json: { phone, senderPhone, msg: `\u23f8\ufe0f Contact *${phone}* has been paused.\\nThey will no longer receive follow-ups until resumed.` } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "b71c7a1f-54d3-48c5-8a7f-eac1a84418c4",
      "name": "Airtable \u2013 Pause Contact",
      "type": "n8n-nodes-base.airtable",
      "position": [
        784,
        1712
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": "appBOslXzqBuBTIv8",
          "cachedResultUrl": "https://airtable.com/appBOslXzqBuBTIv8",
          "cachedResultName": "Campaign Manager"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "tbl7ZtUheEOFOvPEL",
          "cachedResultUrl": "https://airtable.com/appBOslXzqBuBTIv8/tbl7ZtUheEOFOvPEL",
          "cachedResultName": "Contacts Table"
        },
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "id",
              "defaultMatch": true
            },
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "company",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "company",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Campaign Id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Campaign Id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Variant",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Variant",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "followUpCount",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "followUpCount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "nextFollowUp",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "nextFollowUp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "enrolledAt",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "enrolledAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "65c89e37-a59d-4090-91fa-69d0976c7698",
      "name": "WATI \u2013 Confirm Pause",
      "type": "n8n-nodes-wati.wati",
      "position": [
        992,
        1712
      ],
      "parameters": {
        "target": "={{ $json.senderPhone }}",
        "messageText": "={{ $json.msg }}"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ac02ba19-d670-4539-95ec-819fd9766525",
      "name": "Schedule Trigger \u2013 9AM Daily",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        2576,
        1504
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 9 * * *"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "d344cbf1-10db-4904-beb8-a7e71e7d3755",
      "name": "Airtable \u2013 Read Active Contacts",
      "type": "n8n-nodes-base.airtable",
      "position": [
        2816,
        1504
      ],
      "parameters": {
        "options": {},
        "resource": "base"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "f9ec16ae-ba7e-45a6-a066-ed29e252a254",
      "name": "Filter Due Follow-ups",
      "type": "n8n-nodes-base.code",
      "position": [
        3056,
        1504
      ],
      "parameters": {
        "jsCode": "// Filter contacts where nextFollowUp <= today AND status = Active\nconst allRows = $input.all();\nconst today   = new Date().toISOString().split('T')[0];\n\nconst due = allRows.filter(r => {\n  const s = (r.json.status || '').toLowerCase();\n  const d = r.json.nextFollowUp || '';\n  return s === 'active' && d <= today;\n});\n\nif (due.length === 0) return [{ json: { message: 'No follow-ups due today', count: 0 } }];\n\nreturn due.map(r => ({ json: {\n  recordId:      r.json.id,\n  contactId:     r.json.id,\n  phone:         r.json.phone,\n  name:          r.json.name,\n  company:       r.json.company,\n  campaignId:    r.json.campaignId,\n  variant:       r.json.variant || 'A',\n  followUpCount: parseInt(r.json.followUpCount || 0),\n  stepNumber:    parseInt(r.json.followUpCount || 0) + 1,\n  lastFollowUp:  r.json.lastFollowUp || null\n}}));"
      },
      "typeVersion": 2
    },
    {
      "id": "fe514b6c-4747-48a3-a1c8-8d2c44461c87",
      "name": "Airtable \u2013 Read Campaign",
      "type": "n8n-nodes-base.airtable",
      "position": [
        3296,
        1504
      ],
      "parameters": {
        "options": {},
        "resource": "base"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "cf1115a6-1b14-4b9b-ae6f-829a48967cc5",
      "name": "OpenAI \u2013 Personalise Follow-up Message",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3536,
        1504
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/chat/completions",
        "method": "POST",
        "options": {},
        "sendHeaders": true,
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "openAiApi"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        },
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d4108fc2-e81a-4203-a3f9-33974c08824e",
      "name": "Build Follow-up Row",
      "type": "n8n-nodes-base.code",
      "position": [
        3776,
        1504
      ],
      "parameters": {
        "jsCode": "// Build the FollowUps log row + compute next follow-up date\nconst contact  = $('Filter Due Follow-ups').item.json;\nconst msgRaw   = $json?.choices?.[0]?.message?.content || '';\nconst message  = msgRaw.trim();\nconst now      = new Date();\n\n// Compute next follow-up: 3 days from today (adjust per campaign intervalDays if available)\nconst allCampaigns = $('Airtable \u2013 Read Campaign').all ? $('Airtable \u2013 Read Campaign').all() : [];\nconst campaign = allCampaigns.find(r => r.json.campaignId === contact.campaignId);\nconst intervalDays = parseInt(campaign?.json?.intervalDays || 3);\n\nconst nextDate = new Date(now);\nnextDate.setDate(nextDate.getDate() + intervalDays);\n\nconst followUpId = `FU-${contact.phone}-${now.getTime()}`;\n\nreturn [{ json: {\n  followUpId,\n  contactId:    contact.contactId,\n  recordId:     contact.recordId,\n  phone:        contact.phone,\n  name:         contact.name,\n  campaignId:   contact.campaignId,\n  variant:      contact.variant,\n  stepNumber:   contact.stepNumber,\n  messageText:  message,\n  sentAt:       now.toISOString(),\n  nextFollowUp: nextDate.toISOString().split('T')[0],\n  newCount:     contact.followUpCount + 1\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f04f5e61-95e0-4d93-a8e6-ff1342f89d77",
      "name": "Airtable \u2013 Log Follow-up Sent",
      "type": "n8n-nodes-base.airtable",
      "position": [
        4016,
        1504
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": "appBOslXzqBuBTIv8",
          "cachedResultUrl": "https://airtable.com/appBOslXzqBuBTIv8",
          "cachedResultName": "Campaign Manager"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "tblSlSYnAeIG2LVA9",
          "cachedResultUrl": "https://airtable.com/appBOslXzqBuBTIv8/tblSlSYnAeIG2LVA9",
          "cachedResultName": "FollowUps Table"
        },
        "columns": {
          "value": {
            "phone": "={{ $json.phone }}",
            "sentAt": "={{ $json.sentAt }}",
            "variant": "={{ $json.variant }}",
            "contactId": "={{ $json.contactId }}",
            "campaignId": "={{ $json.campaignId }}",
            "stepNumber": "={{ $json.stepNumber }}",
            "messageText": "={{ $json.messageText }}"
          },
          "schema": [
            {
              "id": "followUpId",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "followUpId",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "contactId",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "contactId",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "campaignId",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "campaignId",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "variant",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "variant",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "stepNumber",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "stepNumber",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "messageText",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "messageText",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sentAt",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "sentAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "create"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "eb128198-faaa-4640-8cb1-872504057e8e",
      "name": "Airtable \u2013 Update Contact After Send",
      "type": "n8n-nodes-base.airtable",
      "position": [
        4256,
        1504
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": "appBOslXzqBuBTIv8",
          "cachedResultUrl": "https://airtable.com/appBOslXzqBuBTIv8",
          "cachedResultName": "Campaign Manager"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "tbl7ZtUheEOFOvPEL",
          "cachedResultUrl": "https://airtable.com/appBOslXzqBuBTIv8/tbl7ZtUheEOFOvPEL",
          "cachedResultName": "Contacts Table"
        },
        "columns": {
          "value": {
            "nextFollowUp": "={{ $('Build Follow-up Row').item.json.nextFollowUp }}",
            "followUpCount": "={{ $('Build Follow-up Row').item.json.newCount }}"
          },
          "schema": [
            {
              "id": "id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "id",
              "defaultMatch": true
            },
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "company",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "company",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Campaign Id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Campaign Id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Variant",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Variant",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "followUpCount",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "followUpCount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "nextFollowUp",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "nextFollowUp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "enrolledAt",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "enrolledAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "94ca322c-8279-4335-9778-5b4562c85e75",
      "name": "WATI \u2013 Send Follow-up",
      "type": "n8n-nodes-wati.wati",
      "position": [
        4496,
        1504
      ],
      "parameters": {
        "target": "={{ $('Build Follow-up Row').item.json.phone }}",
        "messageText": "={{ $('Build Follow-up Row').item.json.messageText }}"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7d1ee1f8-0451-4a5a-9e6c-0532a9532650",
      "name": "Airtable \u2013 Find Contact by Phone",
      "type": "n8n-nodes-base.airtable",
      "position": [
        528,
        2384
      ],
      "parameters": {
        "options": {},
        "resource": "base"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "83d0dfdc-00aa-4d00-a94f-875ecb2d17cd",
      "name": "Find Contact Record",
      "type": "n8n-nodes-base.code",
      "position": [
        784,
        2384
      ],
      "parameters": {
        "jsCode": "// Find the matching contact record for the inbound phone number\nconst inboundPhone = $('Wati Trigger').item.json.waId || $('Wati Trigger').item.json.from;\nconst allRows = $input.all();\nconst match = allRows.find(r => (r.json.phone||'').replace(/\\D/g,'') === inboundPhone.replace(/\\D/g,''));\n\nif (!match) {\n  return [{ json: { found: false, inboundPhone, replyText: $('Wati Trigger').item.json.text } }];\n}\nreturn [{ json: {\n  found:       true,\n  inboundPhone,\n  recordId:    match.json.id,\n  contactId:   match.json.id,\n  name:        match.json.name,\n  company:     match.json.company,\n  campaignId:  match.json.campaignId,\n  variant:     match.json.variant,\n  replyText:   $('Wati Trigger').item.json.text\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "837581e7-94bf-4f5f-8c79-c76a82a11b51",
      "name": "OpenAI \u2013 Detect Reply Intent",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1024,
        2384
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/chat/completions",
        "method": "POST",
        "options": {},
        "sendHeaders": true,
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "openAiApi"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        },
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "e528ff43-3c9d-4964-a67c-8bdb81664558",
      "name": "Process Reply & Build Log",
      "type": "n8n-nodes-base.code",
      "position": [
        1280,
        2384
      ],
      "parameters": {
        "jsCode": "const intent = ($json?.choices?.[0]?.message?.content || 'other').trim().toLowerCase();\nconst contact = $('Find Contact Record').item.json;\nconst now = new Date();\n\nconst intentEmoji = {\n  interested:     '\ud83d\udc40 Interested!',\n  not_interested: '\ud83d\ude4f Not Interested',\n  question:       '\u2753 Has a Question',\n  converted:      '\ud83c\udf89 CONVERTED!',\n  unsubscribe:    '\ud83d\udeab Unsubscribed',\n  other:          '\ud83d\udcac Other'\n}[intent] || '\ud83d\udcac Other';\n\n// Build response message based on intent\nconst responseMap = {\n  interested:     `\ud83d\ude0a Great to hear, *${contact.name}!* We'll be in touch very soon with more details. Is there anything specific you'd like to know?`,\n  not_interested: `No worries at all, *${contact.name}!* We appreciate your time. Feel free to reach out anytime if things change. Wishing you all the best! \ud83d\udc4b`,\n  question:       `Thanks for reaching out, *${contact.name}!* We'll get someone from our team to answer your question as soon as possible. \ud83d\ude4c`,\n  converted:      `\ud83c\udf89 Wonderful, *${contact.name}!* We're so excited to work with you and *${contact.company}*. Our team will contact you shortly to get started!`,\n  unsubscribe:    `Understood, *${contact.name}.* We've removed you from our follow-up list. No more messages from us! Take care. \ud83d\udc4b`,\n  other:          `Thanks for your message, *${contact.name}!* Our team will review and get back to you shortly.`\n};\n\n// New contact status based on intent\nconst newStatus = { converted: 'Converted', unsubscribe: 'Unsubscribed', not_interested: 'Paused' }[intent] || null;\n\nreturn [{ json: {\n  inboundPhone:  contact.inboundPhone,\n  recordId:      contact.recordId,\n  contactId:     contact.contactId,\n  campaignId:    contact.campaignId,\n  variant:       contact.variant,\n  replyText:     contact.replyText,\n  intent,\n  intentEmoji,\n  repliedAt:     now.toISOString(),\n  responseMsg:   responseMap[intent],\n  newStatus,\n  shouldUpdate:  !!newStatus\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "9d4e616a-535e-4df7-9877-8717920c3b32",
      "name": "Airtable \u2013 Log Engagement",
      "type": "n8n-nodes-base.airtable",
      "position": [
        1456,
        2384
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": "appBOslXzqBuBTIv8",
          "cachedResultUrl": "https://airtable.com/appBOslXzqBuBTIv8",
          "cachedResultName": "Campaign Manager"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "tblQS5jEqJD0sGk9O",
          "cachedResultUrl": "https://airtable.com/appBOslXzqBuBTIv8/tblQS5jEqJD0sGk9O",
          "cachedResultName": "Engagement Table"
        },
        "columns": {
          "value": {
            "intent": "={{ $json.intent }}",
            "contactId": "={{ $json.contactId }}",
            "repliedAt": "={{ $json.repliedAt }}",
            "replyText": "={{ $json.replyText }}"
          },
          "schema": [
            {
              "id": "engagementId",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "engagementId",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "contactId",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "contactId",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "intent",
              "type": "boolean",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "intent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "replyText",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "replyText",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Attachments",
              "type": "array",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Attachments",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "repliedAt",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "repliedAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "create"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "64560c52-0b22-4ac1-b8fa-2f45a5c2cf91",
      "name": "Airtable \u2013 Update Contact Status",
      "type": "n8n-nodes-base.airtable",
      "position": [
        1680,
        2384
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": "appBOslXzqBuBTIv8",
          "cachedResultUrl": "https://airtable.com/appBOslXzqBuBTIv8",
          "cachedResultName": "Campaign Manager"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "tbl7ZtUheEOFOvPEL",
          "cachedResultUrl": "https://airtable.com/appBOslXzqBuBTIv8/tbl7ZtUheEOFOvPEL",
          "cachedResultName": "Contacts Table"
        },
        "columns": {
          "value": {
            "status": "={{ $('Process Reply & Build Log').item.json.newStatus }}",
            "followUpCount": 0
          },
          "schema": [
            {
              "id": "id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "id",
              "defaultMatch": true
            },
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "phone",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "phone",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "company",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "company",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Campaign Id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Campaign Id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Variant",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Variant",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "followUpCount",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "followUpCount",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "nextFollowUp",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "nextFollowUp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "enrolledAt",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "enrolledAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "aa55efda-bd57-4a62-b603-2900e8226366",
      "name": "WATI \u2013 Send Reply Acknowledgement",
      "type": "n8n-nodes-wati.wati",
      "position": [
        1904,
        2384
      ],
      "parameters": {
        "target": "={{ $('Process Reply & Build Log').item.json.inboundPhone }}",
        "messageText": "={{ $('Process Reply & Build Log').item.json.responseMsg }}"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "9d83a298-23b8-4254-b6f6-db9a325ce5d7",
      "name": "Airtable \u2013 Read All Follow-ups",
      "type": "n8n-nodes-base.airtable",
      "position": [
        560,
        1328
      ],
      "parameters": {
        "options": {},
        "resource": "base"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "21cf8238-68c0-4c61-b099-d3a4edd91682",
      "name": "Airtable \u2013 Read All Engagement",
      "type": "n8n-nodes-base.airtable",
      "position": [
        736,
        1328
      ],
      "parameters": {
        "options": {},
        "resource": "base"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "f7cca15c-1900-4555-b489-9950fc335f41",
      "name": "Build Analytics Report",
      "type": "n8n-nodes-base.code",
      "position": [
        944,
        1328
      ],
      "parameters": {
        "jsCode": "// Build A/B analytics + campaign dashboard\n// Reads from both FollowUps and Engagement tables\n\nconst requesterPhone = $('Wati Trigger').item.json.waId || $('Wati Trigger').item.json.from;\nconst requesterName  = $('Wati Trigger').item.json.senderName || 'User';\n\nconst followUps  = $('Airtable \u2013 Read All Follow-ups').all().map(r => r.json);\nconst engagement = $('Airtable \u2013 Read All Engagement').all().map(r => r.json);\n\nconst now = new Date();\n\n// \u2500\u2500 Overall stats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst totalSent    = followUps.length;\nconst totalReplies = engagement.length;\nconst converted    = engagement.filter(e => e.intent === 'converted').length;\nconst unsubscribed = engagement.filter(e => e.intent === 'unsubscribe').length;\nconst replyRate    = totalSent > 0 ? ((totalReplies / totalSent) * 100).toFixed(1) : '0.0';\nconst convRate     = totalSent > 0 ? ((converted    / totalSent) * 100).toFixed(1) : '0.0';\n\n// \u2500\u2500 A/B breakdown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst sentA = followUps.filter(f => f.variant === 'A').length;\nconst sentB = followUps.filter(f => f.variant === 'B').length;\nconst replA = engagement.filter(e => e.variant === 'A').length;\nconst replB = engagement.filter(e => e.variant === 'B').length;\nconst convA = engagement.filter(e => e.variant === 'A' && e.intent === 'converted').length;\nconst convB = engagement.filter(e => e.variant === 'B' && e.intent === 'converted').length;\nconst rrA   = sentA > 0 ? ((replA / sentA) * 100).toFixed(1) : '0.0';\nconst rrB   = sentB > 0 ? ((replB / sentB) * 100).toFixed(1) : '0.0';\nconst crA   = sentA > 0 ? ((convA / sentA) * 100).toFixed(1) : '0.0';\nconst crB   = sentB > 0 ? ((convB / sentB) * 100).toFixed(1) : '0.0';\nconst winner = parseFloat(crA) >= parseFloat(crB) ? '\ud83c\udfc6 Variant A wins on conversion' : '\ud83c\udfc6 Variant B wins on conversion';\n\n// \u2500\u2500 Per-campaign stats \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst byCampaign = {};\nfor (const f of followUps) {\n  const c = f.campaignId || 'Unknown';\n  if (!byCampaign[c]) byCampaign[c] = { sent:0, replies:0, conversions:0 };\n  byCampaign[c].sent++;\n}\nfor (const e of engagement) {\n  const c = e.campaignId || 'Unknown';\n  if (!byCampaign[c]) byCampaign[c] = { sent:0, replies:0, conversions:0 };\n  byCampaign[c].replies++;\n  if (e.intent === 'converted') byCampaign[c].conversions++;\n}\n\nconst campLines = Object.entries(byCampaign).map(([id, s]) => {\n  const rr = s.sent > 0 ? ((s.replies/s.sent)*100).toFixed(0) : 0;\n  return `  \ud83d\udcc1 *${id}* \u2014 Sent: ${s.sent} | Replies: ${s.replies} (${rr}%) | Conv: ${s.conversions}`;\n});\n\n// \u2500\u2500 Recent replies \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst intentEmoji = { interested:'\ud83d\udc40',not_interested:'\ud83d\ude4f',question:'\u2753',converted:'\ud83c\udf89',unsubscribe:'\ud83d\udeab',other:'\ud83d\udcac' };\nconst recentReplies = engagement\n  .sort((a,b) => new Date(b.repliedAt) - new Date(a.repliedAt))\n  .slice(0,5)\n  .map(e => {\n    const dt = e.repliedAt ? new Date(e.repliedAt).toLocaleDateString('en-IN',{day:'numeric',month:'short',hour:'2-digit',minute:'2-digit'}) : '';\n    return `  ${intentEmoji[e.intent]||'\ud83d\udcac'} ${e.phone} \u2014 ${e.intent} (${dt})`;\n  });\n\n// \u2500\u2500 Build message \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst lines = [\n  `\ud83d\udcca *Follow-up Campaign Analytics*`,\n  `\ud83d\udc64 ${requesterName} \u00b7 ${now.toLocaleDateString('en-IN',{day:'numeric',month:'short',year:'numeric'})}`,\n  '',\n  `\u2501\u2501 *Overall Performance* \u2501\u2501`,\n  `\ud83d\udce4 Total Sent:      *${totalSent}*`,\n  `\ud83d\udcac Total Replies:   *${totalReplies}* (${replyRate}%)`,\n  `\ud83c\udf89 Conversions:    *${converted}* (${convRate}%)`,\n  `\ud83d\udeab Unsubscribed:   *${unsubscribed}*`,\n  '',\n  `\u2501\u2501 *A/B Test Results* \u2501\u2501`,\n  `\ud83c\udd70\ufe0f Variant A \u2014 Sent: ${sentA} | Reply: ${rrA}% | Conv: ${crA}%`,\n  `\ud83c\udd71\ufe0f Variant B \u2014 Sent: ${sentB} | Reply: ${rrB}% | Conv: ${crB}%`,\n  winner,\n  '',\n  `\u2501\u2501 *By Campaign* \u2501\u2501`,\n  ...campLines,\n  '',\n  `\u2501\u2501 *Recent Replies* \u2501\u2501`,\n  ...recentReplies,\n  '',\n  'Reply *enroll* to add a contact \u2014 *pause* to pause one.'\n];\n\nreturn [{ json: { requesterPhone, report: lines.join('\\n') } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "ffa4b9e0-0301-4d52-b919-23ba426106e9",
      "name": "WATI \u2013 Send Analytics Report",
      "type": "n8n-nodes-wati.wati",
      "position": [
        1200,
        1328
      ],
      "parameters": {
        "target": "={{ $json.requesterPhone }}",
        "messageText": "={{ $json.report }}"
      },
      "credentials": {
        "watiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ba01ef3f-db26-4af1-b0ad-754a96b5d4f6",
      "name": "Pipeline B Guide",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2544,
        1264
      ],
      "parameters": {
        "color": 7,
        "width": 2168,
        "height": 448,
        "content": "### \u23f0 Pipeline B: Follow-up Scheduler\n**Nodes:** `9AM Trigger` \u2192 `Read Contacts` \u2192 `OpenAI Message` \u2192 `Update Contact`.\n\n**Function:**\n1. **Filtering:** Lists all Airtable contacts where `Status=Active` and `NextFollowUp <= Today`.\n2. **AI Writer:** OpenAI generates a custom follow-up message using the contact's name, company, and assigned A/B tone (Formal vs. Friendly).\n3. **CRM Sync:** Increments the follow-up counter and schedules the next touchpoint date."
      },
      "typeVersion": 1
    },
    {
      "id": "93b4c274-97a6-4c09-b68a-b5bbb0177023",
      "name": "Pipeline C Guide",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        512,
        2064
      ],
      "parameters": {
        "color": 7,
        "width": 1592,
        "height": 640,
        "content": "Engagement Tracker\n**Nodes:** `Reply Trigger` \u2192 `Find Record` \u2192 `OpenAI Intent` \u2192 `Log Engagement`.\n\n**Function:**\n1. **Identification:** Matches the sender's WhatsApp number to an existing lead in Airtable.\n2. **AI Intent:** OpenAI classifies the reply (e.g., 'Interested', 'Question', 'Unsubscribe').\n3. **Action:** If the lead is 'Converted' or 'Unsubscribed', the bot automatically pauses further follow-ups to save your reputation."
      },
      "typeVersion": 1
    },
    {
      "id": "f21df589-0287-4a0c-8dac-0d47b427e737",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        512,
        1600
      ],
      "parameters": {
        "color": 7,
        "width": 944,
        "height": 400,
        "content": "Branch: Pause Outreach\nHow it works:Command Parsing: Extracts the target phone number from the WhatsApp command and prepares the confirmation text.\nCRM Update: Updates the lead's status to 'Paused' in Airtable to exclude them from the 9 AM automated follow-up scheduler.\nConfirmation: Sends a WhatsApp alert back to the representative confirming that automated outreach for that lead has stopped."
      },
      "typeVersion": 1
    },
    {
      "id": "94279b21-975b-49dc-8391-dadd8c8dbc28",
      "name": "\ud83d\udccc Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1696,
        -32
      ],
      "parameters": {
        "width": 820,
        "height": 680,
        "content": "\ud83d\udce3 WhatsApp Follow-up Campaign Manager\nWorkflow Purpose\nAutomates personalized outreach via WATI, lead management in Airtable, and AI-driven messaging with OpenAI. Supports A/B testing and real-time performance analytics.\n\n\ud83d\ude80 Core Pipelines\nCampaign Setup: Enrolls contacts and assigns A/B variants via WhatsApp commands.\n\nFollow-up Scheduler: Daily 9 AM trigger for AI-personalized follow-ups based on lead variant.\n\nReply Tracker: Detects reply intent (e.g., Interested, Unsubscribe) and auto-updates CRM status.\n\nAnalytics Dashboard: Generates on-demand A/B stats and conversion reports via the report command.\n\n\ud83d\udd27 Setup Essentials\nRequired Apps\n\nWATI: Triggers and sends WhatsApp communications.\n\nAirtable: Acts as the central lead and campaign CRM.\n\nOpenAI: Handles message personalization and intent classification."
      },
      "typeVersion": 1
    },
    {
      "id": "d3565254-6cb9-4c8a-b442-b5997c6f9c09",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -592,
        992
      ],
      "parameters": {
        "color": 7,
        "width": 880,
        "height": 1120,
        "content": "Branch: Command Routing\nNodes: Wati Trigger \u2192 Command Router\n\nHow it works:\n\nCentral Intake: The Wati Trigger listens for all incoming WhatsApp messages and passes the text to the router.\n\nIntent Detection: The Command Router (Switch node) acts as the traffic controller, sorting messages into four distinct paths based on keywords:\n\nEnrollment: Messages starting with enroll .\n\nAnalytics: Messages exactly matching report.\n\nManual Override: Messages starting with pause .\n\nEngagement Tracking: All other replies (via the extra output)."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Wati Trigger": {
      "main": [
        [
          {
            "node": "Command Router",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Command Router": {
      "main": [
        [
          {
            "node": "Parse Enroll Command",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Airtable \u2013 Read All Follow-ups",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Parse Pause Command",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Airtable \u2013 Find Contact by Phone",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Follow-up Row": {
      "main": [
        [
          {
            "node": "Airtable \u2013 Log Follow-up Sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Contact Record": {
      "main": [
        [
          {
            "node": "OpenAI \u2013 Detect Reply Intent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Pause Command": {
      "main": [
        [
          {
            "node": "Airtable \u2013 Pause Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Enroll Command": {
      "main": [
        [
          {
            "node": "Airtable \u2013 Create Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Due Follow-ups": {
      "main": [
        [
          {
            "node": "Airtable \u2013 Read Campaign",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Analytics Report": {
      "main": [
        [
          {
            "node": "WATI \u2013 Send Analytics Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Reply & Build Log": {
      "main": [
        [
          {
            "node": "Airtable \u2013 Log Engagement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable \u2013 Pause Contact": {
      "main": [
        [
          {
            "node": "WATI \u2013 Confirm Pause",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable \u2013 Read Campaign": {
      "main": [
        [
          {
            "node": "OpenAI \u2013 Personalise Follow-up Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable \u2013 Create Contact": {
      "main": [
        [
          {
            "node": "WATI \u2013 Confirm Enrollment",
            "type": "main",
            "index": 0
          },
          {
            "node": "WATI \u2013 Send Welcome to Contact",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable \u2013 Log Engagement": {
      "main": [
        [
          {
            "node": "Airtable \u2013 Update Contact Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
  

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Maximize your conversion rates with this end-to-end automated outreach and lead nurturing system. This workflow manages the entire sales lifecycle—from instant contact enrollment via WhatsApp to AI-personalized follow-ups and comprehensive A/B performance tracking—all integrated…

Source: https://n8n.io/workflows/13723/ — original creator credit. Request a take-down →

More Marketing & Ads workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Marketing & Ads

Automate your lead generation by scraping targeted prospects from Apollo.io, enriching with contact details, and seamlessly syncing to Airtable for organized outreach—all without manual data entry.

Airtable, HTTP Request
Marketing & Ads

This workflow is designed to take user inputs in order to generate an image using the Riverflow 2.0 model through the Replicate API. It can handle both image generation as well as image editing. Addit

Form Trigger, Data Table, HTTP Request +1
Marketing & Ads

Content Analyzer (Tiktok). Uses stickyNote, scheduleTrigger, sort, airtable. Scheduled trigger; 27 nodes.

Airtable, HTTP Request, Google Drive
Marketing & Ads

AI Lead Qualification & Roting System. Uses httpRequest, twilio, airtable. Webhook trigger; 26 nodes.

HTTP Request, Twilio, Airtable
Marketing & Ads

Viral TikToks. Uses httpRequest, airtable, scheduleTrigger, splitInBatches. Scheduled trigger; 25 nodes.

HTTP Request, Airtable