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 →
{
"createdAt": "2025-08-24T18:22:48.481Z",
"updatedAt": "2025-10-12T12:36:24.000Z",
"id": "bZoHiEBDbW5RY1rV",
"name": "Follow-up Sender: Whatsapp Companion",
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 10
}
]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
-1232,
240
],
"id": "2fdb8d21-8726-4f11-8576-e81ab7e1affd",
"name": "Schedule Trigger",
"notes": "Purpose: Kick off the sender periodically.\nUse: Cron/interval; keep conservative while validating.\nMeta: Stateless; all gating happens downstream."
},
{
"parameters": {
"options": {
"reset": false
}
},
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
-800,
240
],
"id": "efbc3f42-bf90-44d1-979e-cfb889712598",
"name": "Loop Over Items",
"notes": "Purpose: Process follow-ups one-by-one.\nUse: \u201cLoop\u201d output drives the chain; all exits return to the Loop\u2019s input.\nMeta: Idempotent per item; set \u201cReset\u201d off for scheduled runs. "
},
{
"parameters": {
"jsCode": "// --- language helpers ---\nfunction mapToLanguageCode(lang) {\n const l = String(lang || '').toLowerCase().replace('_','-');\n const map = {\n 'en':'en','en-us':'en','en-gb':'en',\n 'es':'es','es-es':'es','es-mx':'es',\n 'it':'it','fr':'fr','de':'de','pt':'pt','pt-br':'pt','fa':'fa'\n };\n return map[l] || 'en';\n}\n\n// --- declare what each template needs and in what order ---\nconst TEMPLATE_REQUIREMENTS = {\n check_in_generic: ['topic', 'child_name'],\n sleep_checkin_morning:['child_name'],\n success_celebration: ['child_name'],\n voice_suggestion: ['child_name'],\n open_loop_nudge: ['topic'],\n error_fallback: ['topic'],\n conversion_invite: ['topic'], // optional header handled below\n};\n\nconst row = $json;\nconst p = row.parameters || {};\nconst name = row.template_name;\n\nconst required = TEMPLATE_REQUIREMENTS[name] || [];\n\nconst bodyParameters = [];\nconst missing = [];\n\nfor (const key of required) {\n const value = (p[key] ?? '').toString().trim();\n if (!value) missing.push(key);\n // IMPORTANT: include parameter_name for named-variable templates\n bodyParameters.push({\n type: 'text',\n text: value || '',\n parameter_name: key,\n });\n}\n\n// optional header image for specific templates\nlet headerImageLink = null;\nif (name === 'conversion_invite' && p.header_image_url) {\n headerImageLink = String(p.header_image_url);\n}\n\nconst languageCode = mapToLanguageCode(row.language);\n\n// Build the exact payload WhatsApp requires\nconst waBody = {\n messaging_product: 'whatsapp',\n to: row.phone_number, // E.164 digits\n type: 'template',\n template: {\n name,\n language: { code: languageCode },\n components: [\n ...(headerImageLink ? [{\n type: 'header',\n parameters: [{ type: 'image', image: { link: headerImageLink } }]\n }] : []),\n { type: 'body', parameters: bodyParameters },\n ],\n },\n};\n\nreturn [{\n json: {\n followup_id: row.id,\n phone_number: row.phone_number,\n template_name: name,\n languageCode,\n bodyParameters,\n headerImageLink,\n is_valid: missing.length === 0,\n validation_error: missing.length ? `Missing required variables: ${missing.join(', ')}` : null,\n waBody, // <- Send node uses {{$json.waBody}}\n }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-544,
256
],
"id": "bc54aba8-0af6-4538-9f60-636b43168b96",
"name": "Build WA payload + locale",
"notes": "Purpose: Validate required variables and build exact WhatsApp payload.\nUse: Emits { waBody, is_valid, validation_error, followup_id, \u2026 }.\nMeta: Named parameters (parameter_name) match template vars; optional header image supported; language normalized."
},
{
"parameters": {
"method": "POST",
"url": "https://idewetjagvjpdjvqlpvl.supabase.co/rest/v1/rpc/mark_followup_sent",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"p_id\": \"{{$item(0).$node['Merge clamp + payload'].json.followup_id || $item(0).$node['Build WA payload + locale'].json.followup_id}}\",\n \"p_provider_id\": \"{{$json.messages?.[0]?.id || $json.id || null}}\"\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
672,
224
],
"id": "38dbfc67-b3c6-4dea-8581-d7491951ed48",
"name": "Supabase: mark_followup_sent",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
},
"notes": "Purpose: Mark success with provider message id.\nUse: Body { p_id: <followup_id>, p_provider_id: <WA msg id> }.\nMeta: Increments attempts, stamps last_attempt_at, clears last_error, stores provider_msg_id. "
},
{
"parameters": {
"method": "POST",
"url": "https://idewetjagvjpdjvqlpvl.supabase.co/rest/v1/rpc/fetch_due_followups",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { p_now: $now.toISO(), p_limit: 50 } }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-1024,
240
],
"id": "45cebcf8-3328-4b10-b12d-ce1bb87e8e2e",
"name": "Supabase: fetch_due_followups",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
},
"notes": "Purpose: Pull due + unsent follow-ups.\nUse: Body { p_now: $now.toISO(), p_limit: <n> }.\nMeta: Returns rows with parameters JSON; limit controls run cost. "
},
{
"parameters": {
"method": "POST",
"url": "https://idewetjagvjpdjvqlpvl.supabase.co/rest/v1/rpc/mark_followup_failed",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "=={\n p_id: \"{{$item(0).$node['Merge clamp + payload'].json.followup_id || $json.followup_id}}\",\n p_error: \"={{ ($json.error?.description || $json.error?.message || 'send failed') }}\"\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
672,
448
],
"id": "0de6426e-b11a-4342-a65b-2c2e5f58b60a",
"name": "mark_followup_failed",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
},
"notes": "Purpose: Persist failure and bump attempts.\nUse: Body { p_id, p_error } where p_error comes from WA error or validation.\nMeta: Truncate error in RPC (e.g., LEFT(\u2026,500)). Returns nothing; flow returns to Loop."
},
{
"parameters": {
"method": "POST",
"url": "https://graph.facebook.com/v22.0/742665365605812/messages",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "whatsAppApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.waBody }}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
464,
240
],
"id": "3a7fc692-0f90-41e6-828b-134f2250be48",
"name": "Send Message",
"retryOnFail": true,
"credentials": {
"whatsAppApi": {
"name": "<your credential>"
}
},
"onError": "continueErrorOutput",
"notes": "Purpose: Send the template message.\nUse: Body is {{$json.waBody}} built upstream.\nMeta: Retries on fail; Success \u2192 messages[0].id captured. Error branch continues to \u201cfailed\u201d. "
},
{
"parameters": {
"method": "POST",
"url": "=https://idewetjagvjpdjvqlpvl.supabase.co/rest/v1/rpc/followup_clamp_inputs",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "supabaseApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"p_phone\": \"={{ $json.phone_number }}\",\n \"p_cutoff_2h\": \"={{ $now.minus({ hours: 2 }).toISO() }}\",\n \"p_cutoff_1d\": \"={{ $now.minus({ days: 1 }).toISO() }}\",\n \"p_cutoff_7d\": \"={{ $now.minus({ days: 7 }).toISO() }}\"\n}",
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-368,
64
],
"id": "1ae4d42f-e7ad-4087-9053-315119e826d4",
"name": "Supabase: Clamp inputs (RPC)",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
},
"notes": "Purpose: Anti-spam guard \u2014 recent inbound / sent in last 1d/7d.\nUse: Body { p_phone, p_cutoff_2h, p_cutoff_1d, p_cutoff_7d }.\nMeta: Returns booleans/counters for gating."
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "803dfd99-bf6a-4040-84c3-a108dc58289a",
"leftValue": "={{ $json.recent_inbound || $json.sent_1d > 0 || $json.sent_7d > 0 }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
-16,
240
],
"id": "fc7938da-dc51-4e12-abad-226cf6c8cf71",
"name": "Skip due to recent reply?",
"notes": "Purpose: Drop sends if recent inbound or too many in last 1d/7d.\nUse: Condition: recent_inbound || sent_1d>0 || sent_7d>0.\nMeta: True \u2192 loop next item; False \u2192 continue. "
},
{
"parameters": {
"mode": "combine",
"combineBy": "combineByPosition",
"options": {}
},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
-208,
240
],
"id": "62db951d-b9ee-491a-b42c-642023a86cc0",
"name": "Merge clamp + payload",
"notes": "Purpose: Combine validation + clamp data into one item.\nUse: Combine by position (Input1 = clamp, Input2 = payload).\nMeta: Keeps structure stable for downstream IFs"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "3dc13156-1892-48d6-966e-76e2d6ed39c1",
"leftValue": "={{ /^(\\+?\\d+)$/.test($json.phone_number) }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
240,
256
],
"id": "3719138e-82c4-4a3d-b81c-5319ee190131",
"name": "Is WhatsApp number?",
"notes": "Purpose: Ensure phone_number is E.164 digits.\nUse: Regex: /^(\\+?\\d+)$/.\nMeta: False \u2192 mark_failed with reason; True \u2192 send. "
}
],
"connections": {
"Schedule Trigger": {
"main": [
[
{
"node": "Supabase: fetch_due_followups",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[],
[
{
"node": "Build WA payload + locale",
"type": "main",
"index": 0
}
]
]
},
"Build WA payload + locale": {
"main": [
[
{
"node": "Merge clamp + payload",
"type": "main",
"index": 1
},
{
"node": "Supabase: Clamp inputs (RPC)",
"type": "main",
"index": 0
}
]
]
},
"Supabase: fetch_due_followups": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Supabase: mark_followup_sent": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Send Message": {
"main": [
[
{
"node": "Supabase: mark_followup_sent",
"type": "main",
"index": 0
}
],
[
{
"node": "mark_followup_failed",
"type": "main",
"index": 0
}
]
]
},
"Supabase: Clamp inputs (RPC)": {
"main": [
[
{
"node": "Merge clamp + payload",
"type": "main",
"index": 0
}
]
]
},
"Skip due to recent reply?": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
],
[
{
"node": "Is WhatsApp number?",
"type": "main",
"index": 0
}
]
]
},
"Merge clamp + payload": {
"main": [
[
{
"node": "Skip due to recent reply?",
"type": "main",
"index": 0
}
]
]
},
"Is WhatsApp number?": {
"main": [
[
{
"node": "Send Message",
"type": "main",
"index": 0
}
],
[
{
"node": "mark_followup_failed",
"type": "main",
"index": 0
}
]
]
},
"mark_followup_failed": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": {
"node:Schedule Trigger": {
"recurrenceRules": []
}
},
"meta": {
"templateCredsSetupCompleted": true
},
"versionId": "bfe787a6-74d1-40a3-a2d4-c9ccab09fc4a",
"triggerCount": 1,
"shared": [
{
"createdAt": "2025-08-24T18:22:48.485Z",
"updatedAt": "2025-08-24T18:22:48.485Z",
"role": "workflow:owner",
"workflowId": "bZoHiEBDbW5RY1rV",
"projectId": "O4WbEo8M4wdnIuwK",
"project": {
"createdAt": "2025-08-14T08:13:10.107Z",
"updatedAt": "2025-08-14T08:13:13.212Z",
"id": "O4WbEo8M4wdnIuwK",
"name": "Koosha Najmi <koosha@bubuapp.ai>",
"type": "personal",
"icon": null,
"description": null,
"projectRelations": [
{
"createdAt": "2025-08-14T08:13:10.107Z",
"updatedAt": "2025-08-14T08:13:10.107Z",
"userId": "00345f41-7293-4394-9edc-d95d972c1462",
"projectId": "O4WbEo8M4wdnIuwK",
"user": {
"createdAt": "2025-08-14T08:13:08.631Z",
"updatedAt": "2025-10-12T08:12:04.000Z",
"id": "00345f41-7293-4394-9edc-d95d972c1462",
"email": "koosha@bubuapp.ai",
"firstName": "Koosha",
"lastName": "Najmi",
"personalizationAnswers": null,
"settings": {
"userActivated": true,
"easyAIWorkflowOnboarded": true,
"firstSuccessfulWorkflowId": "tjk89cBgzinji59E",
"userActivatedAt": 1755208424155,
"npsSurvey": {
"responded": true,
"lastShownAt": 1755544077552
}
},
"disabled": false,
"mfaEnabled": false,
"lastActiveAt": "2025-10-11",
"isPending": false
}
}
]
}
}
],
"tags": []
}
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.
supabaseApiwhatsAppApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Follow-up Sender: Whatsapp Companion. Uses httpRequest. Scheduled trigger; 11 nodes.
Source: https://gist.github.com/kooshanajmi86/46b74d6b3020c5ecbc11686fad7eeb87 — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Auto Hunt. Uses httpRequest, googleSheets, rssFeedRead, telegram. Scheduled trigger; 78 nodes.
Auto-Hunt. Uses httpRequest, googleSheets, rssFeedRead, telegram. Scheduled trigger; 78 nodes.
debug. Uses httpRequest, slack, redis, mailgun. Scheduled trigger; 60 nodes.
Seller Follow-Up Engine (Enhanced). Uses httpRequest, slack. Scheduled trigger; 44 nodes.
This workflow runs daily at 9 AM, uses Perplexity to compile vaping industry news, optionally captures article screenshots via Browserless, and generates a HeyGen avatar video from a template. It then