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 \u2014 P1",
"nodes": [
{
"parameters": {
"httpMethod": [
"GET",
"POST"
],
"path": "whatsapp",
"responseMode": "responseNode",
"options": {}
},
"id": "d5122d4b-1504-4d46-843d-0fce1470d782",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
0
],
"notes": "Verificacion GET + eventos POST de Meta Cloud API. En Meta for Developers, configurar la URL del tunel + /whatsapp. La verificacion de firma se hace en el nodo 'Verificar Firma Meta'.",
"multipleMethods": true
},
{
"parameters": {
"conditions": [
{
"id": "1",
"leftValue": "={{ $json.body && $json.body.entry ? 'false' : 'true' }}",
"rightValue": "true",
"operator": "string"
}
],
"options": {}
},
"id": "a6433ae9-a3ee-4968-85cb-766ca9cc6d80",
"name": "IF Verificaci\u00f3n",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
240,
0
],
"notes": "Rama true = handshake de Meta (devuelve hub.challenge). Rama false = evento real."
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $json.query['hub.challenge'] }}"
},
"id": "44570dd2-323f-4293-924a-00a7f643b33f",
"name": "Responder Challenge",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
480,
-120
],
"notes": "Responde el challenge de Meta para validar el webhook."
},
{
"parameters": {
"jsCode": "// Normalize WhatsApp Cloud API webhook payload with input guards.\nconst payload = $input.first().json;\nconst headers = payload.headers || {};\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 // Loop prevention: ignore messages sent by the business's own number.\n if (businessPhone && from === businessPhone) {\n return [{ json: { valid: false, reason: 'own_number' } }];\n }\n const isText = msg.type === 'text' && msg.text && msg.text.body;\n if (!isText) {\n return [{ json: { valid: false, reason: 'unsupported_type' } }];\n }\n return [{ json: {\n valid: true,\n reason: 'text',\n contact_phone: from,\n message_text: String(msg.text.body).slice(0, 2000),\n message_id: msg.id || '',\n timestamp: msg.timestamp || '',\n type: msg.type\n }}];\n }\n if (value.statuses && value.statuses.length > 0) {\n return [{ json: { valid: false, reason: 'status' } }];\n }\n }\n}\nreturn [{ json: { valid: false, reason: 'no_message' } }];"
},
"id": "c33bc535-dbc9-4a64-b739-47be017dc62b",
"name": "Normalizar Mensaje",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
480,
80
],
"notes": "Extrae solo mensajes de texto; descarta status y tipos no soportados; loop prevention y max 2000 caracteres."
},
{
"parameters": {
"conditions": [
{
"id": "1",
"leftValue": "={{ $json.valid ? 'true' : 'false' }}",
"rightValue": "true",
"operator": "string"
}
],
"options": {}
},
"id": "44bade24-748e-464c-bdf9-d4c1a165cbe2",
"name": "IF Mensaje V\u00e1lido",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
720,
80
],
"notes": "Rama true = mensaje de texto procesable. Rama false = fin del flujo."
},
{
"parameters": {
"resource": "row",
"operation": "getAll",
"tableId": "contactos",
"returnAll": false,
"limit": 1,
"filterType": "manual",
"matchType": "allFilters",
"filters": {
"conditions": [
{
"keyName": "telefono",
"condition": "eq",
"keyValue": "={{ $json.contact_phone }}"
}
]
}
},
"id": "1dbd302b-4fa3-4235-8a29-2c6b7b9a7b04",
"name": "Obtener Contacto",
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
960,
80
],
"notes": "Busca el contacto por telefono (unico por tenant). Credenciales: supabaseApi (URL + Service Role Key)."
},
{
"parameters": {
"conditions": [
{
"id": "1",
"leftValue": "={{ $json.exists ? 'true' : 'false' }}",
"rightValue": "true",
"operator": "string"
}
],
"options": {}
},
"id": "3792c6a5-d17a-4b71-b037-5dba5b19d93b",
"name": "IF Contacto Existe",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1200,
80
],
"notes": "Rama true = contacto conocido (usa su id y tenant). Rama false = primer mensaje: crearlo."
},
{
"parameters": {
"resource": "row",
"operation": "create",
"tableId": "contactos",
"dataToSend": "defineBelow",
"fieldsUi": {
"fieldValues": [
{
"fieldId": "telefono",
"fieldValue": "={{ $json.contact_phone }}"
},
{
"fieldId": "nombre",
"fieldValue": "={{ null }}"
},
{
"fieldId": "tenant_id",
"fieldValue": "={{ $env.TENANT_ID }}"
}
]
}
},
"id": "2d728fb1-3c2c-4223-810e-5bdc45c0dc29",
"name": "Crear Contacto",
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
1440,
200
],
"notes": "Crea el contacto con el TENANT_ID del .env (primer mensaje del cliente)."
},
{
"parameters": {
"resource": "row",
"operation": "getAll",
"tableId": "conversaciones",
"returnAll": false,
"limit": 20,
"orderBy": "created_at.desc",
"filterType": "manual",
"matchType": "allFilters",
"filters": {
"conditions": [
{
"keyName": "contacto_id",
"condition": "eq",
"keyValue": "={{ $('Resolver Contacto').item.json.contacto_id }}"
}
]
}
},
"id": "f7199594-f6ff-40fd-8372-1641f94da55f",
"name": "Obtener Historial",
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
1440,
-40
],
"notes": "Ultimas 20 conversaciones del contacto (mas recientes primero) para contexto de Gemini."
},
{
"parameters": {
"resource": "row",
"operation": "create",
"tableId": "conversaciones",
"dataToSend": "defineBelow",
"fieldsUi": {
"fieldValues": [
{
"fieldId": "tenant_id",
"fieldValue": "={{ $('Resolver Contacto').item.json.tenant_id }}"
},
{
"fieldId": "contacto_id",
"fieldValue": "={{ $('Resolver Contacto').item.json.contacto_id }}"
},
{
"fieldId": "role",
"fieldValue": "user"
},
{
"fieldId": "content",
"fieldValue": "={{ $('Normalizar Mensaje').item.json.message_text }}"
}
]
}
},
"id": "da0528a2-b0bc-4a65-85db-22d574d3fbf8",
"name": "Guardar Mensaje Usuario",
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
1680,
80
],
"notes": "Persiste el mensaje del usuario antes de llamar a Gemini."
},
{
"parameters": {
"jsCode": "// Historial seguro (try/catch + normalizacion) + mensaje nuevo.\nlet historyRows = [];\ntry { historyRows = $('Obtener Historial').all().map(i => i.json).flat(1).filter(r => r && typeof r === 'object' && !Array.isArray(r)); } catch (e) {}\nconst contents = historyRows.map(h => ({ role: h.role === 'assistant' ? 'model' : 'user', parts: [{ text: h.content }] }));\ncontents.push({ role: 'user', parts: [{ text: $('Normalizar Mensaje').item.json.message_text }] });\nconst systemInstruction = $env.SYSTEM_PROMPT || 'Eres el asistente virtual del negocio. Responde corto y claro.';\nreturn [{ json: { model: 'gemini-2.5-flash', contents, system_instruction: { parts: [{ text: systemInstruction }] } } }];"
},
"id": "7408b250-c516-475e-b19a-f651eccbf83d",
"name": "Preparar Contexto Gemini",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1920,
80
],
"notes": "Construye el body de Gemini con historial + mensaje nuevo. SYSTEM_PROMPT viene del .env (requiere N8N_BLOCK_ENV_ACCESS_IN_NODE=false)."
},
{
"parameters": {
"method": "POST",
"url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json }}",
"options": {},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "x-goog-api-key",
"value": "={{ $env.GEMINI_API_KEY }}"
}
]
}
},
"id": "1068693f-2785-48b8-923b-86ee954e6021",
"name": "Gemini Responder",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2160,
80
],
"notes": "Free tier: sin tarjeta. Si cambias de modelo (DeepSeek, Claude), solo cambia esta URL y el modelo en 'Preparar Contexto Gemini'."
},
{
"parameters": {
"jsCode": "// Extract text from Gemini response and clean control markers\nconst text = ($json.candidates && $json.candidates[0] && $json.candidates[0].content && $json.candidates[0].content.parts[0].text) || '';\nconst clean = text.replace(/\\[DERIVAR\\]/gi, '').replace(/\\[LEAD[^\\]]*\\]/gi, '').trim().slice(0, 4000);\nreturn [{ json: { raw: text, clean, needs_human: /\\[DERIVAR\\]/i.test(text), lead_marker: (text.match(/\\[LEAD ([^\\]]*)\\]/i) || [null, ''])[1] } }];"
},
"id": "433f1a6c-544c-408e-b925-087ec7c0f3c9",
"name": "Extraer Respuesta",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2400,
80
],
"notes": "Limpia los marcadores [DERIVAR] y [LEAD ...] de la respuesta y detecta si requiere humano."
},
{
"parameters": {
"resource": "row",
"operation": "create",
"tableId": "conversaciones",
"dataToSend": "defineBelow",
"fieldsUi": {
"fieldValues": [
{
"fieldId": "tenant_id",
"fieldValue": "={{ $('Resolver Contacto').item.json.tenant_id }}"
},
{
"fieldId": "contacto_id",
"fieldValue": "={{ $('Resolver Contacto').item.json.contacto_id }}"
},
{
"fieldId": "role",
"fieldValue": "assistant"
},
{
"fieldId": "content",
"fieldValue": "={{ $json.clean }}"
}
]
}
},
"id": "f97f29fa-c196-431b-b08c-be2d853a18af",
"name": "Guardar Respuesta IA",
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
2640,
80
],
"notes": "Persiste la respuesta del asistente (texto limpio, sin marcadores)."
},
{
"parameters": {
"conditions": [
{
"id": "1",
"leftValue": "={{ $('Extraer Respuesta').item.json.needs_human ? 'true' : 'false' }}",
"rightValue": "true",
"operator": "string"
}
],
"options": {}
},
"id": "beb32019-26a8-4857-bd41-128a1638d485",
"name": "IF Derivar a Humano",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
2880,
80
],
"notes": "Rama true = notifica al humano por Telegram. Rama false = responde directo."
},
{
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "7309831214",
"text": "Derivacion a humano. Contacto: {{ $('Normalizar Mensaje').item.json.contact_phone }}. Respuesta IA: {{ $('Extraer Respuesta').item.json.clean }}. Atender en el panel.",
"additionalFields": {}
},
"id": "7c4cad52-c9c6-4d6b-83ad-9ff2c2c8e654",
"name": "Notificar Humano",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
3120,
-40
],
"notes": "Omitido el Update Row derivado_a_humano=true para mantener el flujo simple; si se requiere auditoria, anadir un nodo Update Row sobre la conversacion del usuario filtrando por content."
},
{
"parameters": {
"resource": "message",
"operation": "send",
"messageType": "text",
"recipientPhoneNumber": "={{ $('Normalizar Mensaje').item.json.contact_phone }}",
"textBody": "={{ $('Extraer Respuesta').item.json.clean }}"
},
"id": "a312a38f-4b48-45ad-9ca6-193e35140066",
"name": "Enviar WhatsApp",
"type": "n8n-nodes-base.whatsApp",
"typeVersion": 1.1,
"position": [
3120,
200
],
"notes": "Envia la respuesta del asistente via WhatsApp Business Cloud. El 'Sender Phone Number' se selecciona en el nodo (carga los numeros del Business Account de la credencial whatsAppApi)."
},
{
"parameters": {
"jsCode": "// Verify Meta webhook signature (X-Hub-Signature-256) with the app secret.\n// Dev (WEBHOOK_VERIFY_SIGNATURE=false): pasa sin verificar (modo disabled).\n// Produccion: HMAC-SHA256 via WebCrypto (sin require, compatible con el sandbox de n8n).\nconst mode = String($env.WEBHOOK_VERIFY_SIGNATURE || 'false').toLowerCase();\nconst passthrough = (verified, m) => [{ json: Object.assign({ verified, mode: m }, $input.first().json) }];\n\nif (mode === 'false') {\n return passthrough(true, 'disabled');\n}\n\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);\n\nif (!secret) {\n return passthrough(false, 'missing_secret');\n}\n\nconst run = async () => {\n try {\n const encoder = new TextEncoder();\n const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);\n const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(rawBody));\n const hex = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('');\n return passthrough(signature === 'sha256=' + hex, 'enabled');\n } catch (e) {\n return passthrough(false, 'crypto_unavailable');\n }\n};\nreturn run();"
},
"id": "783bf572-e631-42a6-bc63-89ff22d371d9",
"name": "Verificar Firma Meta",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
240,
240
],
"notes": "Verifica X-Hub-Signature-256 con META_APP_SECRET. Activa con WEBHOOK_VERIFY_SIGNATURE=true. Devuelve el payload original (Object.assign) para que los nodos siguientes no lo pierdan. En dev queda desactivada."
},
{
"parameters": {
"conditions": [
{
"id": "1",
"leftValue": "={{ $json.verified ? 'true' : 'false' }}",
"rightValue": "true",
"operator": "string"
}
],
"options": {}
},
"id": "5f218d74-a366-4a8f-ab77-e427411fe71c",
"name": "IF Firma OK",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
480,
240
],
"notes": "Rama true = firma valida (o verificacion desactivada en dev) -> Normalizar Mensaje. Rama false = rechazo 401."
},
{
"parameters": {
"respondWith": "text",
"responseBody": "unauthorized",
"options": {
"statusCode": 401
}
},
"id": "ac44e27d-ffd9-4163-862d-f08b9745bbab",
"name": "Responder Rechazo",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
720,
240
],
"notes": "Responde 401 cuando la firma HMAC no es valida."
},
{
"parameters": {
"jsCode": "// Normaliza el resultado de Obtener Contacto (items-fila o 1 item-array) y emite SIEMPRE 1 item.\nconst raw = $('Obtener Contacto').all().map(i => i.json);\nconst rows = raw.flat(1).filter(r => r && typeof r === 'object' && !Array.isArray(r));\nconst telefono = $('Normalizar Mensaje').item.json.contact_phone || '';\nreturn [{ json: { exists: rows.length > 0, contacto: rows[0] || null, telefono } }];"
},
"id": "eval-contacto-0001",
"name": "Evaluar Contacto",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
480,
300
],
"notes": "Convierte el resultado de Obtener Contacto (0 o 1 filas) en un item unico { exists, contacto }."
},
{
"parameters": {
"jsCode": "// Unifica el contacto existente o el recien creado; try/catch porque los nodos pueden no haberse ejecutado.\nlet fromEval = {};\ntry { fromEval = $('Evaluar Contacto').all().map(i => i.json)[0] || {}; } catch (e) {}\nlet fromCreate = {};\ntry { fromCreate = $('Crear Contacto').all().map(i => i.json)[0] || {}; } catch (e) {}\nconst c = (fromEval.contacto && fromEval.contacto.id ? fromEval.contacto : fromCreate) || {};\nreturn [{ json: { contacto_id: c.id || null, tenant_id: c.tenant_id || null, telefono: fromEval.telefono || '' } }];"
},
"id": "resolver-contacto-0001",
"name": "Resolver Contacto",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
960,
260
],
"notes": "Unifica el contacto existente o el creado; siempre emite 1 item con contacto_id y tenant_id."
}
],
"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
}
]
]
},
"Normalizar Mensaje": {
"main": [
[
{
"node": "IF Mensaje V\u00e1lido",
"type": "main",
"index": 0
}
]
]
},
"IF Mensaje V\u00e1lido": {
"main": [
[
{
"node": "Obtener Contacto",
"type": "main",
"index": 0
},
{
"node": "Evaluar Contacto",
"type": "main",
"index": 0
}
]
]
},
"IF Contacto Existe": {
"main": [
[
{
"node": "Resolver Contacto",
"type": "main",
"index": 0
}
],
[
{
"node": "Crear Contacto",
"type": "main",
"index": 0
}
]
]
},
"Crear Contacto": {
"main": [
[
{
"node": "Resolver Contacto",
"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": "IF Derivar a Humano",
"type": "main",
"index": 0
}
]
]
},
"IF Derivar a Humano": {
"main": [
[
{
"node": "Notificar Humano",
"type": "main",
"index": 0
}
],
[
{
"node": "Enviar WhatsApp",
"type": "main",
"index": 0
}
]
]
},
"Notificar Humano": {
"main": [
[
{
"node": "Enviar WhatsApp",
"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
}
]
]
},
"Evaluar Contacto": {
"main": [
[
{
"node": "IF Contacto Existe",
"type": "main",
"index": 0
}
]
]
},
"Resolver Contacto": {
"main": [
[
{
"node": "Obtener Historial",
"type": "main",
"index": 0
},
{
"node": "Guardar Mensaje Usuario",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"active": true,
"tags": []
}
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 supabase, httpRequest, telegram, whatsApp. Webhook trigger; 22 nodes.
Source: https://github.com/JeanCardozo/chatAI/blob/main/workflows/whatsapp-assistant-base.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 httpRequest, supabase, whatsApp. Webhook trigger; 20 nodes.
WhatsApp Assistant Base - P1. Uses httpRequest, supabase, whatsApp. Webhook trigger; 20 nodes.
🤖 Telegram Messaging Agent for Text/Audio/Images. Uses telegram, stickyNote, httpRequest, lmChatOpenAi. Webhook trigger; 39 nodes.
🤖 Telegram Messaging Agent for Text/Audio/Images. Uses telegram, stickyNote, httpRequest, lmChatOpenAi. Webhook trigger; 39 nodes.
This workflow is designed to monitor the Top 5 cryptocurrencies in real-time, calculate trading signals (BUY, SELL, HOLD), and send human-readable alerts through multiple channels. It integrates data