AutomationFlows β€Ί AI & RAG β€Ί Labr - Nuevo Asistente (reparado)

Labr - Nuevo Asistente (reparado)

Original n8n title: πŸ§ͺ Labr - Nuevo Asistente (reparado)

πŸ§ͺ LABR - nuevo asistente (REPARADO). Uses httpRequest, postgres, postgresTool, toolCalculator. Webhook trigger; 63 nodes.

Webhook triggerβ˜…β˜…β˜…β˜…β˜… complexityAI-powered63 nodesHTTP RequestPostgresPostgres ToolTool CalculatorMemory Buffer WindowTool Http RequestTool CodeAgent
AI & RAG Trigger: Webhook Nodes: 63 Complexity: β˜…β˜…β˜…β˜…β˜… AI nodes: yes Added:
Labr - Nuevo Asistente (reparado) β€” n8n workflow card showing HTTP Request, Postgres, Postgres Tool integration

This workflow follows the Agent β†’ 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 β†’

Download .json
{
  "updatedAt": "2026-03-30T22:18:19.769Z",
  "createdAt": "2026-03-09T14:05:16.356Z",
  "id": "sNeOUViiSYyROtea",
  "name": "\ud83e\uddea LABR - nuevo asistente (REPARADO)",
  "description": null,
  "active": true,
  "isArchived": false,
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "ycloud-lab-test-v2",
        "options": {}
      },
      "id": "2daa57db-e4ab-4696-a96a-4b4cad12b8fc",
      "name": "\ud83d\udce5 Webhook YCloud",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -528,
        1200
      ],
      "typeVersion": 2
    },
    {
      "parameters": {
        "respondWith": "text",
        "responseBody": "OK",
        "options": {}
      },
      "id": "e0703b6d-d536-4e7f-bd8a-6c0adf2969e4",
      "name": "\u2705 Responder OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -160,
        976
      ],
      "typeVersion": 1.4
    },
    {
      "parameters": {
        "jsCode": "// ======================================================\n// \ud83d\udee1\ufe0f FIX: Verificar que el nodo se ejecut\u00f3 antes de acceder\n// ======================================================\nlet cliente = {};\ntry {\n  cliente = $('2. Obtener Cliente').first().json || {};\n} catch (e) {\n  cliente = { id: null, telefono: '', nombre: 'Cliente' };\n}\n\n// ======================================================\n// \ud83e\udde0 CONTIN\u00daA CON EL RESTO DEL C\u00d3DIGO ORIGINAL\n// ======================================================\n\n// \ud83e\udde0 PRE-PROCESAMIENTO v15 - BLOQUEO DE ESCALADOS\n// =====================================================\n// \u00daltima actualizaci\u00f3n: 2026-02-14\n// NUEVO: Bloqueo autom\u00e1tico de clientes escalados\n// =====================================================\n\nconst rawJson = $input.item.json;\nconst body = rawJson.body || rawJson;\nlet whatsappMsg = body.whatsappInboundMessage;\n\nif (!whatsappMsg && body.body && body.body.whatsappInboundMessage) {\n  whatsappMsg = body.body.whatsappInboundMessage;\n}\n\n// =====================================================\n// \ud83d\udee1\ufe0f DETECTAR MODO ADMIN/CLIENTE (MAURICIO)\n// =====================================================\nconst NUMEROS_DIRECTOR = ['573203062007', '3203062007'];\n\nconst fromRaw = whatsappMsg?.from || '';\nconst fromNormalizado = fromRaw.replace(/[\\+\\s-]/g, '');\nconst from = (whatsappMsg?.from || '').replace('+', '');\nconst to = (whatsappMsg?.to || '').replace('+', '');\nconst customerName = whatsappMsg?.customerProfile?.name || 'Cliente';\nconst messageType = whatsappMsg?.type || 'unknown';\nconst messageTextRaw = whatsappMsg?.text?.body || '';\nconst messageText = (NUMEROS_DIRECTOR.some(num => fromNormalizado === num || fromNormalizado.endsWith(num))) && (messageTextRaw.trim().startsWith('>')) ? messageTextRaw.trim().substring(1).trim() : messageTextRaw;\n\n// Saludo seg\u00fan hora (COLOMBIA UTC-5)\nconst now = new Date();\nconst horaCol = new Date(now.getTime() - 5 * 60 * 60 * 1000).getHours();\nlet saludo;\nif (horaCol < 12) saludo = 'Buenos d\u00edas';\nelse if (horaCol < 19) saludo = 'Buenas tardes';\nelse saludo = 'Buenas noches';\n\nconst esNumeroDirector = NUMEROS_DIRECTOR.some(num =>\n    fromNormalizado === num || fromNormalizado.endsWith(num)\n);\n\n// =====================================================\n// \ud83d\udd04 PREFIJO: > para modo CLIENTE\n// =====================================================\nconst mensajeEmpiezaConPrefijo = messageTextRaw.trim().startsWith('>');\nconst mensajeSinPrefijo = mensajeEmpiezaConPrefijo\n    ? messageTextRaw.trim().substring(1).trim()\n    : messageTextRaw;\n\n// =====================================================\n// \ud83d\udeab NUEVO: BLOQUEO DE CLIENTES ESCALADOS\n// =====================================================\n// IMPORTANTE: Si el cliente est\u00e1 escalado, NO procesar el mensaje\n// El cliente debe esperar respuesta humana\nconst ESTADOS_ESCALADOS = ['ESCALADO', 'PENDIENTE_HUMANO', 'EN_REVISION_HUMANA'];\n\n// Variable para guardar si el mensaje fue bloqueado\nlet mensajeBloqueado = false;\n\n// =====================================================\n// DETECTAR RESPUESTAS DE BOTONES INTERACTIVOS\n// =====================================================\nlet esRespuestaBoton = false;\nlet botonId = '';\nlet botonTexto = '';\nif (messageType === 'interactive') {\n    const interactive = whatsappMsg.interactive;\n    if (interactive?.type === 'button_reply') {\n        esRespuestaBoton = true;\n        botonId = interactive.button_reply?.id || '';\n        botonTexto = interactive.button_reply?.title || '';\n    } else if (interactive?.type === 'list_reply') {\n        esRespuestaBoton = true;\n        botonId = interactive.list_reply?.id || '';\n        botonTexto = interactive.list_reply?.title || '';\n    }\n}\n\n// =====================================================\n// MANEJO DE IM\u00c1GENES Y MEDIA\n// =====================================================\nconst esReaccion = messageType === 'reaction';\nconst esImagen = messageType === 'image';\nconst esDocumento = messageType === 'document';\nconst esAudio = messageType === 'audio';\nconst esVideo = messageType === 'video';\nconst esSticker = messageType === 'sticker';\nconst esMedia = esImagen || esDocumento || esAudio || esVideo || esSticker;\n\n// =====================================================\n// MANEJO DE REACCIONES (ESTADOS)\n// =====================================================\nif (esReaccion) {\n    const emoji = whatsappMsg.reaction?.emoji || '\u2764\ufe0f';\n    return [{\n        json: {\n            esComandoCopiloto: false,\n            mensajeCliente: `[Reacci\u00f3n: ${emoji}]`,\n            from, to, customerName, saludo,\n            esMediaNoSoportado: false,\n            esMedia: false,\n            esReaccion: true,\n            emojiReaccion: emoji,\n            tipoMensaje: 'reaction',\n            messageText: `[El cliente reaccion\u00f3 con ${emoji}]`,\n            esRespuestaBoton: false,\n            botonId: '', botonTexto: '',\n            productoIdDelBoton: null,\n            accionBoton: '',\n            esSoloSaludo: false,\n            esPreguntaConversacional: false,\n            debeHacerBusqueda: false,\n            esPedidoPlataforma: false,\n            infoPedidoPlataforma: null,\n            tieneMultiplesProductos: false,\n            terminosIndividuales: [],\n            terminoBusqueda: '',\n            terminoNormalizado: '',\n            variantesBusqueda: [],\n            terminosNormalizados: [],\n            timestamp: whatsappMsg?.sendTime\n        }\n    }];\n}\n\n// Si es media, no importa si est\u00e1 escalado o no\nif (esMedia) {\n    let textoMedia = '[Mensaje multimedia]';\n    if (esImagen) textoMedia = '[\ud83d\udcf8 Imagen recibida]';\n    else if (esDocumento) textoMedia = '[\ud83d\udcc4 Documento recibido]';\n    else if (esAudio) textoMedia = '[\ud83c\udfb5 Audio recibido]';\n    else if (esVideo) textoMedia = '[\ud83c\udfa5 Video recibido]';\n    else if (esSticker) textoMedia = '[\ud83d\ude0a Sticker recibido]';\n    \n    return [{\n        json: {\n            esComandoCopiloto: false,\n            mensajeCliente: messageText,\n        from, to, customerName, saludo,\n            esMediaNoSoportado: false,\n            esMedia: true,\n            esImagen, esDocumento, esAudio, esVideo, esSticker,\n            tipoMensaje: messageType,\n            messageText: textoMedia,\n            captionImagen: whatsappMsg?.image?.caption ||\n                whatsappMsg?.document?.caption || '',\n            esRespuestaBoton: false,\n            botonId: '', botonTexto: '',\n            productoIdDelBoton: null,\n            accionBoton: '',\n            esSoloSaludo: false,\n            esPreguntaConversacional: false,\n            debeHacerBusqueda: false,\n            esPedidoPlataforma: false,\n            infoPedidoPlataforma: null,\n            tieneMultiplesProductos: false,\n            terminosIndividuales: [],\n            terminoBusqueda: '',\n            terminoNormalizado: '',\n            variantesBusqueda: [],\n            terminosNormalizados: [],\n            timestamp: whatsappMsg?.sendTime\n        }\n    }];\n}\n\n// =====================================================\n// MANEJO DE RESPUESTAS DE BOTONES\n// =====================================================\nif (esRespuestaBoton) {\n    let productoIdDelBoton = null;\n    let accionBoton = 'otro';\n    \n    if (botonId.startsWith('agregar_producto_')) {\n        productoIdDelBoton = parseInt(botonId.replace('agregar_producto_', ''));\n        accionBoton = 'agregar';\n    } else if (botonId.startsWith('ver_mas_producto_')) {\n        productoIdDelBoton = parseInt(botonId.replace('ver_mas_producto_', ''));\n        accionBoton = 'ver_mas';\n    } else if (botonId === 'ver_carrito') {\n        accionBoton = 'ver_carrito';\n    } else if (botonId === 'completar_pedido') {\n        accionBoton = 'completar_pedido';\n    } else if (botonId === 'cancelar_pedido') {\n        accionBoton = 'cancelar_pedido';\n    }\n    \n    return [{\n        json: {\n            esComandoCopiloto: false,\n            mensajeCliente: messageText,\n        from, to, customerName, saludo,\n            esMediaNoSoportado: false,\n            esMedia: false,\n            tipoMensaje: messageType,\n            messageText: botonTexto,\n            esRespuestaBoton: true,\n            botonId, botonTexto,\n            productoIdDelBoton,\n            accionBoton,\n            esSoloSaludo: false,\n            esPreguntaConversacional: false,\n            debeHacerBusqueda: false,\n            esPedidoPlataforma: false,\n            infoPedidoPlataforma: null,\n            tieneMultiplesProductos: false,\n            terminosIndividuales: [],\n            terminoBusqueda: '',\n            terminoNormalizado: '',\n            variantesBusqueda: [],\n            terminosNormalizados: [],\n            timestamp: whatsappMsg?.sendTime\n        }\n    }];\n}\n\n// =====================================================\n// VALIDAR MENSAJE DE TEXTO\n// =====================================================\nif (!whatsappMsg || messageType !== 'text') {\n    return [{\n        json: {\n            esComandoCopiloto: false,\n            mensajeCliente: messageText,\n        from, to, customerName, saludo,\n            esMediaNoSoportado: true,\n            esMedia: false,\n            tipoMensaje: messageType,\n            messageText: '',\n            esRespuestaBoton: false,\n            botonId: '', botonTexto: '',\n            productoIdDelBoton: null,\n            accionBoton: '',\n            esSoloSaludo: false,\n            esPreguntaConversacional: false,\n            debeHacerBusqueda: false,\n            esPedidoPlataforma: false,\n            infoPedidoPlataforma: null,\n            tieneMultiplesProductos: false,\n            terminosIndividuales: [],\n            terminoBusqueda: '',\n            terminoNormalizado: '',\n            variantesBusqueda: [],\n            terminosNormalizados: [],\n            timestamp: whatsappMsg?.sendTime\n        }\n    }];\n}\n\n// =====================================================\n// \ud83d\udeab NUEVO: VERIFICAR SI EST\u00c1 ESCALADO\n// =====================================================\n// Obtener estado del cliente (si viene del input) - CON SEGURIDAD\nlet clienteInput = {};\ntry {\n    clienteInput = $('2. Obtener Cliente').first().json || {};\n} catch (e) {\n    clienteInput = {};\n}\n\n// Verificar si el cliente est\u00e1 escalado\nif (clienteInput && clienteInput.estado_conversacion && \n    ESTADOS_ESCALADOS.includes(clienteInput.estado_conversacion)) {\n    \n    console.log('\ud83d\udeab Cliente escalado detectado. Bloqueando respuesta autom\u00e1tica.');\n    console.log(`\ud83d\udcde Cliente: ${from}`);\n    console.log(`\ud83d\udcca Estado: ${clienteInput.estado_conversacion}`);\n    console.log(`\ud83d\udcdd Mensaje: ${messageTextRaw}`);\n    \n    // \u2705 RETORNAR MENSAJE DE ESPERA (NO PROCESAR M\u00c1S)\n    mensajeBloqueado = true;\n    \n    return [{\n        json: {\n            mensajeCliente: messageText,\n        from, to, customerName, saludo,\n            esComandoCopiloto: false,\n            esMediaNoSoportado: false,\n            esMedia: false,\n            tipoMensaje: 'text',\n            messageText: messageTextRaw,\n            esRespuestaBoton: false,\n            botonId: '', botonTexto: '',\n            productoIdDelBoton: null,\n            accionBoton: '',\n            // Indicadores para el siguiente flujo\n            esClienteEscalado: true,\n            estado_conversacion: clienteInput.estado_conversacion,\n            // Marcador para que el flujo sepa que est\u00e1 escalado\n            bloquearRespuesta: true\n        }\n    }];\n}\n\n// =====================================================\n// SI ES DIRECTOR\n// =====================================================\nif (esNumeroDirector) {\n    if (mensajeEmpiezaConPrefijo) {\n        // Contin\u00faa como cliente normal\n    } else {\n        // MODO COPILOTO\n        const normalizedText = messageTextRaw.replace(/[^\\d]/g, '');\n        const phoneMatch = normalizedText.match(/(?:57)?(3\\d{9})/);\n        const telefono_objetivo = phoneMatch ? phoneMatch[1].replace(/^57/, '') : null;\n        \n        let nombre_extraido = null;\n        const nombrePatterns = [\n            /(?:a|al nombre)\\s+[\"']?([A-Za-z\u00c1-\u00ff\\s]+?)[\"']?(?:\\s|$)/i,\n            /(?:nombre es|llama)\\s+[\"']?([A-Za-z\u00c1-\u00ff\\s]+?)[\"']?(?:\\s+y|\\s*$)/i,\n        ];\n        for (const pattern of nombrePatterns) {\n            const match = messageTextRaw.match(pattern);\n            if (match && match[1]) {\n                nombre_extraido = match[1].trim().replace(/\\b(del|cliente|el|la)\\b/gi, '').trim();\n                if (nombre_extraido.length >= 2) break;\n                else nombre_extraido = null;\n            }\n        }\n        \n        return [{\n            json: {\n                esComandoCopiloto: true,\n                mensajeCliente: messageText,\n        from, to, customerName, saludo,\n                messageText: messageTextRaw,\n                prompt: messageTextRaw,\n                mensajeClienteActual: messageTextRaw,\n                telefono_objetivo: telefono_objetivo,\n                nombre_extraido: nombre_extraido,\n                esMediaNoSoportado: false,\n                esMedia: false,\n                tipoMensaje: messageType,\n                timestamp: whatsappMsg?.sendTime\n            }\n        }];\n    }\n}\n\n// =====================================================\n// RESTO DEL C\u00d3DIGO (CLIENTES NORMALES O DIRECTOR CON >)\n// =====================================================\n// messageText movido al inicio\n\n// Detectar saludos simples\nconst esSoloSaludo = [\n    /^hola[,!\\.\\s]*$/i,\n    /^buenos?\\s+(d[i\u00ed]as?|tardes?|noches?)[,!\\.\\s]*$/i,\n    /^buenas[,!\\.\\s]*$/i,\n    /^hey[,!\\.\\s]*$/i,\n    /^qu[e\u00e9]\\s+tal[,!\\.\\s]*$/i,\n    /^c[o\u00f3]mo\\s+est[a\u00e1](s|n)?[,!\\.\\s]*$/i,\n    /^gracias[,!\\.\\s]*$/i,\n    /^ok[,!\\.\\s]*$/i,\n    /^s[i\u00ed][,!\\.\\s]*$/i,\n    /^no[,!\\.\\s]*$/i,\n    /^perfecto[,!\\.\\s]*$/i,\n    /^listo[,!\\.\\s]*$/i,\n    /^dale[,!\\.\\s]*$/i,\n    /^bien[,!\\.\\s]*$/i\n].some(p => p.test(messageText));\n\n// Detectar pedido desde plataforma\nconst esPedidoPlataforma = [\n    /acabo\\s+de\\s+hacer\\s+un\\s+pedido/i,\n    /hice\\s+un\\s+pedido\\s+en\\s+(la\\s+)?tienda/i,\n    /tus-aguacates\\.vercel\\.app/i,\n    /mi\\s+pedido:.*\\n.*total/is\n].some(p => p.test(messageText));\n\nlet infoPedidoPlataforma = null;\nif (esPedidoPlataforma) {\n    const nombreMatch = messageText.match(/me\\s+llamo\\s+(\\w+)/i);\n    const pagoMatch = messageText.match(/pago:\\s*(.+?)(\\n|$)/i);\n    const totalMatch = messageText.match(/total:\\s*\\$?([.\\d,]+)/i);\n    infoPedidoPlataforma = {\n        nombre: nombreMatch ? nombreMatch[1] : null,\n        metodoPago: pagoMatch ? pagoMatch[1].trim() : null,\n        total: totalMatch ? totalMatch[1] : null\n    };\n}\n\n// Detectar preguntas conversacionales\nconst esPreguntaConversacional = [\n    /c[o\u00f3]mo\\s+(veo|entro|accedo|visito|llego|abro)/i,\n    /d[o\u00f3]nde\\s+(est[a\u00e1]|queda|encuentro|veo)/i,\n    /tienda\\s+(en\\s+)?l[i\u00ed]nea/i,\n    /p[a\u00e1]gina\\s+web/i,\n    /\\b(link|enlace|url)\\b/i,\n    /\\bhorario/i,\n    /a\\s+qu[e\u00e9]\\s+hora/i,\n    /hasta\\s+qu[e\u00e9]\\s+hora/i,\n    /\\benv[i\u00ed]o/i,\n    /\\bentrega/i,\n    /\\bdomicilio/i,\n    /hacen\\s+env[i\u00ed]o/i,\n    /zona\\s+de\\s+(cobertura|entrega)/i,\n    /m[e\u00e9]todo(s)?\\s+de\\s+pago/i,\n    /aceptan\\s+(tarjeta|nequi|daviplata|efectivo)/i,\n    /puedo\\s+pagar\\s+con/i,\n    /\\bcontacto/i,\n    /\\bubicaci[o\u00f3]n/i,\n    /\\bdirecci[o\u00f3]n/i,\n    /d[o\u00f3]nde\\s+est[a\u00e1]n/i,\n    /mi\\s+pedido/i,\n    /qu[e\u00e9]\\s+llevo/i,\n    /eso\\s+es\\s+todo/i,\n    /total\\s+(del\\s+)?pedido/i,\n    /cu[a\u00e1]nto\\s+es\\s+(el\\s+)?total/i,\n    /cu[a\u00e1]nto\\s+te\\s+debo/i\n].some(p => p.test(messageText));\n\n// =====================================================\n// \ud83d\udd24 EXTRACCI\u00d3N DE T\u00c9RMINO DE B\u00daSQUEDA - v14 ENRIQUECIDO\n// =====================================================\nconst palabrasIgnorar = new Set([\n    // === PALABRAS DE PRUEBA ===\n    'test', 'prueba', 'testing', 'probando', 'probar',\n    // === PREGUNTAS ===\n    'cuales', 'cu\u00e1les', 'cual', 'cu\u00e1l', 'donde', 'd\u00f3nde',\n    'cuando', 'cu\u00e1ndo', 'porque', 'porqu\u00e9', 'como', 'c\u00f3mo',\n    // === VERBOS PODER (todas conjugaciones) ===\n    'poder', 'puedo', 'puedes', 'puede', 'podemos', 'pueden',\n    'podr\u00eda', 'podr\u00edas', 'podr\u00edamos', 'podr\u00edan',\n    'pudiera', 'pudieras', 'pudieran', 'pude', 'pudiste',\n    // === VERBOS QUERER (todas conjugaciones) ===\n    'querer', 'quiero', 'quieres', 'quiere', 'queremos', 'quieren',\n    'quisiera', 'quisieras', 'quisi\u00e9ramos', 'quisieran',\n    'querr\u00eda', 'querr\u00edas', 'querr\u00edamos', 'querr\u00edan',\n    // === VERBOS AGREGAR/A\u00d1ADIR (todas conjugaciones) ===\n    'agregar', 'agrego', 'agregas', 'agrega', 'agregamos', 'agregan',\n    'agr\u00e9game', 'agregarme', 'agregarle', 'agr\u00e9gale',\n    'a\u00f1adir', 'a\u00f1ado', 'a\u00f1ades', 'a\u00f1ade', 'a\u00f1adimos', 'a\u00f1aden',\n    'a\u00f1ademe', 'a\u00f1adirme', 'a\u00f1adirle',\n    // === VERBOS DAR (todas conjugaciones) ===\n    'dar', 'doy', 'das', 'da', 'damos', 'dan',\n    'dame', 'deme', 'darme', 'darle', 'd\u00e1melo',\n    'd\u00e1ndome', 'd\u00e1ndole',\n    // === VERBOS TRAER/ENVIAR ===\n    'traer', 'traigo', 'traes', 'trae', 'traemos', 'traen',\n    'traeme', 'traerme', 'traerle',\n    'enviar', 'env\u00edo', 'env\u00edas', 'env\u00eda', 'enviamos', 'env\u00edan',\n    'enviame', 'enviarme', 'enviarle', 'env\u00edame',\n    // === VERBOS PONER/METER ===\n    'poner', 'pongo', 'pones', 'pone', 'ponemos', 'ponen',\n    'ponme', 'ponerle',\n    'meter', 'meto', 'metes', 'mete', 'metemos', 'meten',\n    'meteme', 'meterme', 'meterle',\n    // === VERBOS CONSEGUIR ===\n    'conseguir', 'consigo', 'consigues', 'consigue', 'conseguimos', 'consiguen',\n    'conseguirme', 'conseguirle',\n    // === VERBOS TENER/HABER ===\n    'tener', 'tengo', 'tienes', 'tiene', 'tenemos', 'tienen',\n    'tendr\u00eda', 'tendr\u00edas', 'tendr\u00edamos', 'tendr\u00edan', 'tendr\u00e1n',\n    'haber', 'hay', 'hab\u00eda', 'habr\u00e1', 'habr\u00eda', 'habr\u00edamos', 'habr\u00edan',\n    // === VERBOS BUSCAR ===\n    'buscar', 'busco', 'buscas', 'busca', 'buscamos', 'buscan',\n    'buscando', 'b\u00fascame', 'buscarme',\n    // === VERBOS VENDER/MANEJAR ===\n    'vender', 'vendo', 'vendes', 'vende', 'vemos', 'venden',\n    'manejar', 'manejo', 'manejas', 'maneja', 'manejamos', 'manejan',\n    'ofrecer', 'ofrezco', 'ofreces', 'ofrece', 'ofrecemos', 'ofrecen',\n    // === VERBOS NECESITAR ===\n    'necesitar', 'necesito', 'necesitas', 'necesita', 'necesitamos', 'necesitan',\n    // === VERBOS LLEVAR/COMPRAR/PEDIR ===\n    'llevar', 'llevo', 'llevas', 'leva', 'llevamos', 'levan',\n    'comprar', 'compro', 'compras', 'compra', 'compramos', 'compran',\n    'pedir', 'pido', 'pides', 'pide', 'pedimos', 'piden',\n    'pedido', 'pedidos',\n    // === VERBOS SER/ESTAR ===\n    'ser', 'soy', 'eres', 'es', 'somos', 'son',\n    'era', 'fue', 'era', 'estoy', 'est\u00e1s', 'est\u00e1', 'estamos', 'est\u00e1n',\n    'ser\u00eda', 'ser\u00edas', 'ser\u00edamos', 'ser\u00edan',\n    'estar\u00eda', 'estar\u00edas', 'estar\u00edamos', 'estar\u00edan',\n    // === VERBOS VER/MOSTRAR ===\n    'ver', 'veo', 'ves', 've', 'vemos', 'ven',\n    'mostrar', 'muestro', 'muestras', 'muestra', 'mostramos', 'muestran',\n    'ense\u00f1ar', 'ense\u00f1o', 'ense\u00f1as', 'ense\u00f1a', 'ense\u00f1amos', 'ense\u00f1an',\n    'mirar', 'miro', 'miras', 'mira', 'miramos', 'miran',\n    // === ART\u00cdCULOS ===\n    'el', 'la', 'los', 'las', 'un', 'una', 'unos', 'unas',\n    'este', 'esta', 'estos', 'estas', 'ese', 'esa',\n    'aquel', 'aquella', 'aquellos', 'aquellas',\n    // === PRONOMBRES DEMOSTRATIVOS ===\n    'este', 'esta', 'estos', 'estas', 'ese', 'esa',\n    'aquel', 'aquella', 'aquellos', 'aquellas',\n    // === PRONOMBRES PERSONALES ===\n    'yo', 'tu', 't\u00fa', 'usted', '\u00e9l', 'ella', 'nosotros', 'ustedes', 'ellos', 'ellas',\n    'me', 'te', 'se', 'le', 'lo', 'la', 'les',\n    // === PREPOSICIONES ===\n    'de', 'del', 'a', 'al', 'para', 'por', 'con', 'sin', 'hacia', 'desde', 'entre', 'sobre', 'en',\n    // === INTERROGATIVOS ===\n    'que', 'qu\u00e9', 'cu\u00e1l', 'cu\u00e1les', 'd\u00f3nde', 'cu\u00e1ndo', 'cu\u00e1ndo', 'cu\u00e1nto', 'cu\u00e1nta',\n    'cu\u00e1ntos', 'cu\u00e1ntas', 'c\u00f3mo', 'c\u00f3mo', 'cu\u00e1l',\n    // === PRECIOS ===\n    'vale', 'valen', 'cuesta', 'cuestan', 'costar', 'costar\u00eda', 'precio', 'precios', 'costo', 'costos',\n    'valor', 'valores',\n    // === SALUDOS ===\n    'hola', 'buenos', 'buenas', 'hey', 'oye', 'oiga', 'disculpa', 'disculpe', 'perd\u00f3n', 'perdon',\n    'gracias', 'muchas', 'mil', 'please', 'plz', 'plis',\n    // === CORTES\u00cdA ===\n    'm\u00e1s', 'mas', 'menos', 'muy', 'mucho', 'mucha', 'poco', 'poca', 'tambi\u00e9n', 'tambien', 'adem\u00e1s', 'ademas', 'incluso',\n    'bueno', 'buena', 'buenos', 'buenas', 'mejor', 'mejores', 'peor', 'peores', 'grande', 'peque\u00f1o', 'peque\u00f1a',\n    'fresco', 'fresca', 'frescos', 'frescas',\n    // === ADJETIVOS/ADVERBIOS ===\n    'm\u00e1s', 'mas', 'menos', 'muy', 'mucho', 'mucha', 'poco', 'poca', 'tambi\u00e9n', 'tambien', 'adem\u00e1s', 'ademas', 'incluso',\n    'bueno', 'buena', 'buenos', 'buenas', 'mejor', 'mejores', 'peor', 'peores', 'grande', 'peque\u00f1o', 'peque\u00f1a',\n    'fresco', 'fresca', 'frescos', 'frescas',\n    // === N\u00daMEROS ===\n    'uno', 'dos', 'tres', 'cuatro', 'cinco', 'seis', 'siete', 'ocho', 'nueve', 'diez',\n    'primero', 'segundo', 'tercero', 'cuarto', 'quinto', 'sexto', 's\u00e9ptimo', 'octavo', 'noveno', 'd\u00e9cimo',\n    // === UNIDADES ===\n    'kilo', 'kilos', 'gramos', 'grs', 'libra', 'libras', 'unidad', 'unidades', 'paquete', 'paquetes',\n    'bandeja', 'bandejas', 'caja', 'cajas',\n    // === CONECTORES ===\n    'pero', 'aunque', 'entonces', 'pues', 'bueno', 'mira', 'oiga', 'digame', 'd\u00edgame', 'cu\u00e9ntame', 'cuentame',\n    'okay', 'vale', 'listo', 'dale', 'aja', 'aj\u00e1',\n    // === ADVERBIOS TIEMPO/LUGAR ===\n    'solo', 's\u00f3lo', 'solamente', '\u00fanicamente', 'unicamente',\n    'siempre', 'nunca', 'ahora', 'despu\u00e9s', 'despues', 'antes', 'antes',\n    'aqu\u00ed', 'aqui', 'ac\u00e1', 'aca', 'all\u00e1', 'alla', 'ah\u00ed', 'ahi',\n    // === OTRAS PALABRAS COMUNES ===\n    'ok', 'si', 'no', 'perfecto', 'listo', 'bien', 'todo', 'nada', 'alguno', 'alguna',\n    'otro', 'otra', 'otros', 'otras', 'misma', 'mismo', 'mismos', 'mismas',\n    'tal', 'vez', 'general', 'claro', 'casi', 'aprox', 'realmente', 'cierto'\n]);\n\nlet terminoBusqueda = messageText\n    .replace(/[^\\w\\s\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1\u00fc\u00c1-\u00ff\u00cd-\u00da\\s]/g, '')\n    .split(/\\s+/)\n    .filter(w => w.length > 2 && !palabrasIgnorar.has(w.toLowerCase()))\n    .join(' ')\n    .trim();\n\n// Detectar m\u00faltiples productos\nconst separadores = /\\s+(?:y|o|,)\\s+/gi;\nconst tieneMultiplesProductos = separadores.test(terminoBusqueda);\n\nlet terminosIndividuales = [];\nif (tieneMultiplesProductos) {\n    terminosIndividuales = terminoBusqueda\n        .split(/s*(?:y|o|,)\\s*/gi)\n        .map(t => t.trim())\n        .filter(t => t.length > 0);\n} else if (terminoBusqueda) {\n    terminosIndividuales = [terminoBusqueda];\n}\n\n// NORMALIZACI\u00d3N DE PLURALES\nlet terminoNormalizado = terminoBusqueda;\nlet variantesBusqueda = [];\nlet terminosNormalizados = [];\n\nterminosIndividuales.forEach(termino => {\n    const palabras = termino.split(' ');\n    const palabrasNormalizadas = palabras.map(palabra => {\n        if (palabra.endsWith('s') && palabra.length > 3) {\n            return palabra.slice(0, -1);\n        }\n        if (palabra.endsWith('es') && palabra.length > 4) {\n            return palabra.slice(0, -2);\n        }\n        return palabra;\n    });\n    \n    const terminoNormalizado = palabrasNormalizadas.join(' ');\n    terminosNormalizados.push(terminoNormalizado);\n    \n    // Mantener el t\u00e9rmino original tambi\u00e9n\n    if (!variantesBusqueda.includes(termino)) {\n        variantesBusqueda.push(termino);\n    }\n    if (!variantesBusqueda.includes(terminoNormalizado)) {\n        variantesBusqueda.push(terminoNormalizado);\n    }\n});\n\nterminoNormalizado = terminosNormalizados.join(' ');\nvariantesBusqueda = [...new Set(variantesBusqueda)];\n\n// DECISI\u00d3N DE B\u00daSQUEDA\nconst debeHacerBusqueda =\n    !esSoloSaludo &&\n    !esPreguntaConversacional &&\n    terminoBusqueda.length > 0;\n\n// RESULTADO FINAL (CLIENTE NORMAL)\nreturn [{\n    json: {\n        esComandoCopiloto: false,\n        mensajeCliente: messageText,\n        from, to, customerName, saludo,\n        esMediaNoSoportado: false,\n        esMedia: false,\n        esImagen: false,\n        esDocumento: false,\n        esAudio: false,\n        esVideo: false,\n        esSticker: false,\n        tipoMensaje: 'text',\n        captionImagen: '',\n        esRespuestaBoton: false,\n        botonId: '',\n        botonTexto: '',\n        productoIdDelBoton: null,\n        accionBoton: '',\n        esSoloSaludo,\n        esPreguntaConversacional,\n        debeHacerBusqueda,\n        esPedidoPlataforma,\n        infoPedidoPlataforma,\n        tieneMultiplesProductos,\n        terminosIndividuales,\n        terminoBusqueda,\n        terminoNormalizado,\n        variantesBusqueda,\n        timestamp: whatsappMsg.sendTime\n    }\n}];\n"
      },
      "id": "c2d8c4de-9bdd-4b66-bbd8-abac56aa9c8f",
      "name": "1. Pre-procesamiento YCloud",
      "type": "n8n-nodes-base.code",
      "position": [
        -224,
        1440
      ],
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "es-media",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.esMediaNoSoportado }}",
              "rightValue": true
            }
          ]
        },
        "options": {}
      },
      "id": "2ce881a6-e0cc-4836-91ab-c43a398a3b68",
      "name": "\u2753 \u00bfEs Media?",
      "type": "n8n-nodes-base.if",
      "position": [
        320,
        1168
      ],
      "typeVersion": 2.2
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://httpbin.org/post",
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {}
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        416,
        976
      ],
      "id": "1c1dc511-7909-42f1-89bc-538c2ba4245a",
      "name": "\ud83d\udcf1 Responder Media No Soportado"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH nuevo_cliente AS (\n  INSERT INTO clientes (telefono, nombre, estado_conversacion)\n  SELECT\n    '{{ $json.from }}',\n    NULL,\n    'NUEVO'\n  WHERE NOT EXISTS (\n    SELECT 1 FROM clientes WHERE telefono = '{{ $json.from }}'\n  )\n  RETURNING id\n), candidatos AS (\n  SELECT\n    id,\n    telefono,\n    nombre,\n    direccion,\n    estado_conversacion,\n    pre_pedido,\n    total_pedidos,\n    CASE\n      WHEN COALESCE(jsonb_array_length(COALESCE(pre_pedido, '[]'::jsonb)), 0) > 0 THEN 0\n      ELSE 1\n    END AS prioridad_carrito\n  FROM clientes\n  WHERE telefono = '{{ $json.from }}'\n)\nSELECT id, telefono, nombre, direccion, estado_conversacion, pre_pedido, total_pedidos\nFROM candidatos\nORDER BY prioridad_carrito ASC, id DESC\nLIMIT 1;",
        "options": {}
      },
      "name": "2. Obtener Cliente",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "id": "61583a03-3ecc-4030-94f8-3acc01b29774",
      "position": [
        464,
        1344
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "es-busqueda",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $('1. Pre-procesamiento YCloud').first().json.debeHacerBusqueda }}",
              "rightValue": true
            }
          ]
        },
        "options": {}
      },
      "id": "9987fdfe-c019-409d-a941-1538ada467b3",
      "name": "\u2753 \u00bfBusca Producto?",
      "type": "n8n-nodes-base.if",
      "position": [
        1152,
        1184
      ],
      "typeVersion": 2.2
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "WITH search_terms AS (\n  SELECT\n    LOWER('{{ $json.terminoBusqueda }}') AS term_original,\n    LOWER(\n      CASE\n        WHEN LOWER('{{ $json.terminoBusqueda }}') IN ('hass', 'aguacate', 'aguacate hass') THEN '{{ $json.terminoBusqueda }}'\n        WHEN LOWER('{{ $json.terminoBusqueda }}') ~ 'es$' AND LENGTH('{{ $json.terminoBusqueda }}') > 4 THEN TRIM(TRAILING 'es' FROM LOWER('{{ $json.terminoBusqueda }}'))\n        WHEN LOWER('{{ $json.terminoBusqueda }}') ~ 's$' AND LENGTH('{{ $json.terminoBusqueda }}') > 3 THEN TRIM(TRAILING 's' FROM LOWER('{{ $json.terminoBusqueda }}'))\n        ELSE '{{ $json.terminoBusqueda }}'\n      END\n    ) AS term_base,\n    LOWER('{{ $json.mensajeCliente || \"\" }}') AS full_message\n), productos_match AS (\n  SELECT\n    p.id, p.name, p.price, p.main_image_url, p.description, p.category_name, p.stock, p.supabase_id,\n    CASE\n      WHEN s.term_original = 'hass' AND LOWER(p.name) LIKE '%hass%' THEN 0\n      WHEN s.term_original LIKE '%aguacate%' AND LOWER(p.name) LIKE '%aguacate%' AND LOWER(p.name) LIKE 'caja %' THEN 0\n      WHEN s.full_message LIKE '%24%' AND LOWER(p.name) LIKE '%24%' THEN 0\n      WHEN s.full_message LIKE '%caja%' AND LOWER(p.name) LIKE 'caja %' THEN 1\n      WHEN regexp_replace(LOWER(p.name), '[^a-z0-9\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1\u00fc ]', '', 'g') ~ ('(^| )' || regexp_replace(s.term_original, '[^a-z0-9\u00e1\u00e9\u00ed\u00f3\u00fa\u00f1\u00fc ]', '', 'g') || '( |$)') THEN 2\n      WHEN LOWER(p.name) LIKE '%' || s.term_original || '%' THEN 3\n      WHEN LOWER(COALESCE(p.category_name, '')) LIKE '%' || s.term_original || '%' THEN 4\n      ELSE 99\n    END AS match_priority\n  FROM public.productos_tienda p, search_terms s\n  WHERE p.is_active = true\n    AND (\n      LOWER(p.name) LIKE '%' || s.term_original || '%'\n      OR LOWER(COALESCE(p.category_name, '')) LIKE '%' || s.term_original || '%'\n      OR LOWER(p.description) LIKE '%' || s.term_original || '%'\n    )\n)\nSELECT pm.id, pm.name, pm.price, pm.main_image_url, pm.description, pm.category_name, pm.stock, pm.supabase_id, pm.match_priority,\n  (SELECT COALESCE(json_agg(json_build_object('variante_id', v.id,'tipo', v.variant_name,'presentacion', v.variant_value,'precio', v.price,'stock', v.stock_quantity) ORDER BY v.price), '[]'::json)\n   FROM variantes_productos v WHERE v.product_supabase_id = pm.supabase_id AND v.is_active = true) AS variantes\nFROM productos_match pm\nGROUP BY pm.id, pm.name, pm.price, pm.main_image_url, pm.description, pm.category_name, pm.stock, pm.supabase_id, pm.match_priority\nORDER BY pm.match_priority ASC, CASE WHEN LOWER(pm.name) LIKE 'caja %' THEN 0 ELSE 1 END, pm.name\nLIMIT 3;",
        "options": {}
      },
      "id": "2a74cbde-7789-4f86-a771-9eed4733e6b1",
      "name": "3. B\u00fasqueda Autom\u00e1tica Productos",
      "type": "n8n-nodes-base.postgres",
      "position": [
        1296,
        752
      ],
      "typeVersion": 2.6,
      "alwaysOutputData": true,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Combinar datos del cliente y resultados de b\u00fasqueda\n// v5: Manejo robusto de clientes y pedidos web\n\nconst preproceso = $('1. Pre-procesamiento YCloud').first().json;\n\n// Obtener cliente con manejo de errores\nlet cliente = {};\ntry {\n  cliente = $('2. Obtener Cliente').first().json || {};\n} catch (e) {\n  // Si no existe el cliente, crear uno vac\u00edo\n  cliente = { id: null, telefono: preproceso.from, nombre: preproceso.customerName || 'Cliente' };\n}\n\n// Obtener productos de b\u00fasqueda\nlet productosEncontrados = [];\ntry {\n  const busquedaItems = $('3. B\u00fasqueda Autom\u00e1tica Productos').all();\n  productosEncontrados = busquedaItems\n    .map(item => item.json)\n    .filter(p => p && p.id && p.name && typeof p.price !== 'undefined');\n} catch (e) {\n  productosEncontrados = [];\n}\n\n// Formatear productos de forma SIMPLE\nlet productosTexto = '';\nif (productosEncontrados.length > 0) {\n  const lineas = [];\n  productosEncontrados.forEach(p => {\n    if (p.variantes && Array.isArray(p.variantes) && p.variantes.length > 0) {\n      p.variantes.forEach(v => {\n        lineas.push(`\u2022 ${p.name} - ${v.presentacion} $${Number(v.precio).toLocaleString('es-CO')} [ID:${p.id}]`);\n      });\n    } else {\n      lineas.push(`\u2022 ${p.name} - $${Number(p.price).toLocaleString('es-CO')} [ID:${p.id}]`);\n    }\n  });\n  productosTexto = [...new Set(lineas)].join('\\n');\n} else if (preproceso.debeHacerBusqueda && preproceso.terminoBusqueda) {\n  productosTexto = `\u274c No se encontraron productos para: \"${preproceso.terminoBusqueda}\"`;\n} else {\n  productosTexto = '(No se realiz\u00f3 b\u00fasqueda)';\n}\n\n// Contexto de tiempo\nconst now = new Date();\nconst dayOfWeek = now.getDay();\nconst hour = now.getHours();\nconst pasoCutoff = hour >= 10;\n\nlet diasHastaEntrega, mensajeEntrega;\nswitch(dayOfWeek) {\n  case 0: diasHastaEntrega = 2; mensajeEntrega = \"Pr\u00f3xima entrega: Martes\"; break;\n  case 1: diasHastaEntrega = 1; mensajeEntrega = \"Pr\u00f3xima entrega: Martes\"; break;\n  case 2: \n    if (pasoCutoff) { diasHastaEntrega = 3; mensajeEntrega = \"Pr\u00f3xima entrega: Viernes\"; }\n    else { diasHastaEntrega = 0; mensajeEntrega = \"\u00a1Entrega HOY antes 10AM!\"; }\n    break;\n  case 3: diasHastaEntrega = 2; mensajeEntrega = \"Pr\u00f3xima entrega: Viernes\"; break;\n  case 4: diasHastaEntrega = 1; mensajeEntrega = \"Pr\u00f3xima entrega: Viernes\"; break;\n  case 5:\n    if (pasoCutoff) { diasHastaEntrega = 4; mensajeEntrega = \"Pr\u00f3xima entrega: Martes\"; }\n    else { diasHastaEntrega = 0; mensajeEntrega = \"\u00a1Entrega HOY antes 10AM!\"; }\n    break;\n  case 6: diasHastaEntrega = 3; mensajeEntrega = \"Pr\u00f3xima entrega: Martes\"; break;\n  default: diasHastaEntrega = 1; mensajeEntrega = \"Pr\u00f3xima entrega: Martes o Viernes\";\n}\n\nconst fechaEntrega = new Date(now);\nfechaEntrega.setDate(now.getDate() + diasHastaEntrega);\nconst proximaEntrega = fechaEntrega.toLocaleDateString('es-CO', { weekday: 'long', day: 'numeric', month: 'long' });\nconst fechaActual = now.toLocaleDateString('es-CO', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });\nconst horaActual = now.toLocaleTimeString('es-CO', { hour: '2-digit', minute: '2-digit', hour12: true });\n\nreturn {\n  json: {\n    clienteId: cliente.id || null,\n    clienteTelefono: cliente.telefono || preproceso.from,\n    clienteNombre: cliente.nombre || preproceso.customerName || 'Cliente',\n    clienteEstado: cliente.estado_conversacion || 'NUEVO',\n    clienteDireccion: cliente.direccion || '',\n    clienteCarrito: cliente.pre_pedido || [],\n    clienteTotalPedidos: cliente.total_pedidos || 0,\n    \n    mensajeCliente: preproceso.mensajeCliente || preproceso.messageText,\n    terminoBusqueda: preproceso.terminoBusqueda || '',\n    esIntentoBusqueda: preproceso.esIntentoBusqueda || false,\n    esPedidoPlataforma: preproceso.esPedidoPlataforma || false,\n    infoPedidoPlataforma: preproceso.infoPedidoPlataforma || null,\n    \n    productosEncontrados,\n    productosTexto,\n    \n    from: preproceso.from,\n    to: preproceso.to,\n    saludo: preproceso.saludo,\n    \n    fechaActual, horaActual, horaNumero: hour, diaNumero: dayOfWeek,\n    pasoCutoff, proximaEntrega, mensajeEntrega, diasHastaEntrega,\n    retry_count: 0\n  }\n};\n"
      },
      "id": "4313e79a-64c2-46a3-8e81-e3e4efa7236d",
      "name": "4. Merge Datos + Productos",
      "type": "n8n-nodes-base.code",
      "position": [
        1584,
        592
      ],
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "USA OBLIGATORIAMENTE cuando el cliente diga \"quiero\", \"dame\", \"agr\u00e9game\" o similares. NO digas que ya lo hiciste si no has ejecutado esta herramienta. Requiere: producto_id, producto_nombre, precio, cantidad.",
        "operation": "executeQuery",
        "query": "UPDATE clientes \nSET pre_pedido = COALESCE(pre_pedido, '[]'::jsonb) || \n  jsonb_build_object(\n    'producto_id', $1::int,\n    'producto_nombre', $2,\n    'precio', $3::numeric,\n    'cantidad', COALESCE($4::int, 1)\n  ),\n  estado_conversacion = 'EN_PEDIDO'\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING pre_pedido;",
        "options": {
          "queryReplacement": "={{ $fromAI('producto_id','ID del producto a agregar','number',0) }}\n{{ $fromAI('producto_nombre','Nombre del producto','string','Producto') }}\n{{ $fromAI('precio','Precio del producto','number',0) }}\n{{ $fromAI('cantidad','Cantidad a agregar','number',1) }}"
        }
      },
      "id": "211df281-543d-497a-b643-25dca132695c",
      "name": "TOOL_AnadirAlCarrito",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        1584,
        1552
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "USA cuando el cliente diga \"eso es todo\", \"cu\u00e1nto es\", \"calcular total\". Devuelve el total del carrito.",
        "operation": "executeQuery",
        "query": "SELECT \n  COALESCE(\n    (SELECT SUM((item->>'precio')::numeric * COALESCE((item->>'cantidad')::int, 1))\n     FROM clientes, jsonb_array_elements(pre_pedido) AS item\n     WHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\n    ), 0\n  ) as total,\n  pre_pedido as items\nFROM clientes\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').item.json.from }}';",
        "options": {}
      },
      "id": "68d9c2da-d254-46d7-be1e-e702329223e9",
      "name": "TOOL_CalcularTotalPrePedido",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2512,
        1680
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Guardar el nombre del cliente cuando lo proporcione.",
        "operation": "executeQuery",
        "query": "UPDATE clientes \nSET \n  nombre = $1, \n  estado_conversacion = 'ATENCION_LUZ' \nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING nombre, estado_conversacion;",
        "options": {
          "queryReplacement": "={{ $fromAI('nombre_cliente','Nombre del cliente','string','Cliente') }}"
        }
      },
      "id": "6a73ac10-62d0-490d-a7a8-59007eab3613",
      "name": "TOOL_GuardarNombreCliente",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        1872,
        1824
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "USA SIEMPRE despu\u00e9s de cada acci\u00f3n importante. Estados: EN_PEDIDO (agreg\u00f3 productos), PEDIDO_CONFIRMADO (confirm\u00f3), PEDIDO_ONLINE (de tienda), ESCALADO (queja).",
        "operation": "executeQuery",
        "query": "UPDATE clientes \nSET estado_conversacion = $1\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING estado_conversacion;",
        "options": {
          "queryReplacement": "={{ $fromAI('nuevo_estado','Estado v\u00e1lido','string','ATENCION_LUZ') }}"
        }
      },
      "id": "84728a22-7913-434d-91b4-15690b9a14b2",
      "name": "TOOL_CambiarEstadoCliente",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2224,
        1744
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const contexto = $('4. Merge Datos + Productos').first().json;\nconst respuestaIA = $input.first().json.text || $input.first().json.output || '';\nlet mensaje = String(respuestaIA || '').replace(/\\*/g, '').trim();\nconst nombreCompleto = String(contexto.clienteNombre || '').trim();\nconst tieneNombreCompleto = nombreCompleto.split(/\\s+/).filter(Boolean).length >= 2;\nconst tieneDireccion = String(contexto.clienteDireccion || '').trim().length > 5;\nconst carrito = Array.isArray(contexto.clienteCarrito) ? contexto.clienteCarrito : [];\nconst pareceResumen = /subtotal|total:|resumen/i.test(mensaje);\nif (pareceResumen && carrito.length > 0 && (!tieneNombreCompleto || !tieneDireccion)) {\n  mensaje = '\u00a1Casi listo! Para coordinar el env\u00edo, por favor conf\u00edrmame tu nombre completo y la direcci\u00f3n de entrega.';\n}\nif (!mensaje) return [];\nreturn [{ json: { from: contexto.to, to: contexto.from, type: 'text', text: { body: mensaje, preview_url: false } } }];"
      },
      "id": "0262ccbe-0660-46a8-a027-0e674dd48319",
      "name": "\ud83d\udce4 Preparar Respuesta",
      "type": "n8n-nodes-base.code",
      "position": [
        2864,
        720
      ],
      "typeVersion": 2
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://httpbin.org/post",
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "lab_intercept",
              "value": "={{ $json.toJsonString() }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3424,
        592
      ],
      "id": "4503a0ed-b573-452a-9788-4c5e27b600a5",
      "name": "\ud83d\udcf1 Enviar WhatsApp YCloud"
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Herramienta de FALLBACK para buscar productos. \u00dasala SOLO cuando los productos encontrados autom\u00e1ticamente NO coinciden con lo que pidi\u00f3 el cliente. Input: t\u00e9rmino de b\u00fasqueda del producto.",
        "operation": "executeQuery",
        "query": "SELECT \n    id,\n    name,\n    COALESCE(discount_price, price) as precio,\n    description,\n    category_name\nFROM productos_tienda p\nWHERE \n    is_active = true\n    AND p.name NOT ILIKE '%Fresa Premium 250g%' AND p.price > 0 AND p.stock > 0 AND (\n        name ILIKE '%' || $1 || '%'\n        OR description ILIKE '%' || $1 || '%'\n        OR category_name ILIKE '%' || $1 || '%'\n    )\nORDER BY \n    CASE \n        WHEN is_featured = true THEN 0 \n        ELSE 1 \n    END,\n    name\nLIMIT 5;\n",
        "options": {
          "queryReplacement": "={{ $fromAI('termino_busqueda','Producto a buscar','string','aguacate') }}"
        }
      },
      "id": "37802349-d077-4ffe-a17d-acbf9652535d",
      "name": "TOOL_BuscarProductos",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2640,
        1568
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "ESTA HERRAMIENTA ES OBLIGATORIA PARA ESCALAR. \u00dasala para transferir la sesi\u00f3n a un humano ante cualquier queja, molestia o pedido de soporte humano. NO PUEDES CONECTAR CON UN HUMANO SOLO CON TEXTO, DEBES ACTIVAR ESTA HERRAMIENTA.",
        "operation": "executeQuery",
        "query": "UPDATE clientes \nSET \n    estado_conversacion = 'ESCALADO'\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING \n    nombre,\n    telefono,\n    estado_conversacion,\n    'Conversaci\u00f3n escalada. Motivo: ' || $1 as mensaje_confirmacion;\n",
        "options": {
          "queryReplacement": "={{ $fromAI('motivo_escalado','Motivo del escalado','string','Cliente solicita hablar con humano') }}"
        }
      },
      "id": "19f90844-7253-4e05-ab20-53780c3e8338",
      "name": "TOOL_EscalarServicioCliente",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        1744,
        1664
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Consultar el estado del cliente y su \u00faltimo pedido. Usarla cuando el cliente pregunte: \u00bfc\u00f3mo va mi pedido? \u00bftengo pedidos?",
        "operation": "executeQuery",
        "query": "SELECT \n  c.nombre,\n  c.telefono,\n  c.estado_conversacion as estado,\n  c.direccion,\n  c.pre_pedido as carrito,\n  c.updated_at as ultima_actualizacion,\n  CASE \n    WHEN c.estado_conversacion = 'PEDIDO_CONFIRMADO' THEN '\u2705 Tu pedido fue confirmado. Te contactaremos para coordinar la entrega.'\n    WHEN c.estado_conversacion = 'PEDIDO_ONLINE' THEN '\ud83d\uded2 Tu pedido de la tienda online est\u00e1 siendo procesado.'\n    WHEN c.estado_conversacion = 'EN_PEDIDO' THEN '\ud83d\udce6 Tienes un pedido en proceso. \u00bfDeseas confirmarlo?'\n    WHEN c.estado_conversacion = 'ESCALADO' THEN '\ud83d\udc64 Un asesor te contactar\u00e1 pronto.'\n    ELSE '\ud83d\udcac No encontr\u00e9 pedidos recientes. \u00bfQuieres hacer uno nuevo?'\n  END as mensaje_estado\nFROM clientes c\nWHERE c.telefono LIKE '%' || RIGHT('{{ $('1. Pre-procesamiento YCloud').first().json.from }}', 10) || '%'\nLIMIT 1;",
        "options": {}
      },
      "id": "65cf129f-27f4-4962-afb3-139f78304515",
      "name": "TOOL_ConsultarEstadoPedido",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2816,
        1488
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Guarda la direcci\u00f3n de entrega del cliente. Usar cuando el cliente proporciona su direcci\u00f3n completa. Input: direccion (la direcci\u00f3n del cliente).",
        "operation": "executeQuery",
        "query": "UPDATE clientes \nSET direccion = $1\nWHERE telefono = '{{ $('1. Pre-procesamiento YCloud').first().json.from }}'\nRETURNING telefono, nombre, direccion, 'Direcci\u00f3n guardada correctamente' as status;",
        "options": {
          "queryReplacement": "=={{ [$fromAI('direccion','Direcci\u00f3n completa del cliente incluyendo calle, n\u00famero, barrio y ciudad','string','')] }}"
        }
      },
      "id": "7e6be0a3-dadf-4979-950a-e18cf3d5e7b0",
      "name": "TOOL_GuardarDireccionCliente",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2976,
        1408
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {},
      "id": "d6bc2944-ed4b-47f0-bf9d-9e1f2c5e40d3",
      "name": "Calculator",
      "type": "@n8n/n8n-nodes-langchain.toolCalculator",
      "position": [
        2336,
        1760
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "USA SIEMPRE cuando el cliente pregunte por un producto. Obtiene tama\u00f1os, pesos y precios. Input: supabase_id del producto (b\u00fascalo en productosEncontrados).",
        "operation": "executeQuery",
        "query": "SELECT \n  v.variant_name as tipo,\n  v.variant_value as presentacion,\n  v.price as precio,\n  v.stock_quantity as stock,\n  p.name as producto\nFROM variantes_productos v\nJOIN productos_tienda p ON p.supabase_id = v.product_supabase_id\nWHERE p.id = $1\n  AND v.is_active = true\nORDER BY v.variant_name, v.price;",
        "options": {
          "queryReplacement": "={{ $fromAI('producto_id','ID num\u00e9rico del producto','number',0) }}"
        }
      },
      "id": "fb1e25a8-8f9b-417a-924f-5e713233d2f9",
      "name": "TOOL_ObtenerVariantes",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        2048,
        1744
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Obtener el producto de la consulta\nconst producto = $input.first().json;\n// Obtener el contexto del preprocesamiento\nconst contexto = $('1. Pre-procesamiento YCloud').first().json;\n// Formatear producto al formato esperado\nconst productoFormateado = {\n  producto_id: producto.id,\n  producto_nombre: producto.name,\n  name: producto.name,\n  id: producto.id,\n  precio: producto.price,\n  price: producto.price,\n  main_image_url: producto.main_image_url,\n  description: producto.description,\n  category: producto.category,\n  stock: producto.stock\n};\n// Retornar contexto con el producto encontrado\nreturn {\n  json: {\n    ...contexto,\n    productosEncontrados: [productoFormateado],\n    productosTexto: `Producto del bot\u00f3n: ${producto.name} - $${Number(producto.price).toLocaleString('es-CO')}`\n  }\n};"
      },
      "id": "89f8b753-f1c0-4d14-9c34-ad9238beb3c5",
      "name": "Code - Formatear Producto",
      "type": "n8n-nodes-base.code",
      "position": [
        1040,
        960
      ],
      "typeVersion": 2
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT \n  p.id,\n  p.name,\n  p.price,\n  p.main_image_url,\n  p.description,\n  p.category_id,\n  p.stock\nFROM public.productos_tienda p\nWHERE p.id = {{ $json.productoIdDelBoton }}\nLIMIT 1;",
        "options": {}
      },
      "id": "23d2a521-41a3-482b-b38f-de96a5d7317b",
      "name": "Buscar Producto del Bot\u00f3n",
      "type": "n8n-nodes-base.postgres",
      "position": [
        768,
        912
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "31594272-3c47-4074-9324-e697e674cd9b",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.esRespuestaBoton }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "Buscar Producto"
            },
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "c5c155de-73a0-4e73-8ec0-0c21914d6b18",
                    "operator": {
                      "type": "string",
                      "operation": "notEmpty",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.esRespuestaBoton }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "Sin Producto"
            }
          ]
        },
        "looseTypeValidation": true,
        "options": {
          "fallbackOutput": "none"
        }
      },
      "id": "2ac9fb8b-0172-48ac-9ab2-cb27f0ec396a",
      "name": "Detectar Click de Bot\u00f3n",
      "type": "n8n-nodes-base.switch",
      "position": [
        624,
        1232
      ],
      "typeVersion": 3.3
    },
    {
      "parameters": {},
      "id": "77068eff-d068-48d8-9f7b-f309db2c675d",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        976,
        1488
      ],
      "typeVersion": 3.2
    },
    {
      "parameters": {},
      "id": "eaf862ec-e845-4496-a1b4-fc4ccbca2e93",
      "name": "Merge1",
      "type": "n8n-nodes-base.merge",
      "position": [
        416,
        1600
      ],
      "typeVersion": 3.2
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "45a7a667-e7c8-41ab-823c-ea0048f4ca53",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.esComandoCopiloto }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "Copiloto"
            },
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "6e38428c-70d8-45a2-a0f1-a98aea4009f7",
                    "operator": {
                      "type": "boolean",
                      "operation": "false",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.esComandoCopiloto }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true,
              "outputKey": "Cliente"
            }
          ]
        },
        "looseTypeValidation": true,
        "options": {}
      },
      "id": "757b19ad-3ad2-47e9-bf59-a9b94b898bc3",
      "name": "\ud83d\udd00 \u00bfEs Copiloto?",
      "type": "n8n-nodes-base.switch",
      "position": [
        144,
        1312
      ],
      "typeVersion": 3.3
    },
    {
      "parameters": {
        "sessionIdType": "customKey",
        "sessionKey": "copiloto_operaciones",
        "contextWindowLength": 2
      },
      "id": "90dfdd22-402b-41b4-b9b3-ae50eb6513f3",
      "name": "\ud83e\udde0 Memoria Copiloto",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        3296,
        1696
      ],
      "typeVersion": 1.3
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Listar clientes SIN NOMBRE. Usar cuando pidan 'clientes sin nombre', 'an\u00f3nimos'.",
        "operation": "executeQuery",
        "query": "SELECT telefono, estado_conversacion, total_pedidos FROM clientes WHERE nombre IS NULL OR TRIM(nombre) = '' OR nombre ILIKE '%cliente%' ORDER BY total_pedidos DESC LIMIT 50;",
        "options": {}
      },
      "id": "ede8d0ad-2614-419a-97fc-189c92b73006",
      "name": "TOOL_ListarClientesSinNombre",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        4032,
        1648
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Actualizar el NOMBRE de un cliente. Requiere: telefono del cliente y nuevo nombre.",
        "operation": "executeQuery",
        "query": "UPDATE clientes SET nombre = $1 WHERE REPLACE(REPLACE(telefono, '+', ''), ' ', '') LIKE '%' || $2 || '%' RETURNING telefono, nombre, 'Actualizado' as status;",
        "options": {
          "queryReplacement": "={{ $fromAI('nombre','Nuevo nombre del cliente','string','') }}\n{{ $fromAI('telefono','Tel\u00e9fono del cliente (solo n\u00fameros)','string','') }}"
        }
      },
      "id": "73d7abf3-eb92-4fff-a438-843d591a5de0",
      "name": "TOOL_ADMIN_ActualizarNombre",
      "type": "n8n-nodes-base.postgresTool",
      "position": [
        3552,
        1680
      ],
      "typeVersion": 2.6,
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "descriptionType": "manual",
        "toolDescription": "Consultar datos completos de UN cliente por tel\u00e9fono. Devuelve nombre, direcci\u00f3n, carrito (pre_pedido), estado y total de pedidos.",
        "operation": "executeQuery",
        "query": "SELECT telefono, nombre, direccion, estado_conversacion, total_pedidos, jsonb_array_length(COALESCE(pre_p

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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

