This workflow follows the HTTP Request → Supabase 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 →
{
"name": "WhatsApp Assistant Base - P1",
"nodes": [
{
"parameters": {
"httpMethod": [
"GET",
"POST"
],
"path": "whatsapp",
"responseMode": "responseNode",
"options": {}
},
"id": "3933499b-611b-4ae6-aa7d-492daae3aafd",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-480,
128
],
"multipleMethods": true
},
{
"parameters": {
"conditions": [
{
"id": "1",
"leftValue": "={{ $json.body && $json.body.entry ? 'false' : 'true' }}",
"rightValue": "true",
"operator": "string"
}
],
"options": {}
},
"id": "ce11aee4-1d4d-4af8-8718-ab345f6c3d20",
"name": "IF Verificaci\u00f3n",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
-240,
128
]
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $json.query['hub.challenge'] }}",
"options": {}
},
"id": "eaa96758-c098-4057-9bac-647efbd4244e",
"name": "Responder Challenge",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
0,
0
]
},
{
"parameters": {
"jsCode": "const mode = String($env.WEBHOOK_VERIFY_SIGNATURE || 'false').toLowerCase();\nconst passthrough = (verified, m) => [{ json: Object.assign({ verified, mode: m }, $input.first().json) }];\nif (mode === 'false') { return passthrough(true, 'disabled'); }\nconst secret = $env.META_APP_SECRET || '';\nconst headers = $input.first().json.headers || {};\nconst signature = headers['x-hub-signature-256'] || headers['X-Hub-Signature-256'] || '';\nconst rawBody = JSON.stringify($input.first().json.body);\nif (!secret) { return passthrough(false, 'missing_secret'); }\nconst run = async () => { try { const encoder = new TextEncoder(); const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']); const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(rawBody)); const hex = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join(''); return passthrough(signature === 'sha256=' + hex, 'enabled'); } catch (e) { return passthrough(false, 'crypto_unavailable'); } };\nreturn run();"
},
"id": "9fe92ab8-35f4-452c-ad12-e474ba45fcb8",
"name": "Verificar Firma Meta",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-240,
368
]
},
{
"parameters": {
"conditions": [
{
"id": "1",
"leftValue": "={{ $json.verified ? 'true' : 'false' }}",
"rightValue": "true",
"operator": "string"
}
],
"options": {}
},
"id": "f3a760a5-e0e6-4f51-bc69-c0e41a6f48c4",
"name": "IF Firma OK",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
0,
368
]
},
{
"parameters": {
"respondWith": "text",
"responseBody": "unauthorized",
"options": {}
},
"id": "9fdfa79e-84ff-49ba-8930-96be53e19559",
"name": "Responder Rechazo",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
240,
368
]
},
{
"parameters": {
"jsCode": "// Normaliza el payload; los eventos no-mensaje (status, tipos raros, propio negocio) terminan en silencio (return []).\nconst payload = $input.first().json;\nconst businessPhone = ($env.WHATSAPP_BUSINESS_PHONE || '').replace(/\\D/g, '');\nfor (const entry of payload.body.entry || payload.entry || []) {\n for (const change of entry.changes || []) {\n const value = change.value || {};\n if (value.messages && value.messages.length > 0) {\n const msg = value.messages[0];\n const from = String(msg.from || '').replace(/\\D/g, '');\n if (businessPhone && from === businessPhone) { return []; }\n const hasText = msg.text && msg.text.body;\n if (msg.type && msg.type !== 'text' && !hasText) {\n const label = { image: '\ud83d\uddbc\ufe0f [El cliente envio una imagen]', audio: '\ud83c\udfa4 [El cliente envio un audio]', video: '\ud83c\udfac [El cliente envio un video]', document: '\ud83d\udcc4 [El cliente envio un documento]' }[msg.type] || '\ud83d\udcce [El cliente envio un adjunto]';\n return [{ json: { valid: true, contact_phone: from, message_text: label, message_id: msg.id || '', timestamp: msg.timestamp || '', type: msg.type || 'media' } }];\n }\n if (!hasText) { return []; }\n return [{ json: { valid: true, contact_phone: from, message_text: String(msg.text.body).slice(0, 2000), message_id: msg.id || '', timestamp: msg.timestamp || '', type: msg.type || 'text' } }];\n }\n if (value.statuses && value.statuses.length > 0) { return []; }\n }\n}\nreturn [];"
},
"id": "d99227e8-6b2e-43cf-97f1-cc528adda5a8",
"name": "Normalizar Mensaje",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
208
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.SUPABASE_URL + '/rest/v1/contactos?on_conflict=tenant_id,telefono' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $env.SUPABASE_SERVICE_KEY }}"
},
{
"name": "Authorization",
"value": "={{ \"Bearer \" + $env.SUPABASE_SERVICE_KEY }}"
},
{
"name": "Prefer",
"value": "resolution=merge-duplicates,return=representation"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ { telefono: $json.contact_phone, tenant_id: $env.TENANT_ID } }}",
"options": {}
},
"id": "9753a2a0-c8d6-4965-84af-08627624afbb",
"name": "Upsert Contacto",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
480,
208
]
},
{
"parameters": {
"jsCode": "// Encuentra la fila del contacto en cualquier forma de respuesta (array, body, anidado).\nconst findRow = (x) => {\n if (Array.isArray(x)) { for (const it of x) { const f = findRow(it); if (f) return f; } return null; }\n if (x && typeof x === 'object') {\n if (x.id && (x.telefono || x.tenant_id)) return x;\n for (const v of Object.values(x)) { const f = findRow(v); if (f) return f; }\n }\n return null;\n};\nconst row = findRow($json) || {};\nif (!row.id) { throw new Error('UPSERT-FALLO: ' + JSON.stringify($json).slice(0, 300)); }\nreturn [{ json: { contacto_id: row.id, tenant_id: row.tenant_id || null } }];"
},
"id": "dfef1f29-2828-4d4a-b342-4b37d5c34c50",
"name": "Contacto Listo",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
720,
208
]
},
{
"parameters": {
"operation": "getAll",
"tableId": "conversaciones",
"limit": 20,
"orderBy": "created_at.desc",
"matchType": "allFilters",
"filters": {
"conditions": [
{
"keyName": "contacto_id",
"condition": "eq",
"keyValue": "={{ $('Contacto Listo').item.json.contacto_id }}"
}
]
}
},
"id": "ac01e36f-ca00-4edb-9244-6af39c310c13",
"name": "Obtener Historial",
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
960,
80
],
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"tableId": "conversaciones",
"fieldsUi": {
"fieldValues": [
{
"fieldId": "tenant_id",
"fieldValue": "={{ $('Contacto Listo').item.json.tenant_id }}"
},
{
"fieldId": "contacto_id",
"fieldValue": "={{ $('Contacto Listo').item.json.contacto_id }}"
},
{
"fieldId": "role",
"fieldValue": "user"
},
{
"fieldId": "content",
"fieldValue": "={{ $('Normalizar Mensaje').item.json.message_text }}"
}
]
}
},
"id": "c4b04d27-8664-4882-a705-dcde7a87ed1a",
"name": "Guardar Mensaje Usuario",
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
960,
320
],
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "let historyRows = [];\ntry { historyRows = $('Obtener Historial').all().map(i => i.json).flat(1).filter(r => r && typeof r === 'object' && !Array.isArray(r)); } catch (e) {}\nconst historial = historyRows.slice(-10).map(h => ({ role: h.role === 'assistant' ? 'assistant' : 'user', content: h.content }));\nconst mensajeActual = $('Normalizar Mensaje').item.json.message_text;\nconst systemPrompt = `Eres el asistente virtual de JeanCRG, consultoria de software e inteligencia artificial en Colombia. Representas a Jean, un emprendedor colombiano que vende asistentes IA para WhatsApp, automatizaciones y desarrollo web.\n\nPERSONALIDAD Y TONO:\n- Cercano, respetuoso y con energia positiva, como un vendedor colombiano profesional.\n- Emojis con moderacion (\ud83d\udc4b \ud83d\ude0a \ud83d\ude80 \u2705 \ud83d\udcc5 \ud83d\udcac).\n- Espanol neutro colombiano: \"\u00bfQue negocio tienes?\", \"claro\", \"perfecto\". Usa \"tu\" (nunca \"vos\"), sin jerga extrema ni regionalismos cerrados.\n- Formato WhatsApp: *negrita* para precios y nombres de servicios.\n- Maximo 2-3 frases por mensaje. SIEMPRE termina con UNA pregunta (nunca dos).\n- Nunca uses listas largas: si el cliente pregunta por todos los servicios, menciona los 3 principales y pregunta cual le llama la atencion.\n\nREGLAS DE CONVERSACION:\n1. Saludo: saludas y te presentas SOLO en la primera interaccion de la conversacion. Las siguientes veces respondes directo al tema, como un vendedor que ya saludo.\n2. Si el cliente vuelve a saludar a mitad de conversacion (\"hola\", \"buenas\"), responde amable sin volver a presentarte: \"\u00a1Hola! \u00bfEn que te ayudo?\"\n3. Si te preguntan si eres robot: \"Soy el asistente virtual de JeanCRG \ud83e\udd16 Respondo al instante y con informacion real del negocio. Si prefieres, te conecto con un asesor humano.\"\n4. Objeciones de precio (\"esta caro\", \"no tengo plata\", \"lo voy a pensar\"): responde con empatia y ofrece el *Piloto de 15 dias por $800.000* (100% descontable) o el *Diagnostico tecnologico por $300.000* (descontable). Ejemplo: \"Te entiendo \ud83d\ude0a Por eso tenemos el piloto de 15 dias por *$800.000*, 100% descontable del servicio final. \u00bfTe cuento como funciona?\"\n5. Agradecimientos y cierres (\"gracias\", \"listo\", \"quedo claro\"): responde calido y deja la puerta abierta: \"\u00a1Con gusto! \ud83d\ude0a Cuando quieras avanzamos. \u00bfTe comparto el enlace para agendar tu consulta gratis?\" (solo si hay interes previo).\n6. Preguntas fuera del tema: redirige amablemente a los servicios de JeanCRG.\n7. Errores de tipeo, mensajes sin puntuacion o de voz: responde normal, sin corregir al cliente.\n8. Si preguntan horarios o disponibilidad: sugiere agendar en https://calendly.com/jeancrg/consulta-gratuita con el caso exacto de agendamiento.\n\nSERVICIOS EXACTOS (nunca inventes precios, servicios NI funciones: no menciones pasarelas de pago, cobros, apps ni nada que no este aqui):\n1) *Asistente IA para WhatsApp* desde *$1.800.000* + retainer mensual de *$250.000* (montaje en 5 dias)\n2) *Diagnostico tecnologico* por *$300.000* (descontable)\n3) *Automatizaciones con n8n* desde *$1.500.000*\n4) *Chatbot con datos propios (RAG)* desde *$2.500.000*\n5) *Tienda online* desde *$2.500.000*\n6) *Desarrollo a medida* desde *$3.000.000*\n*Piloto de 15 dias por $800.000* (100% descontable del servicio final).\n\nCASOS ESPECIFICOS (obligatorio, respuestas EXACTAS cuando aplique):\n- PRIMERA interaccion con mensaje que ES solo un saludo simple (sin pregunta): '\u00a1Hola! \ud83d\udc4b Soy el asistente de JeanCRG. Te ayudo con asistentes IA para WhatsApp, automatizaciones y desarrollo web. \u00bfQue negocio tienes?'\n- PRIMERA interaccion que YA trae una pregunta o necesidad concreta (precio, servicio, problema del negocio): responde DIRECTAMENTE su pregunta con el precio y servicio exactos, y cierra preguntando por su negocio. NO repitas la presentacion completa.\n- Conversacion ya iniciada: responde directo y natural al tema, sin saludos ni presentaciones (ejemplo: '\u00a1Claro! El asistente IA para WhatsApp cuesta desde *$1.800.000* y lo dejamos montado en 5 dias. \u00bfQue negocio tienes?')\n- Quiere agendar/cotizar/propuesta: tu respuesta DEBE ser EXACTAMENTE: '\u00a1Perfecto! \ud83d\udcc5 Te comparto el enlace para que elijas el horario que prefieras: https://calendly.com/jeancrg/consulta-gratuita \u00bfTe queda mejor por la manana o por la tarde?' y agrega [LEAD cita] al final. PROHIBIDO pedir dia u hora, PROHIBIDO confirmar horarios, PROHIBIDO mencionar otros servicios en esa respuesta.\n- Pide hablar con un humano o esta frustrado/insatisfecho: tu respuesta DEBE ser EXACTAMENTE: 'Entendido \ud83d\udc64 Un asesor te escribe por este chat en unos minutos. \u00bfMe confirmas tu nombre para que te atienda con tu caso?' y agrega [DERIVAR] al final. PROHIBIDO seguir vendiendo o pedir mas datos.`;\nconst fechaBogota = new Date().toLocaleString('es-CO', { timeZone: 'America/Bogota', weekday: 'long', day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: true });\nlet notaTiempo = '';\ntry {\n const ordenadas = historyRows.slice().sort((a, b) => String(a.created_at || '').localeCompare(String(b.created_at || '')));\n const ultimaTs = ordenadas.length ? ordenadas[ordenadas.length - 1].created_at : null;\n if (ultimaTs) {\n const horas = (Date.now() - new Date(ultimaTs).getTime()) / 3600000;\n if (horas >= 72) notaTiempo = '\\n\\nCONTEXTO TEMPORAL IMPORTANTE: Han pasado ' + Math.round(horas / 24) + ' DIAS desde el ultimo mensaje del cliente. Empieza reconociendo la ausencia con UNA frase corta y calida (ej: \\u00a1Que bueno saber de ti otra vez!) y SOLO DESPUES retoma el tema.';\n else if (horas >= 20) notaTiempo = '\\n\\nCONTEXTO TEMPORAL: Ha pasado mas de un dia desde el ultimo mensaje del cliente.';\n else notaTiempo = '\\n\\nCONTEXTO TEMPORAL: Conversacion activa hoy mismo.';\n }\n} catch (e) {}\nconst sysContent = 'FECHA Y HORA ACTUAL EN COLOMBIA: ' + fechaBogota + '. Usa esta referencia para palabras como hoy, manana o dias de la semana. Nunca inventes fechas.\\n\\n' + systemPrompt + notaTiempo;\nconst messages = [{ role: 'system', content: sysContent }].concat(historial).concat([{ role: 'user', content: mensajeActual }]);\nreturn [{ json: { model: 'deepseek-v4-flash-vision-exp', thinking: { type: 'disabled' }, messages } }];"
},
"id": "ad0d4b40-506e-4acc-91d3-f6e7b537e6f4",
"name": "Preparar Contexto Gemini",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1200,
208
]
},
{
"parameters": {
"method": "POST",
"url": "https://opencode.ai/zen/go/v1/chat/completions",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $env.OPENCODE_API_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json }}",
"options": {}
},
"id": "fdbefd43-2f6c-4172-a53d-df620a12e21b",
"name": "Gemini Responder",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1440,
208
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"notes": "OpenCode GO (suscripcion) -> mimo-v2.5 sin modo pensamiento (antes deepseek-v4-flash, mas caro). Mover key a $env.OPENCODE_API_KEY para produccion."
},
{
"parameters": {
"jsCode": "const text = (($json.choices && $json.choices[0] && $json.choices[0].message && $json.choices[0].message.content) || '');\nconst cleanRaw = text.replace(/\\[DERIVAR\\]/gi, '').replace(/\\[LEAD[^\\]]*\\]/gi, '').trim().slice(0, 4000);\nconst userText = ($('Normalizar Mensaje').item.json.message_text || '').toLowerCase();\nconst rows = (() => { try { return $('Obtener Historial').all().map(i => i.json).flat(1).filter(r => r && typeof r === 'object' && !Array.isArray(r)); } catch (e) { return [1]; } })();\nconst primerMensaje = !/(cuesta|precio|ayuda|quien|servicio|agenda|pago|pedido|compra|negocio|tienda|vende|vender|gracias|listo)/i.test(userText);\nconst esSaludo = /^(hola|holi|buenas|buen dia|buenos dias|buenas tardes|buenas noches|hi|hello|ey|hey|oie|q mas|que mas|qu[e\u00e9] mas)[\\s!\u00a1?\u00bf.,]*(raza|bebe)?\\s*$/i.test(userText) && primerMensaje;\n// SALUDO (primera vez o de vuelta): respuesta EXACTA fija, sin IA ni marcadores\nif (esSaludo) {\n const txt = rows.length === 0\n ? '\u00a1Hola! \ud83d\udc4b Soy el asistente de JeanCRG. Te ayudo con asistentes IA para WhatsApp, automatizaciones y desarrollo web. \u00bfQu\u00e9 negocio tienes?'\n : '\u00a1Hola! \ud83d\ude0a \u00bfEn qu\u00e9 te ayudo?';\n return [{ json: { raw: text, clean: txt, needs_human: false, lead_marker: '', evento: 'none', determinista: true } }];\n}\nconst HUMAN_RE = /(hablar con (alguien|un humano|una persona|un asesor|alguien real)|asesor humano|persona real|no me convence|no me gusta|queja|quejarme|molest|frustrad|insatisfech|ineficiente|mal servicio|estafa|representante|me canse|aburrid|quien me atiende|hay alguien|atendeme|quiero a alguien|llamen|llamame)/i;\nconst LEAD_RE = /(agendar|cita|reuni[o\u00f3]n|diagn[o\u00f3]stic|cotiz|propuest|presupuest|quiero (comprar|contratar|empezar)|me interesa|cu[\u00e1a]nto cuesta|cu[\u00e1a]nto vale|precio|precios|valor|tarifa|demo|prueba|pruebo|piloto|m[\u00e1a]s informaci[o\u00f3]n|me cuentas|c[o\u00f3]mo funciona|en cu[\u00e1a]nto|ayudame con)/i;\nconst OBJ_RE = /(caro|carita|cara|costoso|no me alcanza|no tengo plata|lo pienso|lo voy a pensar|demasiado|no me sobra|presupuesto apretado|ya tengo otro|lo consulto)/i;\nconst PEDIDO_RE = /(pedido|quiero (pedir|ordenar|encargar|reservar)|me llevo|hacer (un|mi) pedido|domicilio|delivery|a domicilio|talla|disponibilidad)/i;\nconst PAGO_RE = /(pagar|pago|transferenc|nequi|daviplata|davi|tarjeta|bono|cup[o\u00f3]n|promoci[o\u00f3]n|adelanto|consignaci[o\u00f3]n)/i;\nconst marker = (text.match(/\\[LEAD ([^\\]]*)\\]/i) || [null, ''])[1];\nconst aiDeriva = /\\[DERIVAR\\]/i.test(text);\n// DERIVAR solo si el USUARIO pide humano; el [DERIVAR] de la IA solo cuenta si ademas el usuario habla de asesor/humano/persona/llamada\nlet evento = 'none';\nif (HUMAN_RE.test(userText) || (aiDeriva && /(asesor|humano|persona|hablar|llamad)/i.test(userText))) evento = 'derivar';\nelse if (marker || LEAD_RE.test(userText)) evento = 'lead';\nelse if (OBJ_RE.test(userText)) evento = 'objecion';\nelse if (PAGO_RE.test(userText)) evento = 'pago';\nelse if (PEDIDO_RE.test(userText)) evento = 'pedido';\n// si la IA alucino [DERIVAR] sin senal del usuario, usar su texto limpio normal\nreturn [{ json: { raw: text, clean: cleanRaw, needs_human: evento === 'derivar', lead_marker: marker || (LEAD_RE.test(userText) ? 'cita' : ''), evento } }];"
},
"id": "e67b628a-231b-4c05-9329-baa70a4a5f05",
"name": "Extraer Respuesta",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1680,
208
]
},
{
"parameters": {
"tableId": "conversaciones",
"fieldsUi": {
"fieldValues": [
{
"fieldId": "tenant_id",
"fieldValue": "={{ $('Contacto Listo').item.json.tenant_id }}"
},
{
"fieldId": "contacto_id",
"fieldValue": "={{ $('Contacto Listo').item.json.contacto_id }}"
},
{
"fieldId": "role",
"fieldValue": "assistant"
},
{
"fieldId": "content",
"fieldValue": "={{ $json.clean }}"
}
]
}
},
"id": "e7dfceeb-ade9-45f1-84ad-3d4f60a5332b",
"name": "Guardar Respuesta IA",
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
1920,
208
],
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "send",
"phoneNumberId": "=1309447485585504",
"recipientPhoneNumber": "={{ $('Normalizar Mensaje').item.json.contact_phone }}",
"textBody": "={{ $('Extraer Respuesta').item.json.clean }}",
"additionalFields": {}
},
"id": "9a37085a-a6d1-417e-bf92-de713459c79d",
"name": "Enviar WhatsApp",
"type": "n8n-nodes-base.whatsApp",
"typeVersion": 1.1,
"position": [
2160,
320
],
"credentials": {
"whatsAppApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Emite es_lead + datos; sin throw (el insert se hace no-op con body [] cuando no hay lead).\nconst lead = $('Extraer Respuesta').item.json.lead_marker || '';\nconst parts = lead ? String(lead).split('|') : [];\nreturn [{ json: {\n es_lead: Boolean(lead),\n nombre: (parts[0] || '').trim(),\n telefono: $('Normalizar Mensaje').item.json.contact_phone,\n interes: (parts[2] || parts[0] || '').trim()\n} }];"
},
"id": "6b848995-1e2a-4b32-ba52-7dce59512f0e",
"name": "Preparar Lead",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2160,
128
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.SUPABASE_URL + '/rest/v1/leads' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $env.SUPABASE_SERVICE_KEY }}"
},
{
"name": "Authorization",
"value": "={{ \"Bearer \" + $env.SUPABASE_SERVICE_KEY }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.es_lead ? [{ tenant_id: $('Contacto Listo').item.json.tenant_id, contacto_id: $('Contacto Listo').item.json.contacto_id, nombre: $json.nombre, telefono: $json.telefono, interes: $json.interes, estado: 'nuevo', source: 'whatsapp' }] : [] }}",
"options": {}
},
"id": "72d7995f-e24e-440d-ba87-1ef89cfa9b90",
"name": "Guardar Lead",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2400,
128
],
"onError": "continueRegularOutput",
"notes": "Si el lead ya existe (409), se omite silenciosamente y el flujo continua."
},
{
"parameters": {
"jsCode": "const extra = $('Extraer Respuesta').item.json;\nconst phone = $('Normalizar Mensaje').item.json.contact_phone;\nconst evento = extra.evento || 'none';\nconst emojis = { derivar: '\ud83d\udea8 DERIVAR', lead: '\ud83d\udcc5 LEAD', objecion: '\u26a1 OBJECI\u00d3N', pedido: '\ud83d\uded2 PEDIDO', pago: '\ud83d\udcb0 PAGO' };\nconst notificar = evento !== 'none';\nconst texto = notificar ? (emojis[evento] + ': ' + phone + ' | ' + extra.clean) : '';\nreturn [{ json: { notificar, texto } }];"
},
"id": "44e91366-7fd5-4456-b13f-6aa8eeabe626",
"name": "Preparar Notificacion",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2640,
80
]
},
{
"parameters": {
"method": "POST",
"url": "={{ 'https://api.telegram.org/bot8807842110:AAGgU6QDADaN-Lvt_hqZV_orFIhG0dwrT1E/sendMessage' }}",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.notificar ? { chat_id: 7309831214, text: $json.texto } : {} }}",
"options": {}
},
"id": "1607aabe-af32-4226-bf19-d21b874d20e7",
"name": "Enviar Notificacion",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2880,
80
],
"onError": "continueRegularOutput",
"notes": "DEMO: token del bot embebido. PRODUCCION: mover a $env.TELEGRAM_BOT_TOKEN en Railway."
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "IF Verificaci\u00f3n",
"type": "main",
"index": 0
}
],
[
{
"node": "Verificar Firma Meta",
"type": "main",
"index": 0
}
]
]
},
"IF Verificaci\u00f3n": {
"main": [
[
{
"node": "Responder Challenge",
"type": "main",
"index": 0
}
],
[
{
"node": "Verificar Firma Meta",
"type": "main",
"index": 0
}
]
]
},
"Verificar Firma Meta": {
"main": [
[
{
"node": "IF Firma OK",
"type": "main",
"index": 0
}
]
]
},
"IF Firma OK": {
"main": [
[
{
"node": "Normalizar Mensaje",
"type": "main",
"index": 0
}
],
[
{
"node": "Responder Rechazo",
"type": "main",
"index": 0
}
]
]
},
"Normalizar Mensaje": {
"main": [
[
{
"node": "Upsert Contacto",
"type": "main",
"index": 0
}
]
]
},
"Upsert Contacto": {
"main": [
[
{
"node": "Contacto Listo",
"type": "main",
"index": 0
}
]
]
},
"Contacto Listo": {
"main": [
[
{
"node": "Obtener Historial",
"type": "main",
"index": 0
},
{
"node": "Guardar Mensaje Usuario",
"type": "main",
"index": 0
}
]
]
},
"Guardar Mensaje Usuario": {
"main": [
[
{
"node": "Preparar Contexto Gemini",
"type": "main",
"index": 0
}
]
]
},
"Preparar Contexto Gemini": {
"main": [
[
{
"node": "Gemini Responder",
"type": "main",
"index": 0
}
]
]
},
"Gemini Responder": {
"main": [
[
{
"node": "Extraer Respuesta",
"type": "main",
"index": 0
}
]
]
},
"Extraer Respuesta": {
"main": [
[
{
"node": "Guardar Respuesta IA",
"type": "main",
"index": 0
}
]
]
},
"Guardar Respuesta IA": {
"main": [
[
{
"node": "Preparar Lead",
"type": "main",
"index": 0
}
]
]
},
"Preparar Lead": {
"main": [
[
{
"node": "Guardar Lead",
"type": "main",
"index": 0
}
]
]
},
"Guardar Lead": {
"main": [
[
{
"node": "Preparar Notificacion",
"type": "main",
"index": 0
}
]
]
},
"Preparar Notificacion": {
"main": [
[
{
"node": "Enviar Notificacion",
"type": "main",
"index": 0
}
]
]
},
"Enviar Notificacion": {
"main": [
[
{
"node": "Enviar WhatsApp",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": false
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
supabaseApiwhatsAppApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
WhatsApp Assistant Base - P1. Uses httpRequest, supabase, whatsApp. Webhook trigger; 20 nodes.
Source: https://github.com/JeanCardozo/chatAI/blob/main/workflows/whatsapp-assistant-v5-zen.json — 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.
WhatsApp Assistant Base — P1. Uses supabase, httpRequest, telegram, whatsApp. Webhook trigger; 22 nodes.
WhatsApp Assistant Base - P1. Uses httpRequest, supabase, whatsApp. Webhook trigger; 20 nodes.
Course Bot - Didar CRM (v6). Uses n8n-nodes-didar-crm, httpRequest. Webhook trigger; 77 nodes.
Advanced Slackbot With N8N. Uses slack, httpRequest, stickyNote, executeWorkflow. Webhook trigger; 34 nodes.
Slackbots are super powerful. At n8n, we have been using them to get a lot done.. But it can become hard to manage and maintain many different operations that a workflow can do.