{
  "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": []
}