This workflow follows the Error Trigger → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"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": []
}
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.
openAiApisupabaseApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Bubu Telegram Companion. Uses httpRequest, openAi, errorTrigger, telegram. Webhook trigger; 31 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.
Bubu WhatsApp Companion. Uses httpRequest, openAi, errorTrigger. Webhook trigger; 35 nodes.
Listens for completed Fireflies transcripts, qualifies whether a proposal is needed using OpenAI, drafts structured proposal content, populates a Google Doc template, converts to PDF, and sends it to
This workflow turns a Telegram bot into an AI-powered lyrics assistant. Users send a command plus a lyrics URL, and the flow downloads, cleans, and analyzes the text, then replies on Telegram with tra
LU. Uses telegram, openAi, httpRequest. Webhook trigger; 28 nodes.
Video Ads Automation - Real Estate. Uses openAi, telegram, httpRequest, googleDrive. Webhook trigger; 24 nodes.