{
  "createdAt": "2025-09-03T21:28:03.668Z",
  "updatedAt": "2025-09-25T14:00:18.000Z",
  "id": "3kXvp0MLl2zK66C9",
  "name": "Bubu Telegram Companion",
  "active": true,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "telegram",
        "options": {
          "rawBody": true,
          "responseData": "={ \"ok\": true }"
        }
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        64,
        0
      ],
      "id": "f85ffec8-50d6-4f1f-9c59-2e4fe360becc",
      "name": "Webhook"
    },
    {
      "parameters": {
        "jsCode": "const cfg = $item(0).$node[\"Bot config\"].json || {};\nconst BOT_USERNAME = (cfg.bot_username || \"\").trim();\nconst PRIVACY_MODE = (cfg.privacy_mode || \"on\").toLowerCase();\nconst upd = $json.body || $json;\n\n// 1) Ignore non-message updates we don't handle (channel posts, service signals)\nconst msg = upd.message || upd.edited_message || null;\nif (!msg || !msg.chat) {\n  return [{ json: { skip: true, reason: 'no_message_or_chat' } }];\n}\n\n// 2) Basics\nconst chat = msg.chat;\nconst from = msg.from || {};\nconst isGroup = chat.type === 'group' || chat.type === 'supergroup';\nconst isPrivate = chat.type === 'private';\nconst isTopic = !!msg.is_topic_message;\nconst threadId = isTopic ? (msg.message_thread_id || null) : null;\n\n// 3) Content + type (text/voice); keep caption fallback for media\nconst text = msg.text || msg.caption || '';\nconst isVoice = !!msg.voice;\nconst mediaFileId = isVoice ? msg.voice.file_id : null;\nconst audioSeconds = isVoice ? (msg.voice.duration || null) : null;\n\n// 4) Mention detection for Privacy Mode (looks at @mentions in entities)\nlet mentioned = false;\nif (BOT_USERNAME && text && Array.isArray(msg.entities)) {\n  mentioned = msg.entities.some(e => {\n    if (e.type !== 'mention') return false;\n    const slice = text.substring(e.offset, e.offset + e.length);\n    return slice.toLowerCase() === ('@' + BOT_USERNAME).toLowerCase();\n  });\n}\n\n// 5) Commands (optional): surface \"/start\" and similar\nlet command = null;\nif (Array.isArray(msg.entities)) {\n  const cmdEnt = msg.entities.find(e => e.type === 'bot_command');\n  if (cmdEnt) {\n    command = text.substring(cmdEnt.offset, cmdEnt.offset + cmdEnt.length);\n  }\n}\n\n// 6) Reply context (optional)\nconst replyToId = msg.reply_to_message ? String(msg.reply_to_message.message_id) : null;\n\n// 7) Build normalized output\nconst out = {\n  kind: 'message',\n  chat_type: String(chat.type),\n  telegram_chat_id: String(chat.id),\n  telegram_user_id: String(from.id || ''),\n  telegram_message_id: String(msg.message_id),\n  thread_id: threadId,\n  reply_to_message_id: replyToId,\n  type: isVoice ? 'voice' : 'text',\n  content: text || (isVoice ? '[Voice message]' : ''),\n  is_voice: isVoice,\n  media_file_id: mediaFileId,\n  audio_seconds: audioSeconds,\n  transcription_confidence: null,\n  mentioned,\n  privacy_mode: PRIVACY_MODE,\n  command,\n  timestamp: Number.isFinite(msg.date) ? msg.date : Math.floor(Date.now()/1000),\n  lang: from.language_code || null\n};\n\nreturn [{ json: out }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        416,
        0
      ],
      "id": "3df3050b-addf-4450-889f-9c927dc6d869",
      "name": "Parse update"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "082606cf-3c83-4a0e-bced-41743f09833c",
              "leftValue": "={{$json.is_voice}}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1040,
        0
      ],
      "id": "bda262d6-7699-41eb-be64-d6bee573fe3d",
      "name": "is_voice?"
    },
    {
      "parameters": {
        "url": "={{ 'https://api.telegram.org/file/bot' + $env.TELEGRAM_BOT_TOKEN + '/' + $node[\"TG getFile\"].json.result.file_path }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1488,
        -112
      ],
      "id": "75009340-f049-4628-a1cc-d4618d5ebbc5",
      "name": "TG download"
    },
    {
      "parameters": {
        "resource": "audio",
        "operation": "transcribe",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        1696,
        -112
      ],
      "id": "bf42d40b-14da-4fad-8a66-70a73aad4fc6",
      "name": "Transcribe a recording",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const parse = $node[\"Parse update\"].json;\nconst tr = $json;\n\nconst text =\n  (tr.text ?? tr.data?.text ?? tr.output?.text ?? tr.choices?.[0]?.message?.content ?? \"\").toString();\n\nreturn [{\n  json: {\n    kind: 'message',\n    telegram_user_id: parse.telegram_user_id,\n    telegram_chat_id: parse.telegram_chat_id,\n    telegram_message_id: parse.telegram_message_id,\n    thread_id: parse.thread_id || null,\n    type: \"text\",\n    content: text.trim(),\n    is_voice: true,\n    media_file_id: parse.media_file_id || null,\n    transcription_confidence: null,\n    audio_seconds: tr.usage?.seconds ?? tr.duration ?? tr.data?.duration ?? null,\n    timestamp: parse.timestamp\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1904,
        -112
      ],
      "id": "8f7c4a84-d3d0-4246-b60d-56d8fa30e1ce",
      "name": "Build transcribed message"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://idewetjagvjpdjvqlpvl.supabase.co/rest/v1/rpc/get_context",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{\n  $node[\"Parse update\"].json.chat_type === \"private\"\n    ? { \"p_phone\": \"tg:\" + $node[\"Parse update\"].json.telegram_user_id }\n    : { \"p_phone\": \"tg_group:\" + $node[\"Parse update\"].json.telegram_chat_id }\n}}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2384,
        16
      ],
      "id": "8043a41e-76f4-4199-b17b-d440538f9a2e",
      "name": "Supabase: get_context",
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "language": "JavaScript",
        "jsCode": "// --- ctx from Supabase:get_context ---\nconst ctx = $json;\n\n// Normalize legacy key\nif (ctx.failed_slugs && !ctx.failed_by_category) {\n  ctx.failed_by_category = ctx.failed_slugs;\n  delete ctx.failed_slugs;\n}\n\n// --- parse payload (webhook) ---\nconst parse = $node['Parse update'].json; \n// parse has: telegram_user_id, telegram_message_id, is_voice, content, (optional) thread_id, etc.\n\n// --- choose message text (DB-first, match on message id) ---\nconst msgs = Array.isArray(ctx.recent_messages) ? ctx.recent_messages : [];\n\n// helper: does a DB message match this inbound Telegram message?\nfunction matchesInbound(m, parse) {\n  const isInbound = (m.direction === 'in' || m.direction === 'inbound' || m.is_inbound === true);\n\n  // We currently store TG ids in whatsapp_id for compatibility.\n  const byWACompat = (!!m.whatsapp_id && !!parse.telegram_message_id && m.whatsapp_id === String(parse.telegram_message_id));\n\n  // If you later add telegram_message_id to messages, this will start working too.\n  const byTGField = (!!m.telegram_message_id && !!parse.telegram_message_id && m.telegram_message_id === String(parse.telegram_message_id));\n\n  // Some dumps used 'wamid' \u2014 keep as last resort.\n  const byWamid = (!!m.wamid && !!parse.telegram_message_id && m.wamid === String(parse.telegram_message_id));\n\n  return isInbound && (byWACompat || byTGField || byWamid);\n}\n\n// try exact match by Telegram message id\nconst inboundExact = [...msgs].reverse().find(m => matchesInbound(m, parse));\n\n// fallback: last inbound message in the window\nconst inboundLast = inboundExact || [...msgs].reverse().find(m =>\n  (m.direction === 'in' || m.direction === 'inbound' || m.is_inbound === true)\n) || null;\n\n// prefer DB text (already the transcript for voice), else webhook content\nconst dbText = inboundLast?.content ?? inboundLast?.text ?? null;\nconst text = String(dbText ?? parse.content ?? '').trim();\n\n// --- children / language ---\nconst children = Array.isArray(ctx.children) ? ctx.children : [];\nconst lang = ctx.user?.preferred_language || 'en';\n\n// --- pick active child ---\nlet active = null;\nif (children.length === 1) {\n  active = children[0];\n} else if (children.length > 1) {\n  const lower = text.toLowerCase();\n  const hit = children.find(c => lower.includes(String(c.name || '').toLowerCase()));\n  if (hit) active = hit;\n}\nif (!active && ctx.user?.last_active_child_id) {\n  active = children.find(c => c.id === ctx.user.last_active_child_id) || null;\n}\nconst active_child_hint = active ? { id: active.id, name: active.name } : null;\n\n// --- build orchestrator_input ---\nconst orchestrator_input = {\n  lang,\n  children: children.map(c => ({\n    id: c.id,\n    name: String(c.name || '').trim(),\n    date_of_birth: c.date_of_birth || null,\n  })),\n  last_active_child_id: ctx.user?.last_active_child_id || null,\n  active_child_hint,\n  recent_messages: ctx.recent_messages || [],\n  recent_interventions: ctx.recent_interventions || [],\n  history_summary: ctx.user?.history_summary || null,\n  failed_by_category: Array.isArray(ctx.failed_by_category) ? ctx.failed_by_category : [],\n  message: text,\n  is_voice: !!parse.is_voice,\n};\n\n// --- heuristics ---\nconst msgLen = text.length;\nconst recentTurns = msgs.length;\nconst conf = inboundLast?.transcription_confidence; // often undefined with your current model\norchestrator_input.offer_voice_note = (msgLen >= 320) || (recentTurns >= 12);\norchestrator_input.voice_low_confidence = (typeof conf === 'number') && (conf < 0.7);\n\n// --- identity for downstream (reuse p_phone contract with tg:<id>) ---\nconst phone_key = 'tg:' + String(parse.telegram_user_id);\n\n// return\nreturn [{\n  json: {\n    phone_number: phone_key, // keeps RPC compatibility\n    orchestrator_input,\n  }\n}];"
      },
      "id": "0cf4354b-54b5-4290-b91e-951293767108",
      "name": "Resolve child + build",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        2592,
        16
      ]
    },
    {
      "parameters": {
        "language": "JavaScript",
        "jsCode": "const ctx = $node['Supabase: get_context'].json;\nreturn [{\n  json: {\n    draft: $node['OpenAI: Orchestrator'].json, // be explicit\n    context: {\n      children: ctx.children || [],\n      last_active_child_id: ctx.user?.last_active_child_id || null,\n      failed_by_category: ctx.failed_by_category || [],\n      language: ctx.user?.preferred_language || 'en',\n      history_summary: ctx.user?.history_summary || null\n    }\n  }\n}];"
      },
      "id": "7c545bcd-4cd9-4b6f-ad91-ca486a8dec0e",
      "name": "Verify draft",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        3056,
        16
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://idewetjagvjpdjvqlpvl.supabase.co/rest/v1/rpc/apply_updates",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{\n  {\n    \"p_phone\": $json.p_phone,\n    \"p_updates\": $json.p_updates\n  }\n}}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "id": "bf5a7bc8-1227-46b2-8d54-5368c5320a5d",
      "name": "Supabase: apply_updates",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [
        5328,
        32
      ],
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const reply = String($json.response || '');\nconst identity = $json.phone_number; // 'tg:<user_id>' (DM). For groups we\u2019ll override when sending to DB.\nif (!identity) throw new Error('Build p_updates: missing identity');\nif (!reply) throw new Error('Build p_updates: outbound.text missing');\n\nconst tgId = $node['Parse update'].json.telegram_message_id || 'no-id';\nfunction tinyHash(s){ let h=0; for(let i=0;i<s.length;i++) h=(h*31 + s.charCodeAt(i))>>>0; return h.toString(16).padStart(8,'0');}\nconst client_msg_id = `${tgId}:v2:${tinyHash(reply)}`;\n\nconst p_updates = {\n  outbound: {\n    client_msg_id,\n    text: reply,\n    lang: $json.lang || 'en',\n    child_id: Array.isArray($json.interventions) && $json.interventions[0]?.child_id ? $json.interventions[0].child_id : null,\n    provider: 'telegram',\n    thread_id: $json.telegram_thread_id || null      // <\u2014 add this\n  }\n};\n\nif (Array.isArray($json.children) && $json.children.length) p_updates.children = $json.children;\nif (Array.isArray($json.interventions) && $json.interventions.length) {\n  p_updates.interventions = $json.interventions.map(iv => ({\n    category_slug: iv.category_slug,\n    technique_slug: iv.technique_slug ?? null,\n    solution: iv.solution ?? null,\n    expected_outcome: iv.expected_outcome ?? null,\n    child_id: iv.child_id ?? null\n  }));\n}\n\nconst templateName = $json.template_name || null;\nconst sfIso = $json.followup_scheduled_for_iso || null;\nconst hasHours = $json.followup_hours != null;\nif (templateName && (sfIso || hasHours)) {\n  const hours = Number($json.followup_hours ?? 12);\n  const scheduled_for = sfIso || new Date(Date.now() + hours*3600*1000).toISOString();\n  p_updates.followup = {\n    create: true,\n    template_name: templateName,\n    scheduled_for,\n    language: $json.lang || 'en',\n    parameters: $json.followup_params || {},\n    provider: 'telegram',\n    thread_id: $json.telegram_thread_id || null      // <\u2014 optional, for group-topic followups\n  };\n}\n\nreturn [{\n  json: {\n    // identity to write in DB for *this surface*\n    p_phone: ($node['Parse update'].json.chat_type === 'private'\n      ? ('tg:' + $node['Parse update'].json.telegram_user_id)\n      : ('tg_group:' + $node['Parse update'].json.telegram_chat_id)),\n    p_updates\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5152,
        32
      ],
      "id": "a3150242-8790-439f-8197-5767dba627e5",
      "name": "Build p_updates"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-4o",
          "mode": "list",
          "cachedResultName": "GPT-4O"
        },
        "messages": {
          "values": [
            {
              "content": "You are **Bubu**, a warm, practical parenting companion.\n\nINPUT (JSON from the previous node):\n- lang\n- children[]: { id, name, date_of_birth|null }\n- last_active_child_id, active_child_hint\n- recent_messages[] (last 14 days)\n- recent_interventions[]\n- failed_by_category[]: [{ child_id, category_slug, slugs:[technique_slug] }]\n- history_summary: string|null\n- message: parent\u2019s current message\n- is_voice: boolean\n- offer_voice_note: boolean\n- voice_low_confidence: boolean\n\nBEHAVIOR:\n- Be brief, kind, and actionable. 40\u201390 words. Use a child\u2019s name only if present in `children[]`, `active_child_hint`, or explicitly in `message`.\n- Do not guess a name. If unknown, say \u201cyour child\u201d.\n- One clarifier max if advice is blocked; ask one crisp question.\n- Avoid techniques listed in `failed_by_category` for that child/category.\n- Use `history_summary` only for continuity (don\u2019t restate it).\n- Detect medical red flags; if present, set flags.medical_red_flag=true and add a short safety line (no diagnosis).\n- If a check-in will help, suggest it and set a follow-up.\n- Tone: warm coach; 2\u20134 concrete steps.\n- Language = input message language; else reuse last known.\n\nIMPORTANT OUTPUT RULES:\n- Never invent names/ages. If a field isn\u2019t known, output **null**.\n- Only include `intervention` when you give a concrete plan.\n- Only include `child_update` when the parent introduces a *new* child.\n- Only include `schedule_followup` when you explicitly want a check-in; parameters must not contain a child_name unless it is known.\n\nOUTPUT \u2014 return ONLY this JSON (no markdown, no extra text):\n{\n  \"reply\": string,                         // 40\u201390 words, plain text\n  \"lang\": \"en\" | \"es\" | \"it\" | \"...\",\n  \"topic_freeform\": string|null,\n  \"category_slug\": \"sleep\"|\"sleep.naps\"|\"feeding.solids\"|\"feeding\"|\"behavior.tantrums\"|\"behavior\"|\"potty\"|\"health\"|\"development\"|\"routine\"|\"childcare\"|\"travel\"|\"screen_time\"|null,\n\n  \"child\": { \"id\": string|null, \"name\": string|null },\n\n  \"intervention\": {\n    \"technique_slug\": string,\n    \"solution\": string,                    // 2\u20134 short steps\n    \"expected_outcome\": string|null\n  } | null,\n\n  \"clarification\": { \"ask\": boolean, \"question\": string|null },\n\n  \"schedule_followup\": {\n    \"create\": boolean,\n    \"template_name\": string|null,\n    \"scheduled_for_hours\": number|null,\n    \"parameters\": { \"child_name\": string|null } | null\n  },\n\n  \"child_update\": {\n    \"add_or_update\": boolean,\n    \"name\": string|null,\n    \"date_of_birth\": string|null,\n    \"approx_age_months\": number|null\n  } | null,\n\n  \"flags\": { \"medical_red_flag\": boolean, \"upsell\": \"never\"|\"maybe\"|\"now\" }\n}",
              "role": "system"
            },
            {
              "content": "={{ JSON.stringify($node[\"Resolve child + build\"].json.orchestrator_input) }}"
            }
          ]
        },
        "jsonOutput": true,
        "options": {
          "temperature": 0.2
        }
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        2752,
        16
      ],
      "id": "2b9f3a81-049b-494a-a112-32528c3cfcb2",
      "name": "OpenAI: Orchestrator",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-4o",
          "mode": "list",
          "cachedResultName": "GPT-4O"
        },
        "messages": {
          "values": [
            {
              "content": "You are a deterministic verifier/fixer that outputs ONE JSON object named pipeline_ready.\n\nRules (do all):\n- Reply length: \u22645 lines. If longer, condense to \u22645 lines.\n- Safety line: add ONLY if flags.medical_red_flag=true.\n- Resolve child in this order: (1) explicit id; (2) match name in context.children; (3) context.last_active_child_id; else null.\n- Interventions require BOTH category_slug and child.id; otherwise set intervention=null for this turn.\n- If intervention.technique_slug exists in context.failed_by_category for this child+category, replace with a reasonable alternative or drop technique_slug but keep solution.\n- If category_slug=null, try a simple keyword map from topic_freeform; if still null, do NOT include an intervention.\n- If child_update.add_or_update=true in the draft, include children:[{name, date_of_birth|null, approx_age_months|null}].\n- You may use context.history_summary only to resolve names/continuity. Never echo it.\n\nOutput EXACTLY:\n{\n  \"pipeline_ready\": {\n    \"reply\": string,\n    \"lang\": string,\n    \"topic_freeform\": string|null,\n    \"category_slug\": string|null,\n    \"child\": {\"id\": \"uuid|null\", \"name\": \"string|null\"},\n    \"children\": [{\"name\":\"string\",\"date_of_birth\":null|\"YYYY-MM-DD\",\"approx_age_months\":number|null}]|null,\n    \"intervention\": {\"technique_slug\":\"string|null\",\"solution\":\"string|null\",\"expected_outcome\":\"string|null\"}|null,\n    \"schedule_followup\": {\"create\":bool,\"template_name\":\"string|null\",\"scheduled_for_hours\":number|null,\"parameters\":object}|null,\n    \"flags\": {\"medical_red_flag\":bool}\n  }\n}\nReturn ONLY pipeline_ready. No commentary.",
              "role": "system"
            },
            {
              "content": "=Context:\n{{JSON.stringify($json.context)}}\nModel output to verify:\n{{JSON.stringify($json.draft)}}\n\nReturn ONLY pipeline_ready."
            }
          ]
        },
        "jsonOutput": true,
        "options": {
          "maxTokens": 600,
          "temperature": 0,
          "topP": 1
        }
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        3232,
        16
      ],
      "id": "8148d025-2756-4a4d-be7c-ff04dcb8751e",
      "name": "OpenAI: Verifier",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-4o",
          "mode": "list",
          "cachedResultName": "GPT-4O"
        },
        "messages": {
          "values": [
            {
              "content": "Produce a compact, factual relationship summary of the last 10\u201315 turns (child focus, topics, techniques tried, outcomes, next check-in). \u22641100 characters hard cap. No fluff, no advice, no emojis. Return plain text only.",
              "role": "system"
            },
            {
              "content": "=Phone: {{$node[\"Resolve child + build\"].json.phone_number}}\nRecent messages:\n{{JSON.stringify($node[\"Supabase: get_context\"].json.recent_messages || [])}}\nRecent interventions:\n{{JSON.stringify($node[\"Supabase: get_context\"].json.recent_interventions || [])}}"
            }
          ]
        },
        "jsonOutput": true,
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [
        5712,
        32
      ],
      "id": "fbeba85d-e97e-4359-9788-09efa22f0c1e",
      "name": "OpenAI: Summarizer",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.errorTrigger",
      "typeVersion": 1,
      "position": [
        -16,
        304
      ],
      "id": "a90ab114-a457-4612-ba9c-5696c41b9e46",
      "name": "Error Trigger"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://idewetjagvjpdjvqlpvl.supabase.co/rest/v1/rpc/log_event",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi",
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "p_phone",
              "value": "={{$json.phone_number}}"
            },
            {
              "name": "p_type",
              "value": "={{$json.event_type}}"
            },
            {
              "name": "p_meta",
              "value": "={{ JSON.stringify($json.meta) }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        352,
        304
      ],
      "id": "381bc619-2249-44c8-9d02-61fcf2b73feb",
      "name": "log_event",
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://idewetjagvjpdjvqlpvl.supabase.co/rest/v1/rpc/update_user_summary",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "p_phone",
              "value": "={{$node[\"Resolve child + build\"].json.phone_number}}"
            },
            {
              "name": "p_summary",
              "value": "={{ $json?.message?.content || $json?.data || $json?.response || $json }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        6016,
        32
      ],
      "id": "083f6fcc-4b0c-4dc8-93a4-4284bf4dffab",
      "name": "Update Summary",
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Error Trigger payload\nconst exec = $json.execution || {};\nconst run  = exec.data || {};\nconst runData = run?.resultData?.runData || {};\nconst wf   = exec.workflow || {};\nconst err  = exec.error || {};\n\nlet phone = null;\n// Heuristic: scan runData for a phone number from any node output\nouter:\nfor (const nodeName of Object.keys(runData)) {\n  for (const task of (runData[nodeName] || [])) {\n    for (const conn of (task?.data?.main || [])) {\n      for (const item of (conn || [])) {\n        const j = item?.json || {};\n        const candidate = j.phone_number || j.p_phone || j.to || j.from;\n        if (candidate && String(candidate).trim()) { phone = String(candidate).trim(); break outer; }\n      }\n    }\n  }\n}\n\nconst trunc = (s, n) => (s ? String(s).slice(0, n) : null);\n\nreturn {\n  phone_number: phone || 'unknown',\n  event_type: 'workflow_error',\n  meta: {\n    workflow_id: wf.id || null,\n    workflow_name: wf.name || null,\n    run_id: exec.id || null,\n    failed_node: exec.node?.name || null,\n    error_message: trunc(err.message, 500),\n    error_stack: trunc(err.stack, 1000),\n    started_at: run.startedAt || null,\n    stopped_at: run.stoppedAt || null,\n  },\n};"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        176,
        304
      ],
      "id": "6c265173-7336-46ca-bbbe-2ba99de634c2",
      "name": "Build Log"
    },
    {
      "parameters": {
        "jsCode": "// Normalize Verifier output to { pipeline_ready: {...} } no matter the shape\n\nconst raw = $json;\nlet pr = null;\n\n// 1) Ideal: already at root\nif (raw && raw.pipeline_ready) pr = raw.pipeline_ready;\n\n// 2) n8n \"full\" message shape\nif (!pr && raw && raw.message && raw.message.content && raw.message.content.pipeline_ready) {\n  pr = raw.message.content.pipeline_ready;\n}\n\n// 3) Array of choices/messages\nif (!pr && Array.isArray(raw) && raw[0]?.message?.content?.pipeline_ready) {\n  pr = raw[0].message.content.pipeline_ready;\n}\n\n// 4) Occasionally it's under .content directly\nif (!pr && raw && raw.content && raw.content.pipeline_ready) {\n  pr = raw.content.pipeline_ready;\n}\n\n// 5) Last resort: if the model returned the bare object (no wrapper)\n//    we accept it if it looks like a pipeline_ready (has reply/lang)\nif (!pr && raw && typeof raw === 'object' && raw.reply && raw.lang) {\n  pr = raw;\n}\n\nif (!pr || typeof pr !== 'object') {\n  const keys = raw && typeof raw === 'object' ? Object.keys(raw) : typeof raw;\n  throw new Error(`Verifier: missing pipeline_ready object (got: ${JSON.stringify(keys)})`);\n}\n\n// Minimal shape checks (fail fast)\nfor (const k of ['reply','lang','child','flags']) {\n  if (!(k in pr)) throw new Error(`Verifier: pipeline_ready.${k} missing`);\n}\nif (!pr.reply) throw new Error('Verifier: empty reply');\n\n// Output normalized shape for downstream nodes\nreturn [{ json: { pipeline_ready: pr } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3552,
        16
      ],
      "id": "27eb5ca9-0a21-43fc-823c-e9b350ed3bce",
      "name": "Guard: Verifier JSON"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "bd1b5110-5885-45f4-bc69-4be896612165",
              "leftValue": "={{ !!$json.pipeline_ready?.schedule_followup?.create }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3776,
        16
      ],
      "id": "4770565f-6c8e-48eb-b112-1a55870b4031",
      "name": "Has followup?"
    },
    {
      "parameters": {
        "jsCode": "// Expect input from Merge (has RPC counts + scheduled_for_iso/clamped baked in)\nconst out = items[0].json;\nconst pr  = out.pipeline_ready || {};\nconst sf  = pr.schedule_followup || {};\nif (!sf.create) return items;\n\n// 1) Template allow-list\nconst ALLOW = new Set([\n  'sleep_checkin_morning','open_loop_nudge','success_celebration',\n  'conversion_invite','voice_suggestion','check_in_generic',\n  'clarify_blocker','nap_checkin_today','new_approach_suggestion',\n  'reengage_weekly',\n]);\nlet template = String(sf.template_name || '').trim();\nif (!ALLOW.has(template)) template = 'check_in_generic';\n\n// 2) Caps / recency (these come from Merge: RPC + Set)\nconst sent1d = Number(out.sent_1d || 0) > 0;\nconst sent7d = Number(out.sent_7d || 0) >= 3;\nconst recent = !!out.recent_inbound;\n\n// 3) Stop on recent reply unless clarifier\nconst isCritical = template === 'clarify_blocker';\nif (sent1d || sent7d || (recent && !isCritical)) {\n  pr.schedule_followup = { create: false };\n  out.pipeline_ready = pr;\n  return [{ json: out }];\n}\n\n// 4) Keep the ISO we computed upstream\nconst whenIso = sf.scheduled_for_iso || out.scheduled_for_clamped || null;\n\n// 5) Write back (preserve language/params)\npr.schedule_followup = {\n  ...sf,\n  create: true,\n  template_name: template,\n  language: sf.language || 'en',\n  scheduled_for_iso: whenIso,\n};\n\nout.pipeline_ready = pr;\nreturn [{ json: out }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4720,
        -112
      ],
      "id": "29329aac-8d5c-4b70-a74e-cc1d53e7aedf",
      "name": "Follow-up Policy Clamp"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "3c490698-789f-4043-b2d9-1b9fed8d8451",
              "name": "_now_iso",
              "value": "={{ $now.toUTC().toISO() }}",
              "type": "string"
            },
            {
              "id": "25db1f7c-3e30-4ae1-a1e3-ae7d159fe0c6",
              "name": "_cutoff_2h",
              "value": "={{ $now.minus({ hours: 2 }).toUTC().toISO() }}",
              "type": "string"
            },
            {
              "id": "d124e936-1ffc-463d-961f-e841fe6ee4e8",
              "name": "_cutoff_1d",
              "value": "={{ $now.minus({ days: 1 }).toUTC().toISO() }}",
              "type": "string"
            },
            {
              "id": "ce116adc-9335-4c1e-867f-a5cbae1a8e97",
              "name": "_cutoff_7d",
              "value": "={{ $now.minus({ days: 7 }).toUTC().toISO() }}",
              "type": "string"
            },
            {
              "id": "216a8523-a044-489f-bf7c-2721365ad7a1",
              "name": "_candidate_utc",
              "value": "={{ $now.plus({ hours: $json.pipeline_ready?.schedule_followup?.scheduled_for_hours ?? 12 }).toUTC().toISO() }}",
              "type": "string"
            },
            {
              "id": "5426e7ce-24a0-4afa-8807-e39fccab38b2",
              "name": "scheduled_for_clamped",
              "value": "={{\n  (() => {\n    const hours = $json.pipeline_ready?.schedule_followup?.scheduled_for_hours ?? 12;\n    const tz    = 'Europe/Madrid';\n    const cand  = $now.plus({ hours }).setZone(tz);\n    const quiet = (cand.hour >= 22 || cand.hour < 8);\n\n    let target = cand;\n    if (quiet) {\n      const base = (cand.hour < 8) ? cand : cand.plus({ days: 1 });\n      target = base.set({ hour: 8, minute: 30, second: 0, millisecond: 0 });\n    }\n\n    // safety: never schedule in the past (edge cases)\n    const nowL = $now.setZone(tz);\n    if (target < nowL) target = target.plus({ days: 1 });\n\n    return target.setZone('UTC').toISO();\n  })()\n}}",
              "type": "string"
            },
            {
              "id": "712db744-bbd0-4253-a819-81b0180c1a88",
              "name": "phone_number",
              "value": "={{ $json.phone_number || ('tg:' + $node[\"Parse update\"].json.telegram_user_id) }}",
              "type": "string"
            },
            {
              "id": "c0a40e81-aa03-4b34-b2bb-06d0f090b436",
              "name": "pipeline_ready.schedule_followup.scheduled_for_iso",
              "value": "={{\n  (() => {\n    const hours = $json.pipeline_ready?.schedule_followup?.scheduled_for_hours ?? 12;\n    const tz    = 'Europe/Madrid';\n    const cand  = $now.plus({ hours }).setZone(tz);\n    const quiet = (cand.hour >= 22 || cand.hour < 8);\n\n    let target = cand;\n    if (quiet) {\n      const base = (cand.hour < 8) ? cand : cand.plus({ days: 1 });\n      target = base.set({ hour: 8, minute: 30, second: 0, millisecond: 0 });\n    }\n\n    // safety: never schedule in the past (edge cases)\n    const nowL = $now.setZone(tz);\n    if (target < nowL) target = target.plus({ days: 1 });\n\n    return target.setZone('UTC').toISO();\n  })()\n}}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        4032,
        -128
      ],
      "id": "522af7b0-88bc-49d1-875f-89c06c65038e",
      "name": "Time helpers + clamp candidate"
    },
    {
      "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\": \"{{$json._cutoff_2h}}\",\n  \"p_cutoff_1d\": \"{{$json._cutoff_1d}}\",\n  \"p_cutoff_7d\": \"{{$json._cutoff_7d}}\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        4256,
        -288
      ],
      "id": "f2f0b5fd-f241-49f3-82e1-cce5c124dd9a",
      "name": "Supabase: Clamp inputs",
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineByPosition",
        "options": {
          "clashHandling": {
            "values": {
              "resolveClash": "preferInput1"
            }
          },
          "includeUnpaired": false
        }
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        4464,
        -112
      ],
      "id": "166e1e30-fa4d-41bb-9ffc-0c1acc0ae668",
      "name": "Merge"
    },
    {
      "parameters": {
        "url": "={{ 'https://api.telegram.org/bot' + $env.TELEGRAM_BOT_TOKEN + '/getFile?file_id=' + $json.media_file_id }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1280,
        -112
      ],
      "id": "7f840e82-d8df-4c87-9270-970d3f50ab79",
      "name": "TG getFile"
    },
    {
      "parameters": {
        "jsCode": "// 1) Get verifier output\nconst pr = $json.pipeline_ready || $node['Guard: Verifier JSON'].json.pipeline_ready;\nif (!pr || !pr.reply) throw new Error('Formatter: pipeline_ready.reply missing');\n\n// 2) Clamp to \u22645 lines\nlet text = String(pr.reply).replace(/\\s+\\n/g, '\\n').trim().split('\\n').slice(0, 5).join('\\n');\nif (!text) throw new Error('Formatter: empty reply after clamp');\n\n// 3) Optional topic \u2192 category mapping (unchanged)\nfunction mapTopicToCategorySlug(t) {\n  if (!t) return null; const s = t.toLowerCase();\n  if (/(sleep|nap|bedtime|regression|wake window|early waking)/.test(s)) return 'sleep';\n  if (/(feed|breast|bottle|formula|latch|pumping|reflux|spit up|colic)/.test(s)) return 'feeding';\n  if (/(solids|wean|weaning|pur\u00e9e|blw|baby-led)/.test(s)) return 'feeding.solids';\n  if (/(potty|toilet|pee|poo|accident)/.test(s)) return 'potty';\n  if (/(fever|cough|rash|diarrhea|vomit|sick|ill)/.test(s)) return 'health';\n  if (/(routine|schedule|morning|bedtime routine)/.test(s)) return 'routine';\n  if (/(tantrum|meltdown|biting|hitting|aggression|defiance|boundary|discipline|scream)/.test(s)) return 'behavior.tantrums';\n  if (/(milestone|rolling|crawling|walking|talking)/.test(s)) return 'development';\n  if (/(screen|tv|tablet|youtube)/.test(s)) return 'screen_time';\n  if (/(travel|flight|jet lag|car seat)/.test(s)) return 'travel';\n  return null;\n}\nconst category_slug = pr.category_slug || mapTopicToCategorySlug(pr.topic_freeform);\n\n// 4) Read parse context\nconst parse = $node['Parse update'].json || {};\nconst chatType = parse.chat_type;\nconst isDM = chatType === 'private';\nconst threadId = parse.thread_id || null;\n\n// 5) Determine identities and destinations\nconst identityPhone = isDM\n  ? ('tg:' + String(parse.telegram_user_id || ''))\n  : ('tg_group:' + String(parse.telegram_chat_id || ''));\n\nconst sendChatId = isDM\n  ? String(parse.telegram_user_id || '')\n  : String(parse.telegram_chat_id || '');\n\nif (!sendChatId) throw new Error('Formatter: missing chat_id from Parse update');\n\n// tiny stable-ish id for outbound idempotency\nfunction tinyHash(s){ let h=0; for (let i=0;i<s.length;i++) h=(h*31 + s.charCodeAt(i))>>>0; return h.toString(16).padStart(8,'0'); }\nconst baseMsgId = String(parse.telegram_message_id || Date.now());\nconst client_msg_id = `${baseMsgId}:v2:${tinyHash(text)}`;\n\n// 6) Build legacy-compatible object (keeps your older nodes happy)\nconst legacy = {\n  phone_number: isDM ? ('tg:' + String(parse.telegram_user_id || '')) : ('tg_group:' + String(parse.telegram_chat_id || '')),\n  telegram_chat_id: String(parse.telegram_chat_id || ''),\n  telegram_thread_id: threadId,\n  response: text,\n  lang: pr.lang || 'en'\n};\n\n// 7) Build DB payload for apply_updates\nconst p_updates = {\n  outbound: {\n    client_msg_id,\n    text,\n    lang: pr.lang || 'en',\n    child_id: pr.child?.id ?? null,\n    provider: 'telegram',\n    thread_id: threadId\n  }\n};\n\n// optional children upserts\nif (Array.isArray(pr.children) && pr.children.length) {\n  p_updates.children = pr.children\n    .map(c => {\n      const name = String(c.name || '').trim();\n      if (!name) return null;\n      const hasDOB = !!c.date_of_birth;\n      return {\n        name,\n        date_of_birth: hasDOB ? c.date_of_birth : null,\n        approx_age_months: c.approx_age_months ?? null,\n        dob_precision: hasDOB ? 'exact' : 'approx',\n      };\n    })\n    .filter(Boolean);\n}\n\n// optional interventions\nif ((category_slug || pr.category_slug) && pr.child?.id && pr.intervention) {\n  const { technique_slug=null, solution=null, expected_outcome=null } = pr.intervention;\n  if (technique_slug || solution) {\n    p_updates.interventions = [{\n      category_slug: category_slug || pr.category_slug,\n      technique_slug, solution, expected_outcome, child_id: pr.child.id\n    }];\n  }\n}\n\n// optional follow-up passthrough (thread-aware for group topics)\nif (pr.schedule_followup && pr.schedule_followup.create) {\n  p_updates.followup = {\n    create: true,\n    template_name: pr.schedule_followup.template_name || null,\n    scheduled_for: pr.schedule_followup.scheduled_for_iso || null, // your time clamp step may set this later\n    language: pr.lang || 'en',\n    parameters: pr.schedule_followup.parameters || {},\n    provider: 'telegram',\n    thread_id: threadId || null\n  };\n  if (!p_updates.followup.scheduled_for && pr.schedule_followup.scheduled_for_hours != null) {\n    // leave hours to earlier \"Time helpers\" step if you have it;\n    // otherwise set scheduled_for here\n    const hrs = Number(pr.schedule_followup.scheduled_for_hours);\n    if (Number.isFinite(hrs)) {\n      p_updates.followup.scheduled_for = new Date(Date.now() + hrs*3600*1000).toISOString();\n    }\n  }\n}\n\n// 8) Build envelope for Telegram send node\nconst envelope = {\n  chat_id: sendChatId,\n  text,\n  message_thread_id: threadId || undefined\n};\n\n// 9) Final item merges legacy fields + new fields\nreturn [{\n  ...legacy,\n  p_phone: identityPhone,\n  p_updates,\n  envelope\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4928,
        32
      ],
      "id": "55fec3b7-9e9e-4676-982d-40b320841b2b",
      "name": "Format (strict) \u2014 Telegram"
    },
    {
      "parameters": {
        "chatId": "={{$node['Format (strict) \u2014 Telegram'].json.envelope.chat_id}}",
        "text": "={{$node['Format (strict) \u2014 Telegram'].json.envelope.text}}",
        "additionalFields": {
          "appendAttribution": false,
          "message_thread_id": "={{$node['Format (strict) \u2014 Telegram'].json.envelope.message_thread_id}}"
        }
      },
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        5520,
        32
      ],
      "id": "8d28d493-d17b-405f-9794-68d8099077c7",
      "name": "Send a text message",
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://idewetjagvjpdjvqlpvl.supabase.co/rest/v1/rpc/record_inbound_v2",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "supabaseApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{\n  { \"p_msg\": {\n      \"provider\": \"telegram\",\n      \"chat_type\": $node[\"Parse update\"].json.chat_type ?? \"private\",\n      \"chat_id\": String($node[\"Parse update\"].json.telegram_chat_id ?? \"\"),\n      \"from_id\": String($node[\"Parse update\"].json.telegram_user_id ?? \"\"),\n      \"provider_msg_id\": String($node[\"Parse update\"].json.telegram_message_id ?? \"\"),\n      \"thread_id\": $node[\"Parse update\"].json.thread_id ?? null,\n      \"content\": ($json.content ?? $node[\"Parse update\"].json.content ?? \"\"),\n      \"is_voice\": !!($json.is_voice ?? $node[\"Parse update\"].json.is_voice ?? false),\n      \"media_file_id\": ($json.media_file_id ?? $node[\"Parse update\"].json.media_file_id ?? null),\n      \"transcription_confidence\": ($json.transcription_confidence ?? null),\n      \"audio_seconds\": ($json.audio_seconds ?? $node[\"Parse update\"].json.audio_seconds ?? null),\n      \"timestamp\": ($node[\"Parse update\"].json.timestamp ?? $json.timestamp ?? Math.floor(Date.now()/1000)),\n      \"lang\": ($node[\"Parse update\"].json.lang ?? null)\n  } }\n}}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2176,
        16
      ],
      "id": "635860fc-3a25-46b7-a573-d63bf2f512fa",
      "name": "Supabase: record_inbound_v2",
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "85d597e5-8ce8-4df0-b062-951893ee6384",
              "leftValue": "={{ ($json.chat_type === 'group' || $json.chat_type === 'supergroup')\n     ? ($json.mentioned === true || $json.privacy_mode === 'off')\n     : true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        832,
        16
      ],
      "id": "9fa40888-faf2-413a-8e6e-30926345d81e",
      "name": "Group allowed?"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "467a98b9-8242-4f07-9637-d135e5c77c35",
              "name": "bot_username",
              "value": "=bubuappBot",
              "type": "string"
            },
            {
              "id": "440146b7-0178-4a75-94b5-59fee36950ab",
              "name": "privacy_mode",
              "value": "=on",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        240,
        0
      ],
      "id": "75a250f4-24da-49f6-b736-7966f698a3e7",
      "name": "Bot config"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "4d893aac-afc5-4a5b-a665-de214f526a0c",
              "leftValue": "={{ $json.skip === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        624,
        0
      ],
      "id": "bc031958-2075-49fe-bd08-830d7f5f5182",
      "name": "Skip?"
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Bot config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse update": {
      "main": [
        [
          {
            "node": "Skip?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "is_voice?": {
      "main": [
        [
          {
            "node": "TG getFile",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Supabase: record_inbound_v2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "TG download": {
      "main": [
        [
          {
            "node": "Transcribe a recording",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transcribe a recording": {
      "main": [
        [
          {
            "node": "Build transcribed message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build transcribed message": {
      "main": [
        [
          {
            "node": "Supabase: record_inbound_v2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolve child + build": {
      "main": [
        [
          {
            "node": "OpenAI: Orchestrator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify draft": {
      "main": [
        [
          {
            "node": "OpenAI: Verifier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Supabase: apply_updates": {
      "main": [
        [
          {
            "node": "Send a text message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build p_updates": {
      "main": [
        [
          {
            "node": "Supabase: apply_updates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI: Orchestrator": {
      "main": [
        [
          {
            "node": "Verify draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI: Verifier": {
      "main": [
        [
          {
            "node": "Guard: Verifier JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI: Summarizer": {
      "main": [
        [
          {
            "node": "Update Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Error Trigger": {
      "main": [
        [
          {
            "node": "Build Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Log": {
      "main": [
        [
          {
            "node": "log_event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Guard: Verifier JSON": {
      "main": [
        [
          {
            "node": "Has followup?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has followup?": {
      "main": [
        [
          {
            "node": "Time helpers + clamp candidate",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format (strict) \u2014 Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Follow-up Policy Clamp": {
      "main": [
        [
          {
            "node": "Format (strict) \u2014 Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Time helpers + clamp candidate": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          },
          {
            "node": "Supabase: Clamp inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Supabase: Clamp inputs": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Follow-up Policy Clamp",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Supabase: get_context": {
      "main": [
        [
          {
            "node": "Resolve child + build",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "TG getFile": {
      "main": [
        [
          {
            "node": "TG download",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format (strict) \u2014 Telegram": {
      "main": [
        [
          {
            "node": "Build p_updates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send a text message": {
      "main": [
        [
          {
            "node": "OpenAI: Summarizer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Supabase: record_inbound_v2": {
      "main": [
        [
          {
            "node": "Supabase: get_context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Group allowed?": {
      "main": [
        [
          {
            "node": "is_voice?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bot config": {
      "main": [
        [
          {
            "node": "Parse update",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip?": {
      "main": [
        [],
        [
          {
            "node": "Group allowed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "versionId": "0d417a98-b553-4907-b4f7-189649f8d02d",
  "triggerCount": 1,
  "shared": [
    {
      "createdAt": "2025-09-03T21:28:03.671Z",
      "updatedAt": "2025-09-03T21:28:03.671Z",
      "role": "workflow:owner",
      "workflowId": "3kXvp0MLl2zK66C9",
      "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": []
}