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.
This workflow is an automated employee time tracking and reporting system that monitors weekly work hours via TMetric, then delivers personalized summaries directly to each team member on Slack. It co
Import Productboard Notes Companies And Features Into Snowflake. Uses stickyNote, httpRequest, splitOut, snowflake. Scheduled trigger; 35 nodes.
Import Productboard Notes, Companies and Features into Snowflake. Uses stickyNote, httpRequest, splitOut, snowflake. Scheduled trigger; 35 nodes.
This workflow imports Productboard data into Snowflake, automating data extraction, mapping, and updates for features, companies, and notes. It supports scheduled weekly updates, data cleansing, and S
Elevate your shopping experience with an AI-driven personal assistant that lives right in your WhatsApp. This template automates the entire lifecycle of a shopping list—from intelligent intake and liv