This workflow follows the Gmail → 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 →
{
"updatedAt": "2026-05-02T18:24:35.918Z",
"createdAt": "2026-05-02T02:09:28.457Z",
"id": "CjS3Gm0863S4j9Ij",
"name": "Trato Hecho - Chat Agent",
"description": null,
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "chat",
"responseMode": "responseNode",
"options": {
"allowedOrigins": "*"
}
},
"id": "95504852-22df-4116-8621-7d3828534d55",
"name": "Webhook Chat",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-1808,
448
]
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst body = raw.body || raw;\n\nconst uuid = String(body.sessionId || body.uuid || '').trim()\n || 'auto_' + Date.now() + '_' + Math.random().toString(36).substr(2,6);\n\nconst message = String(body.message || body.chatInput || '').trim();\n\nif (!message) throw new Error('message es requerido');\n\nlet bodyHistory = [];\ntry {\n if (Array.isArray(body.history)) bodyHistory = body.history;\n} catch(e) { bodyHistory = []; }\n\nreturn [{ json: { uuid, message, bodyHistory } }];"
},
"id": "2ff5e22d-da17-4d23-aa49-29b7af8c560a",
"name": "Extraer Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1600,
448
]
},
{
"parameters": {
"jsCode": "const extract = $('Extraer Input').first().json;\nconst { uuid, message, bodyHistory } = extract;\n\n// \u2500\u2500 1. HISTORIAL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet history = [];\ntry {\n const raw = $('GET Historial Supabase').first()?.json;\n if (Array.isArray(raw) && raw.length > 0 && Array.isArray(raw[0].messages)) {\n history = raw[0].messages;\n } else if (raw && Array.isArray(raw.messages)) {\n history = raw.messages;\n }\n} catch(e) {}\nif (!Array.isArray(history) || history.length === 0) {\n if (Array.isArray(bodyHistory) && bodyHistory.length > 0) history = bodyHistory;\n}\nhistory = history.filter(m => m && m.content);\n\n// \u2500\u2500 2. COTIZACI\u00d3N EXISTENTE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet quoteContext = '';\ntry {\n const raw = $('GET Cotizaci\u00f3n Supabase').first()?.json;\n const q = Array.isArray(raw) && raw.length > 0 ? raw[0]\n : (raw && raw.numero ? raw : null);\n if (q) {\n quoteContext = '\\n\\nCOTIZACI\u00d3N EXISTENTE #' + q.numero\n + ': ' + q.producto\n + ', ' + q.m2 + ' m\u00b2'\n + ', instalacion=' + (q.instalacion ? 'SI' : 'NO')\n + ', total=$' + Number(q.total).toLocaleString('es-CL')\n + '. Estado: ' + q.estado + '.';\n }\n} catch(e) {}\n\n// \u2500\u2500 3. AN\u00c1LISIS DEL HISTORIAL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// Estado seg\u00fan \u00faltimo mensaje del asistente\nconst lastAssistant = [...history].reverse().find(m => m.role === 'assistant');\nconst lastAsstMsg = lastAssistant ? lastAssistant.content.toLowerCase() : '';\n\nconst estadoPidiendoNombre = /nombre completo|tu nombre|c\u00f3mo te llamas|cu\u00e1l es tu nombre/.test(lastAsstMsg);\nconst estadoPidiendoRut = /\\brut\\b/.test(lastAsstMsg) && !/direcci/.test(lastAsstMsg);\nconst estadoPidiendoDireccion = /direcci/.test(lastAsstMsg);\nconst estadoEnDatos = estadoPidiendoNombre || estadoPidiendoRut || estadoPidiendoDireccion;\nconst estadoPidiendoInstalacion = /instalaci/i.test(lastAsstMsg) && /\\?\\s*$|si o no|responde si/i.test(lastAsstMsg);\n\n// Texto completo del historial + mensaje actual (para detecci\u00f3n de keywords)\nconst historialTexto = history.map(m => m.content).join(' ').toLowerCase() + ' ' + message.toLowerCase();\n\n// \u2500\u2500 Detecci\u00f3n de tipo de producto \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst PRODUCTOS = {\n 'Luxury Emerald (40mm)': { keywords: ['luxury', 'emerald', '40mm'], precio: 28500 },\n 'Soft Touch (30mm)': { keywords: ['soft touch', '30mm', 'suave', 'residencial'], precio: 22900 },\n 'Pet-Friendly Turf (35mm)': { keywords: ['pet', 'friendly', '35mm', 'mascota', 'perro', 'gato'], precio: 26000 },\n};\n\nlet productoNombre = null;\nlet precioPorM2 = 0;\nlet tipoCancha = '';\n\n// Detectar si es deportivo y qu\u00e9 deporte espec\u00edfico\nconst esDeportivo = /deportivo|f\u00fatbol|futbol|tenis|p\u00e1del|padel|cancha/.test(historialTexto);\nif (esDeportivo) {\n if (/f\u00fatbol 7|futbol 7/.test(historialTexto)) { tipoCancha = 'F\u00fatbol 7'; productoNombre = 'Pasto Deportivo F\u00fatbol 7'; }\n else if (/f\u00fatbol 11|futbol 11/.test(historialTexto)) { tipoCancha = 'F\u00fatbol 11'; productoNombre = 'Pasto Deportivo F\u00fatbol 11'; }\n else if (/f\u00fatbol|futbol/.test(historialTexto)) { tipoCancha = 'F\u00fatbol'; productoNombre = 'Pasto Deportivo F\u00fatbol'; }\n else if (/tenis/.test(historialTexto)) { tipoCancha = 'Tenis'; productoNombre = 'Pasto Tenis & P\u00e1del'; }\n else if (/p\u00e1del|padel/.test(historialTexto)) { tipoCancha = 'P\u00e1del'; productoNombre = 'Pasto Tenis & P\u00e1del'; }\n // precioPorM2 = 0 para deportivos (precio a cotizar)\n} else {\n for (const [nombre, info] of Object.entries(PRODUCTOS)) {\n if (info.keywords.some(k => historialTexto.includes(k))) {\n productoNombre = nombre;\n precioPorM2 = info.precio;\n break;\n }\n }\n}\n\n// \u2500\u2500 Detecci\u00f3n de m\u00b2 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Desactivada durante captura de nombre/RUT/direcci\u00f3n\nlet m2Final = 0;\nif (!estadoEnDatos) {\n // Formato \"5x8\", \"5 por 8\", \"5 * 8\"\n const regexDim = /(\\d+(?:[.,]\\d+)?)\\s*(?:x|\u00d7|por|\\*)\\s*(\\d+(?:[.,]\\d+)?)/i;\n const matchDim = message.match(regexDim);\n if (matchDim) {\n m2Final = parseFloat(matchDim[1].replace(',', '.')) * parseFloat(matchDim[2].replace(',', '.'));\n }\n // N\u00famero solo o con unidad\n if (!m2Final) {\n const matchM2 = message.trim().match(/^(\\d+(?:[.,]\\d+)?)\\s*(?:m2|m\u00b2|mt2|mt|mts|metros?)?\\s*$/i);\n if (matchM2) m2Final = parseFloat(matchM2[1].replace(',', '.'));\n }\n}\n// Buscar m\u00b2 en historial si no se encontr\u00f3 en el mensaje actual\nif (!m2Final) {\n for (const turn of [...history].reverse()) {\n if (turn.role !== 'user') continue;\n const dimM = turn.content.match(/(\\d+(?:[.,]\\d+)?)\\s*(?:x|\u00d7|por|\\*)\\s*(\\d+(?:[.,]\\d+)?)/i);\n if (dimM) { m2Final = parseFloat(dimM[1].replace(',', '.')) * parseFloat(dimM[2].replace(',', '.')); break; }\n const m2M = turn.content.trim().match(/^(\\d+(?:[.,]\\d+)?)\\s*(?:m2|m\u00b2|mt2|mt|mts|metros?)?\\s*$/i);\n if (m2M) { m2Final = parseFloat(m2M[1].replace(',', '.')); break; }\n }\n}\n\n// \u2500\u2500 Detecci\u00f3n de instalaci\u00f3n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet instalacionDefinida = null;\nfor (let i = history.length - 1; i >= 0; i--) {\n const m = history[i];\n if (m.role === 'assistant' && /instalaci/i.test(m.content) && /\\?\\s*$|si o no|responde si/i.test(m.content.toLowerCase())) {\n if (i + 1 < history.length && history[i + 1].role === 'user') {\n const resp = history[i + 1].content.toLowerCase().trim();\n if (/^s[i\u00ed]\\b|con instalaci|quiero instalaci|necesito instalaci/.test(resp)) instalacionDefinida = true;\n else if (/^no\\b|sin instalaci|no quiero|no necesito/.test(resp)) instalacionDefinida = false;\n }\n break;\n }\n}\n// Verificar mensaje actual como respuesta a pregunta de instalaci\u00f3n\nif (instalacionDefinida === null && estadoPidiendoInstalacion) {\n const resp = message.toLowerCase().trim();\n if (/^s[i\u00ed]\\b|con instalaci|quiero instalaci|necesito instalaci/.test(resp)) instalacionDefinida = true;\n else if (/^no\\b|sin instalaci|no quiero|no necesito/.test(resp)) instalacionDefinida = false;\n}\n\n// \u2500\u2500 Captura de datos del cliente desde historial \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet nombreCapturado = '';\nlet rutCapturado = '';\nlet direccionCapturada = '';\nfor (let i = 0; i < history.length - 1; i++) {\n const aMsg = history[i];\n const uMsg = history[i + 1];\n if (aMsg.role !== 'assistant' || uMsg.role !== 'user') continue;\n const aLow = aMsg.content.toLowerCase();\n if (!nombreCapturado && /nombre completo|tu nombre|c\u00f3mo te llamas|cu\u00e1l es tu nombre/.test(aLow)) nombreCapturado = uMsg.content.trim();\n if (!rutCapturado && /\\brut\\b/.test(aLow) && !/direcci/.test(aLow)) rutCapturado = uMsg.content.trim();\n if (!direccionCapturada && /direcci/.test(aLow)) direccionCapturada = uMsg.content.trim();\n}\n\n// \u2500\u2500 C\u00e1lculo determin\u00edstico del total (server-side) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet m2Margen = 0, subtotalPasto = 0, subtotalInst = 0, totalFinal = 0;\nif (m2Final > 0) {\n m2Margen = Math.ceil(m2Final * 1.10);\n subtotalPasto = m2Margen * precioPorM2; // 0 para deportivos\n subtotalInst = (instalacionDefinida === true && precioPorM2 > 0) ? m2Margen * 4500 : 0;\n totalFinal = subtotalPasto + subtotalInst;\n}\n\n// \u2500\u2500 4. INSTRUCCI\u00d3N CR\u00cdTICA \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet instruccionInmediata = '';\nconst msgLow = message.toLowerCase().trim();\n\n// Detectar si ya se inform\u00f3 el total o se pidi\u00f3 COTIZAR (para no repetir)\nconst yaSeInformoTotal = history.some(m =>\n m.role === 'assistant' && /escribe cotizar|escribe \"cotizar\"/i.test(m.content)\n);\n// Detectar si ya se pregunt\u00f3 el deporte espec\u00edfico\nconst yaSePreguntoDeporte = history.some(m =>\n m.role === 'assistant' && /f\u00fatbol 7|f\u00fatbol 11|tipo de cancha|qu\u00e9 tipo/i.test(m.content)\n);\n// Detectar si ya se preguntaron las dimensiones (deportivo)\nconst yaSePreguntoMedidas = history.some(m =>\n m.role === 'assistant' && /dimensiones|largo.*ancho|medidas.*cancha/i.test(m.content)\n);\n\nif (estadoPidiendoDireccion && !direccionCapturada && nombreCapturado && rutCapturado && message.trim().length >= 5) {\n // \u2192 Usuario dio la direcci\u00f3n: emitir [COTIZAR:]\n instruccionInmediata =\n `INSTRUCCI\u00d3N CR\u00cdTICA \u2014 M\u00c1XIMA PRIORIDAD. Emite \u00daNICAMENTE esta l\u00ednea, sin ning\u00fan texto antes ni despu\u00e9s:\\n` +\n `[COTIZAR: nombre=${nombreCapturado}, rut=${rutCapturado}, direccion=${message.trim()}, m2=${m2Final}, tipo=${productoNombre || 'No especificado'}, instalacion=${instalacionDefinida ? 'SI' : 'NO'}, total=${totalFinal}]`;\n\n} else if (estadoPidiendoRut && !rutCapturado) {\n // \u2192 Usuario dio el RUT: pedir direcci\u00f3n\n instruccionInmediata =\n `INSTRUCCI\u00d3N CR\u00cdTICA: El cliente acaba de entregar su RUT. Responde EXACTAMENTE:\\n` +\n `\"Gracias. \u00bfCu\u00e1l es la direcci\u00f3n de instalaci\u00f3n completa?\"`;\n\n} else if (estadoPidiendoNombre && !nombreCapturado) {\n // \u2192 Usuario dio el nombre: pedir RUT\n instruccionInmediata =\n `INSTRUCCI\u00d3N CR\u00cdTICA: El cliente acaba de dar su nombre. Responde EXACTAMENTE:\\n` +\n `\"Gracias. \u00bfCu\u00e1l es tu RUT? (formato: 12.345.678-9)\"`;\n\n} else if (/^cotizar$/.test(msgLow)) {\n // \u2192 Cliente escribe COTIZAR: iniciar captura de datos\n instruccionInmediata =\n `INSTRUCCI\u00d3N CR\u00cdTICA: El cliente quiere cotizar. Responde EXACTAMENTE:\\n` +\n `\"Perfecto. \u00bfCu\u00e1l es tu nombre completo?\"`;\n\n// \u2500\u2500 Flujo DEPORTIVO \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n} else if (esDeportivo && tipoCancha && m2Final > 0 && !yaSeInformoTotal) {\n // \u2192 Tiene deporte + m\u00b2: mostrar resumen y pedir COTIZAR\n instruccionInmediata =\n `INSTRUCCI\u00d3N CR\u00cdTICA: El cliente quiere ${tipoCancha}. \u00c1rea confirmada: ${m2Final} m\u00b2 (con margen t\u00e9cnico: ${m2Margen} m\u00b2).\\n` +\n `Responde con este resumen exacto:\\n` +\n `\"Producto: ${productoNombre}\\n` +\n `\u00c1rea: ${m2Final} m\u00b2 \u2192 con margen t\u00e9cnico: ${m2Margen} m\u00b2\\n` +\n `Precio: a cotizar seg\u00fan especificaciones t\u00e9cnicas de la cancha.\\n` +\n `Escribe COTIZAR para generar tu cotizaci\u00f3n formal.\"`;\n\n} else if (esDeportivo && tipoCancha && !m2Final && !yaSePreguntoMedidas) {\n // \u2192 Tiene deporte pero no m\u00b2: pedir dimensiones\n instruccionInmediata =\n `INSTRUCCI\u00d3N CR\u00cdTICA: El cliente quiere ${tipoCancha}. Responde EXACTAMENTE:\\n` +\n `\"\u00bfCu\u00e1les son las dimensiones de la cancha? (largo \u00d7 ancho en metros, ej: 50x40)\"`;\n\n} else if (esDeportivo && tipoCancha && !m2Final && yaSePreguntoMedidas) {\n // \u2192 Ya se pregunt\u00f3 medidas, cliente est\u00e1 respondiendo de forma no num\u00e9rica\n instruccionInmediata =\n `INSTRUCCI\u00d3N CR\u00cdTICA: Est\u00e1s esperando las dimensiones de la cancha de ${tipoCancha} en metros. ` +\n `Si el cliente no sabe, dile que puede estimar con largo \u00d7 ancho. No avances hasta tener las medidas.`;\n\n} else if (esDeportivo && !tipoCancha && !yaSePreguntoDeporte) {\n // \u2192 Es deportivo pero no especific\u00f3 el deporte: preguntar\n instruccionInmediata =\n `INSTRUCCI\u00d3N CR\u00cdTICA: El cliente quiere cancha deportiva. Responde EXACTAMENTE:\\n` +\n `\"\u00bfQu\u00e9 tipo de cancha necesitas? F\u00fatbol 7, F\u00fatbol 11, Tenis o P\u00e1del\"`;\n\n// \u2500\u2500 Flujo RESIDENCIAL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n} else if (productoNombre && m2Final > 0 && instalacionDefinida !== null && !yaSeInformoTotal) {\n // \u2192 Tiene producto + m\u00b2 + instalaci\u00f3n: mostrar total y pedir COTIZAR\n instruccionInmediata =\n `INSTRUCCI\u00d3N CR\u00cdTICA: Muestra este resumen exacto y luego di \"Escribe COTIZAR para tu cotizaci\u00f3n formal.\":\\n` +\n `Producto: ${productoNombre} \u2014 $${precioPorM2.toLocaleString('es-CL')}/m\u00b2\\n` +\n `Superficie: ${m2Final} m\u00b2 \u2192 con margen t\u00e9cnico: ${m2Margen} m\u00b2\\n` +\n `Subtotal pasto: ${m2Margen} \u00d7 $${precioPorM2.toLocaleString('es-CL')} = $${subtotalPasto.toLocaleString('es-CL')}\\n` +\n (instalacionDefinida ? `Instalaci\u00f3n: ${m2Margen} \u00d7 $4.500 = $${subtotalInst.toLocaleString('es-CL')}\\n` : `Instalaci\u00f3n: no incluida\\n`) +\n `TOTAL: $${totalFinal.toLocaleString('es-CL')}`;\n\n} else if (productoNombre && m2Final > 0 && instalacionDefinida === null && !estadoPidiendoInstalacion) {\n // \u2192 Tiene producto + m\u00b2 pero no pregunt\u00f3 instalaci\u00f3n: mostrar subtotal y preguntar\n const m2M = Math.ceil(m2Final * 1.10);\n const subP = m2M * precioPorM2;\n instruccionInmediata =\n `INSTRUCCI\u00d3N CR\u00cdTICA: Muestra el desglose y pregunta por instalaci\u00f3n:\\n` +\n `Producto: ${productoNombre} \u2014 $${precioPorM2.toLocaleString('es-CL')}/m\u00b2\\n` +\n `Superficie: ${m2Final} m\u00b2 \u2192 con margen t\u00e9cnico: ${m2M} m\u00b2\\n` +\n `Subtotal pasto: ${m2M} \u00d7 $${precioPorM2.toLocaleString('es-CL')} = $${subP.toLocaleString('es-CL')}\\n` +\n `Luego pregunta EXACTAMENTE: \"\u00bfNecesita instalaci\u00f3n? (+$4.500/m\u00b2) \u2014 responde SI o NO\"`;\n}\n\n// \u2500\u2500 5. SYSTEM PROMPT \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst systemPrompt = `Eres Queno, vendedor virtual de C\u00e9sped Sint\u00e9tico ARM en Melipilla, Chile.\nTu objetivo es vender c\u00e9sped sint\u00e9tico, tomar medidas, calcular precios y generar cotizaciones formales.\n${instruccionInmediata ? '\\n' + instruccionInmediata + '\\n\\nSI HAY UNA INSTRUCCI\u00d3N CR\u00cdTICA ARRIBA, S\u00cdGUELA AL PIE DE LA LETRA. No hagas nada m\u00e1s que lo que indica.\\n' : ''}${quoteContext}\n\nCAT\u00c1LOGO (precios CLP/m2, con IVA):\n- Luxury Emerald (40mm): $28.500/m2 \u2014 jard\u00edn o terraza premium\n- Soft Touch (30mm): $22.900/m2 \u2014 jard\u00edn residencial, suave al tacto\n- Pet-Friendly Turf (35mm): $26.000/m2 \u2014 jardines con mascotas, fibra antimicrobiana\n- Pasto Deportivo F\u00fatbol: precio a cotizar \u2014 canchas f\u00fatbol 11 y f\u00fatbol 7\n- Pasto Tenis & P\u00e1del: precio a cotizar \u2014 canchas tenis y p\u00e1del\n- Instalaci\u00f3n opcional: +$4.500/m2 (solo productos residenciales)\n\nFLUJO DE VENTA (solo si NO hay instrucci\u00f3n cr\u00edtica arriba):\n1. Sin producto \u2192 pregunta si es jard\u00edn/terraza o cancha deportiva\n2. Con producto residencial, sin m2 \u2192 pregunta cu\u00e1ntos m2 o dimensiones\n3. Con producto + m2 \u2192 muestra desglose y pregunta instalaci\u00f3n (SI o NO)\n4. Con instalaci\u00f3n definida \u2192 muestra total. Di: \"Escribe COTIZAR para cotizaci\u00f3n formal\"\n5. Producto deportivo sin deporte \u2192 pregunta: F\u00fatbol 7, F\u00fatbol 11, Tenis o P\u00e1del\n6. Deporte elegido, sin m2 \u2192 pregunta dimensiones (largo \u00d7 ancho en metros)\n7. Deporte + m2 \u2192 muestra resumen. Di: \"Escribe COTIZAR para cotizaci\u00f3n formal\"\n8. Cliente escribe COTIZAR \u2192 pide nombre completo\n9. Tras nombre \u2192 pide RUT (formato: 12.345.678-9)\n10. Tras RUT \u2192 pide direcci\u00f3n de instalaci\u00f3n completa\n11. Tras direcci\u00f3n \u2192 emite solo: [COTIZAR: nombre=X, rut=X, direccion=X, m2=X, tipo=X, instalacion=SI/NO, total=X]\n\nINTERPRETACI\u00d3N DE MEDIDAS:\n- N\u00famero solo (20) o con unidad (20m2) = m2 directamente\n- Dos n\u00fameros con x/por/* (50x40, 5 por 8) = largo \u00d7 ancho \u2192 calcula m2\n- EXCEPCI\u00d3N: en modo nombre/RUT/direcci\u00f3n, nunca interpretes texto como m2\n\nCUANDO EL CLIENTE NO SABE SUS MEDIDAS:\n- Explica brevemente c\u00f3mo medir y agrega al final: [CALCULAR]\n\nSI HAY COTIZACI\u00d3N EXISTENTE en el contexto:\n- Mu\u00e9strale los datos y ofrece proceder al pago: [PAGAR: numero=X, monto=X]\n\nREGLAS ESTRICTAS:\n- M\u00e1ximo 4 l\u00edneas por respuesta. Solo 1 pregunta por mensaje.\n- NUNCA saludar con \"Hola\" si ya hay historial.\n- NUNCA reiniciar el flujo ni volver a preguntar algo ya respondido.\n- NUNCA pedir datos ya entregados en el historial.\n- NUNCA calcular precios para productos deportivos (precio a cotizar).\n- NUNCA agregar texto antes o despu\u00e9s de [COTIZAR:] o [PAGAR:].\n- No interpretar RUT, nombre ni direcci\u00f3n como metros cuadrados.`;\n\n// \u2500\u2500 6. MENSAJES PARA CLAUDE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst messages = [\n ...history.map(m => ({ role: m.role === 'assistant' ? 'assistant' : 'user', content: m.content })),\n { role: 'user', content: message }\n];\n\n// \u2500\u2500 7. BODY PARA LA API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst claudeBody = JSON.stringify({\n model: 'claude-sonnet-4-5',\n max_tokens: 1024,\n system: systemPrompt,\n messages\n});\n\nreturn [{ json: { uuid, message, claudeBody, historyLength: history.length } }];\n"
},
"id": "742904d0-5ff8-436d-b70f-4a0ef3a56a1c",
"name": "Construir Prompt Claude",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1024,
448
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.anthropic.com/v1/messages",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "anthropic-version",
"value": "2023-06-01"
},
{
"name": "content-type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.claudeBody }}",
"options": {
"timeout": 30000
}
},
"id": "7b976c3e-b321-43a7-9958-a4f0fd000a6a",
"name": "Llamar Claude API",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
-816,
432
],
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
},
"httpHeaderAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const rawText = Array.isArray($json.content)\n ? $json.content\n .filter(b => b.type === 'text')\n .map(b => b.text || '')\n .join('\\n')\n : String($json.content || '');\n\nconst cotizarRegex = /\\[COTIZAR:\\s*([\\s\\S]*?)\\]/i;\nconst pagarRegex = /\\[PAGAR:\\s*([\\s\\S]*?)\\]/i;\n\nconst cotizarMatch = rawText.match(cotizarRegex);\nconst pagarMatch = rawText.match(pagarRegex);\nconsole.log(\"muestra valor:\", rawText.match(cotizarRegex));\n\n\nfunction parseKeyValueBlock(blockText) {\n const result = {};\n const parts = blockText.split(/,\\s*(?=[a-zA-Z_]+\\s*=)/);\n\n for (const part of parts) {\n const idx = part.indexOf('=');\n if (idx === -1) continue;\n const key = part.slice(0, idx).trim().toLowerCase();\n const value = part.slice(idx + 1).trim();\n result[key] = value;\n }\n\n return result;\n}\n\nfunction limpiarNumero(str) {\n return parseInt(String(str || '').replace(/[$.\\s]/g, '').replace(',', '.'), 10) || 0;\n}\n\nlet cleanText = rawText\n .replace(cotizarRegex, '')\n .replace(pagarRegex, '')\n .trim();\n\nlet action = 'CHAT';\nlet actionData = null;\n\nif (cotizarMatch) {\n const data = parseKeyValueBlock(cotizarMatch[1]);\n\n action = 'COTIZAR';\n actionData = {\n nombre: data.nombre || '',\n rut: data.rut || '',\n direccion: data.direccion || '',\n m2: parseFloat(data.m2 || '0') || 0,\n tipo: data.tipo || '',\n instalacion: String(data.instalacion || '').toUpperCase() === 'SI',\n total: limpiarNumero(data.total),\n };\n} else if (pagarMatch) {\n const data = parseKeyValueBlock(pagarMatch[1]);\n\n action = 'PAGAR';\n actionData = {\n numero: data.numero || '',\n monto: limpiarNumero(data.monto),\n };\n}\n\nreturn [\n {\n json: {\n rawText,\n cleanText,\n action,\n actionData,\n debug: {\n cotizarFound: !!cotizarMatch,\n pagarFound: !!pagarMatch,\n cotizarBlock: cotizarMatch ? cotizarMatch[1] : null,\n pagarBlock: pagarMatch ? pagarMatch[1] : null,\n }\n }\n }\n];"
},
"id": "e92d4f67-d377-4b34-899b-a0818086f295",
"name": "Parsear Respuesta Claude",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-624,
432
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ $('Parsear Respuesta Claude').first().json.action }}",
"rightValue": "COTIZAR",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "c91ac9c7-7fb7-4c7b-89cb-48cafb69ec75"
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ $('Parsear Respuesta Claude').first().json.action }}",
"rightValue": "PAGAR",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "db93e55e-8522-4dd5-a01d-b8661b0c6ce8"
}
],
"combinator": "and"
}
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"id": "5e4b8335-a4bb-4ad8-94ca-33b344f23156",
"name": "Switch Acci\u00f3n",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
-176,
144
]
},
{
"parameters": {
"jsCode": "const parseData = $('Parsear Respuesta Claude').first().json;\nconst actionData = parseData.actionData || {};\nconst uuid = parseData.uuid || $('Extraer Input').first().json.uuid || 'sin-uuid';\nconst cleanText = parseData.cleanText || '';\n\n// N\u00famero de cotizaci\u00f3n: COT-A\u00d1O-XXXXX (basado en timestamp)\nconst year = new Date().getFullYear();\nconst seq = String(Date.now()).slice(-5);\nconst numero = 'COT-' + year + '-' + seq;\n\nconst total = Number(actionData.total) || 0;\nconst m2 = Number(actionData.m2) || 0;\nconst instalacion = (String(actionData.instalacion || '')).toUpperCase() === 'SI';\n\nconst quote = {\n numero,\n session_id: uuid,\n nombre: actionData.nombre || '',\n rut: actionData.rut || '',\n direccion: actionData.direccion || '',\n producto: actionData.tipo || '',\n m2,\n instalacion,\n total,\n estado: 'pendiente'\n};\n\nconst totalStr = total > 0\n ? '$' + total.toLocaleString('es-CL')\n : 'a cotizar';\n\nconst message = '\u2705 Cotizaci\u00f3n generada!\\n'\n + '\ud83d\udccb N\u00b0 ' + numero + '\\n'\n + '\ud83d\udc64 ' + quote.nombre + ' \u2014 ' + quote.rut + '\\n'\n + '\ud83d\udccd ' + quote.direccion + '\\n'\n + '\ud83c\udf3f ' + quote.producto + ' \u2014 ' + m2 + ' m\u00b2\\n'\n + (instalacion ? '\ud83d\udd27 Con instalaci\u00f3n\\n' : '')\n + '\ud83d\udcb0 Total: ' + totalStr + '\\n\\n'\n + 'Escribe PAGAR para continuar con el pago.';\n\nreturn [{ json: { quote, message, uuid, cleanText } }];"
},
"id": "8ae42054-a7a7-4111-a2ce-bcd6aa9a2763",
"name": "Crear Cotizaci\u00f3n",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
256,
0
]
},
{
"parameters": {
"jsCode": "// \u2500\u2500 Respuesta al frontend con cotizaci\u00f3n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst quoteData = $('Crear Cotizaci\u00f3n').first().json;\n\nreturn [{\n json: {\n message: quoteData.message,\n quote: quoteData.quote,\n paymentLink: null\n }\n}];"
},
"id": "d31376b6-4f76-4751-9ee0-7894432c3f9c",
"name": "Respuesta Cotizaci\u00f3n",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
688,
0
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.mercadopago.com/checkout/preferences",
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\n \"items\": [\n {\n \"id\": \"123456\",\n \"title\": \"Producto de Prueba\",\n \"description\": \"Descripci\u00f3n del Producto\",\n \"category_id\": \"producto\",\n \"quantity\": 1,\n \"currency_id\": \"ARS\",\n \"unit_price\": 3500\n }\n ],\n \"payer\": {\n \"name\": \"Soledad\",\n \"surname\": \"Gomez\",\n \"email\": \"solgomez@gmail.com\",\n \"phone\": {\n \"area_code\": \"11\",\n \"number\": 116565984\n }\n },\n \"identification\": {\n \"type\": \"DNI\",\n \"number\": \"564654654\"\n },\n \"external_reference\": \"prueba_123\"\n}",
"options": {
"timeout": 15000
}
},
"id": "1801e4f5-7826-41bd-9571-208938a1830b",
"name": "Llamar MercadoPago",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
336,
416
],
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// --- Respuesta al frontend con link de pago ---------\nconst mpResp = $input.first().json;\nconst parseData = $('Parsear Respuesta Claude').first().json;\n\n// Buscamos el link, ya sea producci\u00f3n o sandbox\nconst paymentLink = mpResp.init_point || mpResp.sandbox_init_point || null;\n\n// Acceso seguro a los datos de la cotizaci\u00f3n (si no existen, ponemos vac\u00edo)\nconst numeroCotizacion = 1/**parseData.actionData?.numero*/ || \"N/A\";\nconst textoLimpio = parseData.cleanText || \"\";\n\nconst message = paymentLink \n ? `${textoLimpio}\\n\\n\ud83c\udfab **Orden #${numeroCotizacion}**\\n\u2705 \u00a1Listo! Aqu\u00ed est\u00e1 tu link para pagar de forma segura con Mercado Pago:\\n${paymentLink}`\n : `${textoLimpio}\\n\\n\u26a0\ufe0f Hubo un problema al generar el link de pago. Por favor, cont\u00e1ctanos directamente.`;\n\nreturn [{\n json: {\n message,\n quote: parseData.existingQuote || null,\n paymentLink\n }\n}];"
},
"id": "cc34fb13-2dfe-4d4d-b43d-f73d44266862",
"name": "Respuesta Pago",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
640,
416
]
},
{
"parameters": {
"jsCode": "const parseData = $('Parsear Respuesta Claude').first().json;\nreturn [{\n json: {\n message: parseData.cleanText || parseData.rawText || '',\n quote: parseData.existingQuote || null,\n paymentLink: null\n }\n}];"
},
"id": "d8191df4-5d15-4be3-9249-7f96bbb97fed",
"name": "Respuesta Simple",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
128,
192
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"id": "3799f2c6-4022-42ca-a01e-c0d1cacbaef4",
"name": "Responder Webhook (Cotizar)",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
1360,
0
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"id": "a757e730-1d85-4dd3-9d96-fa7cab12206b",
"name": "Responder Webhook (Pagar)",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
1392,
416
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"id": "c86ac9d9-6f40-4e3d-8594-6a3f7f205ded",
"name": "Responder Webhook (Simple)",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
416,
192
]
},
{
"parameters": {
"chatId": "8605918152",
"text": "=\ud83e\uddfe *Nueva Cotizaci\u00f3n Generada* \ud83d\udccb N\u00famero: `{{ $('Crear Cotizaci\u00f3n').first().json.quote.numero }}` \ud83c\udf3f Producto: {{ $('Crear Cotizaci\u00f3n').first().json.quote.tipo }} \ud83d\udcd0 M\u00b2: {{ $('Crear Cotizaci\u00f3n').first().json.quote.m2 }} m\u00b2 \ud83d\udd27 Instalaci\u00f3n: {{ $('Crear Cotizaci\u00f3n').first().json.quote.instalacion ? 'S\u00ed' : 'No' }} \ud83d\udcb0 Total: ${{ Number($('Crear Cotizaci\u00f3n').first().json.quote.total).toLocaleString('es-CL') }} CLP \ud83d\udcc5 Fecha: {{ $('Crear Cotizaci\u00f3n').first().json.quote.fecha }} \ud83c\udd94 UUID cliente: `{{ $('Crear Cotizaci\u00f3n').first().json.uuid }}`",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
928,
0
],
"id": "b6b932b5-e2e1-4e6d-afd6-ee24617a9069",
"name": "Telegram - MsgeCotiza",
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"sendTo": "aromero.madrid@gmail.com",
"subject": "Nueva Cotizacion de Queno",
"message": "<div style=\"font-family: Arial, sans-serif; line-height: 1.6; color: #333;\"> <h2 style=\"color: #2e7d32;\">Resumen de tu Cotizaci\u00f3n</h2> <p>Hola, adjuntamos los detalles de la cotizaci\u00f3n solicitada para tu proyecto:</p> <table style=\"width: 100%; border-collapse: collapse; margin-bottom: 20px;\"> <tr> <td style=\"padding: 8px; border: 1px solid #ddd; font-weight: bold; background-color: #f9f9f9;\">N\u00famero:</td> <td style=\"padding: 8px; border: 1px solid #ddd;\">{{ $json.quote.numero }}</td> </tr> <tr> <td style=\"padding: 8px; border: 1px solid #ddd; font-weight: bold; background-color: #f9f9f9;\">Fecha:</td> <td style=\"padding: 8px; border: 1px solid #ddd;\">{{ $json.quote.fecha }}</td> </tr> <tr> <td style=\"padding: 8px; border: 1px solid #ddd; font-weight: bold; background-color: #f9f9f9;\">Superficie:</td> <td style=\"padding: 8px; border: 1px solid #ddd;\">{{ $json.quote.m2 }} m\u00b2</td> </tr> <tr> <td style=\"padding: 8px; border: 1px solid #ddd; font-weight: bold; background-color: #f9f9f9;\">Tipo de Pasto:</td> <td style=\"padding: 8px; border: 1px solid #ddd;\">{{ $json.quote.tipo }}</td> </tr> <tr> <td style=\"padding: 8px; border: 1px solid #ddd; font-weight: bold; background-color: #f9f9f9;\">Instalaci\u00f3n:</td> <td style=\"padding: 8px; border: 1px solid #ddd;\">{{ $json.quote.instalacion ? 'Incluida' : 'No incluida' }}</td> </tr> <tr style=\"font-size: 1.2em; color: #2e7d32;\"> <td style=\"padding: 8px; border: 1px solid #ddd; font-weight: bold;\">TOTAL:</td> <td style=\"padding: 8px; border: 1px solid #ddd; font-weight: bold;\">${{ $json.quote.total.toLocaleString('es-CL') }} CLP</td> </tr> </table> <p>Si deseas proceder con la compra, puedes hacerlo directamente a trav\u00e9s del link enviado por chat.</p> <hr> <p style=\"font-size: 0.9em; color: #777;\">C\u00e9sped Sint\u00e9tico SpA - Melipilla, Chile.</p> </div>",
"options": {}
},
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.2,
"position": [
1136,
0
],
"id": "ad075c19-1962-487a-9e36-52fa6e233936",
"name": "MsjeCotiza",
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "8605918152",
"text": "=\ud83d\udcb3 *\u00a1Nueva Venta / Pago Iniciado!* \ud83d\udccb Cotizaci\u00f3n: `{{ $json.actionData.numero }}` \ud83d\udcb0 Monto: ${{ Number($json.actionData.monto).toLocaleString('es-CL') }} CLP \ud83d\udd17 Link de pago: {{ $json.paymentLink \n\n 'No disponible' }} \ud83d\udcc5 Fecha: {{ new Date().toLocaleDateString('es-CL', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) }}",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
880,
416
],
"id": "05e84850-fc6b-4b1b-903c-9b52548fe386",
"name": "Send a text message1",
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"sendTo": "aromero.madrid@gmail.com",
"subject": "Nueva Venta",
"message": "<div style=\"font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; border: 1px solid #eee; padding: 20px;\"> <div style=\"text-align: center; background-color: #2e7d32; padding: 10px;\"> <h1 style=\"color: white; margin: 0;\">\u00a1Gracias por tu compra!</h1> </div> <p style=\"margin-top: 20px;\">Hola,</p> <p>Hemos recibido correctamente el pago/confirmaci\u00f3n de tu pedido. A continuaci\u00f3n, te detallamos el resumen de tu compra para <strong>Melipilla</strong>:</p> <div style=\"background-color: #f4f4f4; padding: 15px; border-radius: 5px; margin: 20px 0;\"> <p style=\"margin: 5px 0;\"><strong>Orden:</strong> {{ $json.quote.numero }}</p> <p style=\"margin: 5px 0;\"><strong>Producto:</strong> {{ $json.quote.tipo }}</p> <p style=\"margin: 5px 0;\"><strong>Superficie:</strong> {{ $json.quote.m2 }} m\u00b2</p> <p style=\"margin: 5px 0;\"><strong>Instalaci\u00f3n:</strong> {{ $json.quote.instalacion ? '\u2705 Incluida' : '\u274c No incluida' }}</p> <p style=\"margin: 5px 0; font-size: 1.2em; color: #2e7d32;\"><strong>Total Pagado: ${{ $json.quote.total.toLocaleString('es-CL') }} CLP</strong></p> </div> <h3 style=\"color: #2e7d32;\">\u00bfQu\u00e9 sigue ahora?</h3> <ol> <li>Nuestro equipo t\u00e9cnico revisar\u00e1 los detalles de tu proyecto.</li> <li>Te contactaremos en las pr\u00f3ximas 24 horas h\u00e1biles para coordinar la entrega o la fecha de instalaci\u00f3n.</li> </ol> <p>Si tienes alguna duda, puedes responder a este correo o escribirnos directamente por WhatsApp.</p> <hr style=\"border: 0; border-top: 1px solid #eee; margin: 30px 0;\"> <p style=\"font-size: 0.8em; color: #999; text-align: center;\"> C\u00e9sped Sint\u00e9tico SpA - Especialistas en \u00e1reas verdes para Melipilla y alrededores. </p> </div>",
"options": {}
},
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.2,
"position": [
1120,
416
],
"id": "389a61ce-d59f-4e7e-8d1f-663e4e9b4d15",
"name": "MsjeVenta1",
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// \u2500\u2500 Respuesta cuando no hay cotizaci\u00f3n previa con monto \u2500\u2500\nconst data = $input.first().json;\n\nreturn [{\n json: {\n message: 'Revisar o generar cotizaci\u00f3n previa',\n quote: data.existingQuote || null,\n paymentLink: null\n }\n}];"
},
"id": "d0fac113-7c3e-4299-8b1b-bf92b3a4c63e",
"name": "Respuesta Sin Cotizaci\u00f3n1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
272,
736
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}",
"options": {
"responseHeaders": {
"entries": [
{
"name": "Access-Control-Allow-Origin",
"value": "*"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"id": "e80152a9-8670-4dbe-9572-7f214fdf5028",
"name": "Responder Webhook (Sin Cotizaci\u00f3n)1",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
576,
736
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "15d6dade-9d8e-4586-a118-3fa82746e800",
"leftValue": "={{ $json.actionData.monto }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
32,
432
],
"id": "fd4800ff-78c5-4b01-ab51-6c30834bc2a2",
"name": "If"
},
{
"parameters": {
"jsCode": "const extract = $('Extraer Input').first()?.json || {};\nconst parse = $('Parsear Respuesta Claude').first()?.json || {};\n\nconst uuid = extract.uuid || 'anonimo';\nconst userMsg = extract.message || '';\n// rawText mantiene etiquetas [COTIZAR:] para que el estado no se rompa\nconst assistantMsg = parse.rawText || parse.cleanText || parse.fullResponse || '';\n\nlet history = [];\ntry {\n const raw = $('GET Historial Supabase').first()?.json;\n if (Array.isArray(raw) && raw.length > 0 && Array.isArray(raw[0].messages)) {\n history = raw[0].messages;\n } else if (raw && Array.isArray(raw.messages)) {\n history = raw.messages;\n }\n} catch(e) {}\n\nif (!Array.isArray(history) || history.length === 0) {\n try {\n const bh = extract.bodyHistory;\n if (Array.isArray(bh)) history = bh;\n } catch(e) {}\n}\n\nif (!Array.isArray(history)) history = [];\nhistory = history.filter(m => m && m.content);\n\nhistory.push({ role: 'user', content: userMsg });\nhistory.push({ role: 'assistant', content: assistantMsg });\nhistory = history.slice(-40);\n\nreturn [{ json: { uuid, history, message: userMsg, assistantMsg } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-432,
432
],
"id": "741ea0da-7694-420e-afd6-255415c84fcb",
"name": "Preparar Historial"
},
{
"parameters": {
"url": "={{ 'https://enpzxntzphvezopbxbtx.supabase.co/rest/v1/historial?session_id=eq.' + $json.uuid + '&select=messages' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "<redacted-credential>"
},
{
"name": "Authorization",
"value": "<redacted-credential>"
}
]
},
"options": {}
},
"id": "89ef7229-9790-46a7-b398-ffe362246ae9",
"name": "GET Historial Supabase",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-1408,
448
],
"alwaysOutputData": true,
"onError": "continueRegularOutput"
},
{
"parameters": {
"url": "={{ 'https://enpzxntzphvezopbxbtx.supabase.co/rest/v1/cotizaciones?session_id=eq.' + $('Extraer Input').first().json.uuid + '&order=created_at.desc&limit=1&select=*' }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "<redacted-credential>"
},
{
"name": "Authorization",
"value": "<redacted-credential>"
}
]
},
"options": {}
},
"id": "97a5b356-2417-471c-836d-4ee2d1469e40",
"name": "GET Cotizaci\u00f3n Supabase",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-1232,
448
],
"alwaysOutputData": true,
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "POST",
"url": "https://enpzxntzphvezopbxbtx.supabase.co/rest/v1/historial",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "<redacted-credential>"
},
{
"name": "Authorization",
"value": "<redacted-credential>"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "resolution=merge-duplicates"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ session_id: $json.uuid, messages: $json.history, updated_at: new Date().toISOString() }) }}",
"options": {}
},
"id": "6eaa3201-8ab4-4cbb-b730-c344233e93bb",
"name": "Guardar Historial Supabase",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
-192,
400
],
"alwaysOutputData": true,
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "POST",
"url": "https://enpzxntzphvezopbxbtx.supabase.co/rest/v1/cotizaciones",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "<redacted-credential>"
},
{
"name": "Authorization",
"value": "<redacted-credential>"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=representation"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.quote) }}",
"options": {}
},
"id": "c3ed2543-9ddb-4afe-8caa-00fc43597399",
"name": "Guardar Cotizaci\u00f3n Supabase",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
480,
0
],
"alwaysOutputData": true,
"onError": "continueRegularOutput"
}
],
"connections": {
"Webhook Chat": {
"main": [
[
{
"node": "Extraer Input",
"type": "main",
"index": 0
}
]
]
},
"Extraer Input": {
"main": [
[
{
"node": "GET Historial Supabase",
"type": "main",
"index": 0
}
]
]
},
"GET Historial Supabase": {
"main": [
[
{
"node": "GET Cotizaci\u00f3n Supabase",
"type": "main",
"index": 0
}
]
]
},
"GET Cotizaci\u00f3n Supabase": {
"main": [
[
{
"node": "Construir Prompt Claude",
"type": "main",
"index": 0
}
]
]
},
"Construir Prompt Claude": {
"main": [
[
{
"node": "Llamar Claude API",
"type": "main",
"index": 0
}
]
]
},
"Llamar Claude API": {
"main": [
[
{
"node": "Parsear Respuesta Claude",
"type": "main",
"index": 0
}
]
]
},
"Parsear Respuesta Claude": {
"main": [
[
{
"node": "Preparar Historial",
"type": "main",
"index": 0
}
]
]
},
"Preparar Historial": {
"main": [
[
{
"node": "Guardar Historial Supabase",
"type": "main",
"index": 0
}
]
]
},
"Guardar Historial Supabase": {
"main": [
[
{
"node": "Switch Acci\u00f3n",
"type": "main",
"index": 0
}
]
]
},
"Switch Acci\u00f3n": {
"main": [
[
{
"node": "Crear Cotizaci\u00f3n",
"type": "main",
"index": 0
}
],
[
{
"node": "If",
"type": "main",
"index": 0
}
],
[
{
"node": "Respuesta Simple",
"type": "main",
"index": 0
}
]
]
},
"Crear Cotizaci\u00f3n": {
"main": [
[
{
"node": "Guardar Cotizaci\u00f3n Supabase",
"type": "main",
"index": 0
}
]
]
},
"Guardar Cotizaci\u00f3n Supabase": {
"main": [
[
{
"node": "Respuesta Cotizaci\u00f3n",
"type": "main",
"index": 0
}
]
]
},
"Respuesta Cotizaci\u00f3n": {
"main": [
[
{
"node": "Telegram - MsgeCotiza",
"type": "main",
"index": 0
}
]
]
},
"Telegram - MsgeCotiza": {
"main": [
[
{
"node": "MsjeCotiza",
"type": "main",
"index": 0
}
]
]
},
"MsjeCotiza": {
"main": [
[
{
"node": "Responder Webhook (Cotizar)",
"type": "main",
"index": 0
}
]
]
},
"If": {
"main": [
[
{
"node": "Llamar MercadoPago",
"type": "main",
"index": 0
}
],
[
{
"node": "Respuesta Sin Cotizaci\u00f3n1",
"type": "main",
"index": 0
}
]
]
},
"Llamar MercadoPago": {
"main": [
[
{
"node": "Respuesta Pago",
"type": "main",
"index": 0
}
]
]
},
"Respuesta Pago": {
"main": [
[
{
"node": "Send a text message1",
"type": "main",
"index": 0
}
]
]
},
"Send a text message1": {
"main": [
[
{
"node": "MsjeVenta1",
"type": "main",
"index": 0
}
]
]
},
"MsjeVenta1": {
"main": [
[
{
"node": "Responder Webhook (Pagar)",
"type": "main",
"index": 0
}
]
]
},
"Respuesta Sin Cotizaci\u00f3n1": {
"main": [
[
{
"node": "Responder Webhook (Sin Cotizaci\u00f3n)1",
"type": "main",
"index": 0
}
]
]
},
"Respuesta Simple": {
"main": [
[
{
"node": "Responder Webhook (Simple)",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"callerPolicy": "workflowsFromSameOwner",
"availableInMCP": false
},
"staticData": {},
"meta": null,
"versionId": "24bb9529-c7f0-4d8c-9918-2e52748b5b6a",
"activeVersionId": "24bb9529-c7f0-4d8c-9918-2e52748b5b6a",
"versionCounter": 176,
"triggerCount": 1,
"shared": [
{
"updatedAt": "2026-05-02T02:09:28.473Z",
"createdAt": "2026-05-02T02:09:28.473Z",
"role": "workflow:owner",
"workflowId": "CjS3Gm0863S4j9Ij",
"projectId": "WFAsewOsXFDwGJWF",
"project": {
"updatedAt": "2026-03-28T13:04:48.870Z",
"createdAt": "2026-03-28T13:01:08.818Z",
"id": "WFAsewOsXFDwGJWF",
"name": "Gonzalo ghon.cbr@gmail.com <ghon.cbr@gmail.com>",
"type": "personal",
"icon": null,
"description": null,
"creatorId": "b4851c5e-a89a-4984-af61-213ba884aaeb"
}
}
],
"tags": [
{
"updatedAt": "2026-03-28T13:23:08.567Z",
"createdAt": "2026-03-28T13:23:08.567Z",
"id": "JL63ldyzA1UQlelo",
"name": "trato-hecho"
},
{
"updatedAt": "2026-03-28T13:23:08.570Z",
"createdAt": "2026-03-28T13:23:08.570Z",
"id": "nnzNct4HPQf6Xwta",
"name": "chat-agent"
}
],
"activeVersion": {
"updatedAt": "2026-05-02T18:24:35.925Z",
"createdAt": "2026-05-02T18:24:35.925Z",
"versionId": "24bb9529-c7f0-4d8c-9918-2e52748b5b6a",
"workflowId": "CjS3Gm0863S4j9Ij",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "chat",
"responseMode": "responseNode",
"options": {
"allowedOrigins": "*"
}
},
"id": "95504852-22df-4116-8621-7d3828534d55",
"name": "Webhook Chat",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-1808,
448
],
"webhookId": "7932d703-57b1-48cd-a334-4f11edcc1725"
},
{
"parameters": {
"jsCode": "const raw = $input.first().json;\nconst body = raw.body || raw;\n\nconst uuid = String(body.sessionId || body.uuid || '').trim()\n || 'auto_' + Date.now() + '_' + Math.random().toString(36).substr(2,6);\n\nconst message = String(body.message || body.chatInput || '').trim();\n\nif (!message) throw new Error('message es requerido');\n\nlet bodyHistory = [];\ntry {\n if (Array.isArray(body.history)) bodyHistory = body.history;\n} catch(e) { bodyHistory = []; }\n\nreturn [{ json: { uuid, message, bodyHistory } }];"
},
"id": "2ff5e22d-da17-4d23-aa49-29b7af8c560a",
"name": "Extraer Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1600,
448
]
},
{
"parameters": {
"jsCode": "const extract = $('Extraer Input').first().json;\nconst { uuid, message, bodyHistory } = extract;\n\n// \u2500\u2500 1. HISTORIAL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet history = [];\ntry {\n const raw = $('GET Historial Supabase').first()?.json;\n if (Array.isArray(raw) && raw.length > 0 && Array.isArray(raw[0].messages)) {\n history = raw[0].messages;\n } else if (raw && Array.isArray(raw.messages)) {\n history = raw.messages;\n }\n} catch(e) {}\nif (!Array.isArray(history) || history.length === 0) {\n if (Array.isArray(bodyHistory) && bodyHistory.length > 0) history = bodyHistory;\n}\nhistory = history.filter(m => m && m.content);\n\n// \u2500\u2500 2. COTIZACI\u00d3N EXISTENTE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet quoteContext = '';\ntry {\n const raw = $('GET Cotizaci\u00f3n Supabase').first()?.json;\n const q = Array.isArray(raw) && raw.length > 0 ? raw[0]\n : (raw && raw.numero ? raw : null);\n if (q) {\n quoteContext = '\\n\\nCOTIZACI\u00d3N EXISTENTE #' + q.numero\n + ': ' + q.producto\n + ', ' + q.m2 + ' m\u00b2'\n + ', instalacion=' + (q.instalacion ? 'SI' : 'NO')\n + ', total=$' + Number(q.total).toLocaleString('es-CL')\n + '. Estado: ' + q.estado + '.';\n }\n} catch(e) {}\n\n// \u2500\u2500 3. AN\u00c1LISIS DEL HISTORIAL \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// Estado seg\u00fan \u00faltimo mensaje del asistente\nconst last
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.
gmailOAuth2httpBearerAuthhttpHeaderAuthtelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Trato Hecho - Chat Agent. Uses httpRequest, telegram, gmail. Webhook trigger; 26 nodes.
Source: https://github.com/Cristofer-SanMartin-Dev/Trato_Hecho/blob/8c13468e090660e015c5726ff9bacca9cd665e28/n8n/workflow_current.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.
leads. Uses supabase, gmail, formTrigger, httpRequest. Webhook trigger; 62 nodes.
This workflow automates document processing using LlamaParse to extract and analyze text from various file formats. It intelligently processes documents, extracts structured data, and delivers actiona
Whatsapp Lead Agent. Uses httpRequest, hunter, @tavily/n8n-nodes-tavily, @mendable/n8n-nodes-firecrawl. Webhook trigger; 35 nodes.
Universal Expense tracker. Uses telegram, httpRequest, openAi, googleSheets. Webhook trigger; 33 nodes.
Automate the entire product return process with an intelligent AI-powered agent that navigates return portals on your behalf 🤖. This workflow receives customer return requests, analyzes portal structu