πŸ§ͺ LABR - nuevo asistente (REPARADO). Uses httpRequest, postgres, postgresTool, toolCalculator. Webhook trigger; 63 nodes.

Source: https://github.com/maurixio8/tus-aguacates/blob/7c1be578345ab34f11b4261c02d27be15fc178e8/n8n-workflows/labr-put-response-3.json β€” original creator credit. Request a take-down β†’

More AI & RAG workflows β†’ Β· Browse all categories β†’

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

CLINICAINTEGRAL_secretary. Uses postgres, mcpClientTool, googleDriveTool, toolWorkflow. Webhook trigger; 89 nodes.

Postgres, Mcp Client Tool, Google Drive Tool +14
AI & RAG

secretaria. Uses postgres, n8n-nodes-evolution-api, openAi, httpRequest. Webhook trigger; 71 nodes.

Postgres, N8N Nodes Evolution Api, OpenAI +12
AI & RAG

Brokeria-v20. Uses n8n-nodes-waha, httpRequest, redis, googleGemini. Webhook trigger; 56 nodes.

N8N Nodes Waha, HTTP Request, Redis +7
AI & RAG

Brokeria-v15. Uses n8n-nodes-waha, httpRequest, postgres, redis. Webhook trigger; 55 nodes.

N8N Nodes Waha, HTTP Request, Postgres +7
AI & RAG

Remi 1.1. Uses lmChatOpenAi, memoryPostgresChat, openAi, postgres. Webhook trigger; 89 nodes.

OpenAI Chat, Memory Postgres Chat, OpenAI +7