Aprende, paso a paso, cómo ganar una de las 200 licencias de n8n por un año en Platzi. Aquí verás qué completar, cómo presentar tu flujo de automatización y qué requisitos cumplir para que la comunidad te vote.
¿Cómo ganar una licencia de n8n en Platzi?
Para participar debes cumplir tres acciones claras. La clave es demostrar un flujo útil y compartirlo con detalle para que otros estudiantes puedan reutilizarlo.
¿Qué debes completar antes de participar?
Termina uno de los tres nuevos cursos de n8n en Platzi.
Asegúrate de que tu progreso esté completo.
¿Cómo crear y presentar tu flujo de automatización?
Construye un flujo en n8n que sea útil y funcional.
Publica tu flujo en la sección de comentarios del curso.
Incluye una descripción detallada: qué problema resuelve y cómo lo hace.
Adjunta el archivo JSON del flujo para que otros puedan usarlo.
¿Cómo se eligen los ganadores?
Tu comentario debe quedar entre los 200 más votados por la comunidad de Platzi.
Si entras en ese grupo, ganas una licencia de n8n por un año.
¿Qué es n8n y por qué automatizar tareas?
Con n8n puedes automatizar flujos de trabajo en muy poco tiempo. La interfaz es intuitiva y visual, lo que facilita conectar herramientas y páginas web sin fricción.
¿Qué problemas puedes resolver con automatización?
Tareas repetitivas que haces más de una vez.
Procesos que consumen tiempo manual.
Integraciones entre múltiples servicios y sitios web.
¿Qué habilidades desarrollas al participar?
Diseño de flujos de automatización basados en objetivos.
Integración de herramientas y páginas web.
Documentación clara de problema y solución.
Compartir y versionar flujos mediante archivo JSON.
Participación en una comunidad con votación colaborativa.
¿Cuáles son las fechas, requisitos y dónde participar?
Toma nota de los plazos y condiciones para que tu participación sea válida desde el inicio hasta el anuncio de ganadores.
¿Qué fechas debes tener presentes?
Participación: desde hoy hasta el 30 de septiembre.
Anuncio de ganadores: 3 de octubre.
¿Qué requisitos son obligatorios?
Contar con suscripción anual activa de Platzi.
Máximo una licencia por persona.
¿Dónde encuentras los cursos y publicas tu participación?
Ingresa a la ruta: Platzi.com/n8n.
Ahí verás los tres cursos, el curso donde debes publicar tu comentario y los términos y condiciones.
¿Tienes dudas o quieres feedback de la comunidad? Publica tu flujo con su JSON y comparte tu experiencia en los comentarios.
Obtén respuestas inmediatasProfundiza lo que acabas de verObtén respuestas inmediatas
Hartos de perder 45 minutos diarios chequeando crypto prices como zombi 🧟♂️, armé este workflow que me hace la vida más fácil.. con n8n que obtiene precios de crypto y forex cada mañana, calcula mi portfolio automáticamente y me envía un reporte con análisis de IA vía Telegram. Transformé 45 minutos de trabajo manual en 15 segundos de información inteligente gracias a la magia de APIs + Google Gemini Chat Model + n8n.
Este es el JSON:
{"name":"Asistente Financiero Crypto","nodes":[{"parameters":{"rule":{"interval":[{"field":"hours","hoursInterval":8},{}]}},"type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[-352,16],"id":"331a5057-b1d4-47dc-8c8c-44d25e85df68","name":"Schedule Trigger"},{"parameters":{"url":"https://api.coingecko.com/api/v3/simple/price","sendQuery":true,"queryParameters":{"parameters":[{"name":"ids","value":"bitcoin,ethereum,cardano,solana"},{"name":"vs_currencies","value":"usd"},{"name":" include_24hr_change","value":"true"},{"name":"include_24hr_vol","value":"true"}]},"options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[-128,-176],"id":"e39d752f-d1ac-4a28-8cf6-830f8fb9be3f","name":"HTTP Request"},{"parameters":{"numberInputs":3},"type":"n8n-nodes-base.merge","typeVersion":3.2,"position":[96,0],"id":"ccb44ea4-0495-497d-8836-4f1cca63dcf9","name":"Merge"},{"parameters":{"url":"https://www.alphavantage.co/query","sendQuery":true,"queryParameters":{"parameters":[{"name":"function","value":"GLOBAL_QUOTE"},{"name":"symbol","value":"AAPL"},{"name":"apikey","value":"9930O1U69MD4SDPR"}]},"options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[-128,208],"id":"b9ede624-4618-4257-8779-389b08685ec9","name":"HTTP Request1"},{"parameters":{"url":"https://api.exchangerate-api.com/v4/latest/USD","options":{}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[-128,16],"id":"262e5004-a9d4-4c08-bdee-30b425660f29","name":"HTTP Request2"},{"parameters":{"jsCode":"const inputs = $input.all();\n\n// Encontrar cada tipo de data\nlet cryptoData = null;\nlet stockData = null; \nlet exchangeData = null;\n\nfor (const input of inputs) {\n const data = input.json;\n \n // Identificar crypto data (tiene bitcoin, ethereum, etc.)\n if (data.bitcoin && data.ethereum) {\n cryptoData = data;\n }\n \n // Identificar exchange data (tiene rates.COP)\n if (data.rates && data.rates.COP) {\n exchangeData = data;\n }\n \n // Stock data (por ahora undefined, lo saltamos)\n // if (data.chart || data.Global Quote) {\n // stockData = data;\n // }\n}\n\n// Tu portfolio (ajusta las cantidades)\nconst portfolio = {\n bitcoin: 0.001, // 0.001 BTC\n ethereum: 0.5, // 0.5 ETH\n cardano: 100, // 100 ADA\n solana: 5 // 5 SOL\n};\n\n// Calcular valor crypto\nlet cryptoValue = 0;\nlet cryptoDetails = [];\n\nfor (const [coin, amount] of Object.entries(portfolio)) {\n const price = cryptoData[coin]?.usd || 0;\n const change24h = cryptoData[coin]?.usd_24h_change || 0;\n const value = price * amount;\n \n cryptoValue += value;\n cryptoDetails.push({\n coin: coin.toUpperCase(),\n amount: amount,\n price: price,\n value: value,\n change24h: change24h\n });\n}\n\n// Por ahora, sin stocks\nconst stockValue = 0;\n\n// Total en USD\nconst totalUSD = cryptoValue + stockValue;\n\n// Convertir a COP\nconst usdToCop = exchangeData.rates.COP;\nconst totalCOP = totalUSD * usdToCop;\n\n// Simular cambio 24h (después puedes guardar histórico)\nconst yesterdayTotal = totalUSD * 0.98; // Simular +2%\nconst change24h = totalUSD - yesterdayTotal;\nconst changePercent = (change24h / yesterdayTotal) * 100;\n\nreturn {\n portfolio: {\n totalUSD: Math.round(totalUSD * 100) / 100,\n totalCOP: Math.round(totalCOP),\n change24h: Math.round(change24h * 100) / 100,\n changePercent: Math.round(changePercent * 100) / 100,\n cryptoValue: Math.round(cryptoValue * 100) / 100,\n stockValue: stockValue\n },\n crypto: cryptoDetails,\n exchangeRate: usdToCop,\n timestamp: new Date().toISOString(),\n summary: `Portfolio: $${Math.round(totalUSD)} USD (${Math.round(changePercent * 100) / 100}% 24h)`\n};"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[320,16],"id":"7207813b-43d9-4d70-9e5b-6f92cd116c09","name":"Code in JavaScript"},{"parameters":{"promptType":"define","text":"=Portfolio Analysis:\n- Total Value: ${{ $json.portfolio.totalUSD }} USD ({{ $json.portfolio.changePercent.toFixed(2) }}% cambio 24h)\n- Crypto: ${{ $json.portfolio.cryptoValue }} USD\n- Stocks: ${{ $json.portfolio.stockValue }} USD\n- USD/COP: {{ $json.exchangeRate }}\n\nTop performers:\n{{ $json.crypto.map(c => `${c.coin}: ${c.change24h.toFixed(2)}%`).join(', ') }}\n\nGenera insights y recomendaciones basadas en estos datos.","options":{"systemMessage":"Eres un asistente financiero experto. Analiza el portfolio y genera insights breves y accionables en español. Incluye:\n1. Resumen del performance\n2. Observación clave del día\n3. Recomendación específica (máximo 1)\n4. Perspectiva de mercado\n\nMantén el tono profesional pero accesible. Máximo 150 palabras."}},"type":"@n8n/n8n-nodes-langchain.agent","typeVersion":2.2,"position":[544,16],"id":"4dd8c7c4-643a-4aa0-b2f4-10e4247cd0c2","name":"AI Agent"},{"parameters":{"options":{}},"type":"@n8n/n8n-nodes-langchain.lmChatGoogleGemini","typeVersion":1,"position":[624,240],"id":"e0a99133-1e9a-4525-87e2-03ada80e0183","name":"Google Gemini Chat Model","credentials":{"googlePalmApi":{"id":"zw4pwDLZb1IAEiP8","name":"Google Gemini(PaLM) Api account"}}},{"parameters":{"chatId":"6816891581","text":"={{ $json.output }}","additionalFields":{"appendAttribution":false}},"type":"n8n-nodes-base.telegram","typeVersion":1.2,"position":[896,16],"id":"404dc3ab-533e-4f6e-ba5c-f9772833af5a","name":"Send a text message","webhookId":"0bebc186-a5a7-4bb1-9168-85323a09f264","credentials":{"telegramApi":{"id":"4qLaaFYHMumoS8Bk","name":"Telegram account"}}}],"pinData":{"Schedule Trigger":[{"json":{"timestamp":"2025-09-10T22:19:10.551-05:00","Readable date":"September 10th 2025, 10:19:10 pm","Readable time":"10:19:10 pm","Day of week":"Wednesday","Year":"2025","Month":"September","Day of month":"10","Hour":"22","Minute":"19","Second":"10","Timezone":"America/Bogota (UTC-05:00)"}}]},"connections":{"Schedule Trigger":{"main":[[{"node":"HTTP Request","type":"main","index":0},{"node":"HTTP Request1","type":"main","index":0},{"node":"HTTP Request2","type":"main","index":0}]]},"HTTP Request":{"main":[[{"node":"Merge","type":"main","index":0}]]},"HTTP Request1":{"main":[[{"node":"Merge","type":"main","index":2}]]},"HTTP Request2":{"main":[[{"node":"Merge","type":"main","index":1}]]},"Merge":{"main":[[{"node":"Code in JavaScript","type":"main","index":0}]]},"Code in JavaScript":{"main":[[{"node":"AI Agent","type":"main","index":0}]]},"Google Gemini Chat Model":{"ai_languageModel":[[{"node":"AI Agent","type":"ai_languageModel","index":0}]]},"AI Agent":{"main":[[{"node":"Send a text message","type":"main","index":0}]]}},"active":false,"settings":{"executionOrder":"v1"},"versionId":"0c289c6f-dde9-4ca5-95dd-e2f233a7ec70","meta":{"templateCredsSetupCompleted":true,"instanceId":"ddf856cf285d771c36044846386b128adabb88daa1e55ed70a1f1d071fe25e4b"},"id":"QxFOL18a95hX36Qo","tags":[]}
Genio
Link del repositorio y codigo, recuerden reemplazar el .env por sus credenciales como esta en .env.example
n8n-discord/n8n.json/Automatizacion.json at main · Nicolas-Barrerra1302/n8n-discord
🧠 Problema que resuelve
En mi equipo, a veces es un enredo saber qué tareas tiene cada quien. Hay que saltar de una app a otra (Notion, chats, recordatorios…) y si alguien más quería ese resumen, ¡me tocaba armarlo y pasarlo!
⚙️ ¿Cómo lo resuelve el flujo?
Con este bot, ahora simplemente escribimos en Discord:
/tareas modo:hoy
ó
/tareas modo:atrasadas
Y listo. El bot hace todo esto:
🔗 Envía un mensaje a n8n (usando un webhook) con el modo solicitado.
🛡️ Verifica la seguridad con un secreto compartido.
📅 Busca en Notion las tareas:
De hoy (si el modo es “hoy”).
Vencidas (si el modo es “atrasadas”).
🧩 Arma un mensaje bonito con:
Título con fecha y total de tareas.
Listado por responsable con viñetas.
Y si no hay tareas, avisa con un ✅.
💬 Devuelve el mensaje al bot, que lo muestra en Discord.
También corta el mensaje en partes si es muy largo, para que Discord lo acepte sin errores.
Configura los nodos “Hoy” y “Atrasadas” con tu base de Notion
Comparte tu base con la integración de Notion
Activa el flujo
6. ¡Probar!
npm run start
En tu Discord:
/tareas modo:hoy → muestra las tareas del día
/tareas modo:atrasadas → muestra las vencidas
🕗 Versión alternativa programada
También hice una variante sin el comando manual:
Reemplacé el webhook por un trigger programado en n8n a las 8:00 am todos los días. El flujo se conecta directamente a las dos bases de Notion y luego envía el mensaje automáticamente al canal de Discord 🎯
JSON de la automatizacion:
{"name":"Ahora si serio","nodes":[{"parameters":{"assignments":{"assignments":[{"id":"ce160e71-aa0c-42bc-a997-7aa86abc1826","name":"name","value":"={{ $json.name }}","type":"string"},{"id":"0c5080a8-b11e-4987-b584-cb6c8272f53a","name":"property_area_responsable","value":"={{ $json.property_area_responsable }}","type":"string"},{"id":"4d8f4bb8-45cd-4e45-ba7b-046204418dbe","name":"property_persona_responsable","value":"={{ $json.property_persona_responsable }}","type":"array"},{"id":"0ab39182-7bd8-4fb1-ad64-43da85f95abf","name":"property_proyecto","value":"={{ $json.property_proyecto }}","type":"string"},{"id":"1efe19ec-2f32-4ac0-b281-b372c645379d","name":"property_estado","value":"={{ $json.property_estado }}","type":"string"},{"id":"584fbb1e-0f10-4105-9f1c-3fe5bdf0653a","name":"property_fecha_de_entrega.start","value":"={{ $json.property_fecha_de_entrega.start }}","type":"string"}]},"options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[592,0],"id":"8db6bd00-290a-4ec9-a268-014c7164bd4c","name":"Edit Fields"},{"parameters":{"resource":"databasePage","operation":"getAll","databaseId":{"__rl":true,"value":"259d267f-b32c-800f-ae0e-d8e158031fd7","mode":"list","cachedResultName":"Tareas de equipo - Agosto","cachedResultUrl":"https://www.notion.so/259d267fb32c800fae0ed8e158031fd7"},"returnAll":true,"filterType":"manual","matchType":"allFilters","filters":{"conditions":[{"key":"Estado|status","condition":"does_not_equal","statusValue":"Listo"},{"key":"Fecha de entrega|date","condition":"equals","date":"={{ $now.toISODate() }}\n"}]},"options":{}},"type":"n8n-nodes-base.notion","typeVersion":2.2,"position":[368,0],"id":"cae7e27d-8386-4173-a7cd-e775f96bbfa3","name":"Hoy ","credentials":{"notionApi":{"id":"0iP1JSiK7f2R18Rb","name":"notion-tareas-equipo"}}},{"parameters":{"resource":"databasePage","operation":"getAll","databaseId":{"__rl":true,"value":"259d267f-b32c-800f-ae0e-d8e158031fd7","mode":"list","cachedResultName":"Tareas de equipo - Agosto","cachedResultUrl":"https://www.notion.so/259d267fb32c800fae0ed8e158031fd7"},"returnAll":true,"filterType":"manual","matchType":"allFilters","filters":{"conditions":[{"key":"Estado|status","condition":"does_not_equal","statusValue":"Listo"},{"key":"Fecha de entrega|date","condition":"before","date":"={{ $now.toISODate() }}\n"}]},"options":{}},"type":"n8n-nodes-base.notion","typeVersion":2.2,"position":[368,192],"id":"172d2221-5a75-4078-aff7-1ffb3b2b4f9e","name":"Atrasados","credentials":{"notionApi":{"id":"0iP1JSiK7f2R18Rb","name":"notion-tareas-equipo"}}},{"parameters":{"assignments":{"assignments":[{"id":"ce160e71-aa0c-42bc-a997-7aa86abc1826","name":"name","value":"={{ $json.name }}","type":"string"},{"id":"0c5080a8-b11e-4987-b584-cb6c8272f53a","name":"property_area_responsable","value":"={{ $json.property_area_responsable }}","type":"string"},{"id":"4d8f4bb8-45cd-4e45-ba7b-046204418dbe","name":"property_persona_responsable","value":"={{ $json.property_persona_responsable }}","type":"array"},{"id":"0ab39182-7bd8-4fb1-ad64-43da85f95abf","name":"property_proyecto","value":"={{ $json.property_proyecto }}","type":"string"},{"id":"1efe19ec-2f32-4ac0-b281-b372c645379d","name":"property_estado","value":"={{ $json.property_estado }}","type":"string"},{"id":"584fbb1e-0f10-4105-9f1c-3fe5bdf0653a","name":"property_fecha_de_entrega.start","value":"={{ $json.property_fecha_de_entrega.start }}","type":"string"}]},"options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[592,192],"id":"d0994a67-0a9b-441f-9a42-6c4f50f2530f","name":"Edit Fields1"},{"parameters":{"jsCode":"// ===== Header \"Tareas de hoy\" + bloques por responsable (código con code-blocks) =====\nconst groups = new Map();\nfor (const {json:j} of items) {\n const resp = Array.isArray(j.property_persona_responsable)\n ? (j.property_persona_responsable[0] || 'Sin responsable')\n : (j.property_persona_responsable || 'Sin responsable');\n\n const nombre = j.name || '(sin nombre)';\n const estado = j.property_estado || '—';\n const fecha = j.property_fecha_de_entrega?.start || '—';\n const area = j.property_area_responsable || '—';\n const proy = j.property_proyecto || '—';\n\n const line = `• ${nombre} — ${estado} — ${fecha} — Area: ${area} — Proyecto: ${proy}`;\n if (!groups.has(resp)) groups.set(resp, []);\n groups.get(resp).push(line);\n}\n\n// Ordenar por responsable (opcional)\nconst ordered = [...groups.entries()].sort((a,b)=>a[0].localeCompare(b[0]));\n\n// ---- Mensaje 1: Header resumen ----\nconst today = new Date().toISOString().slice(0,10);\nconst total = items.length;\nconst porResp = ordered.map(([r,arr]) => `${r}: ${arr.length}`).join(' | ');\nconst header = `**🚀 Tareas de hoy — ${today}**\\nTotal: ${total} · Responsables: ${ordered.length}\\n${porResp ? porResp : ''}`;\n\nconst msgs = [{ json: { text: header } }];\n\n// ---- Mensajes 2..N: Bloques por responsable (tu formato con ``` ) ----\nfor (const [resp, lines] of ordered) {\n const blockTitle = `\\n${resp}\\n`;\n const start = '```';\n const end = '```';\n\n // split si supera ~1900 chars\n let buf = start + blockTitle;\n for (const ln of lines) {\n if ((buf + ln + '\\n' + end).length > 1900) {\n buf += end;\n msgs.push({ json: { text: buf } });\n buf = start + blockTitle;\n }\n buf += ln + '\\n';\n }\n buf += end;\n msgs.push({ json: { text: buf } });\n}\n\nreturn msgs;\n"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[816,0],"id":"a69cafce-cfe0-41ff-8ec3-9f02a1a4220e","name":"Construye texto"},{"parameters":{"jsCode":"// ===== Header \"Tareas atrasadas\" + bloques por responsable (code-blocks) =====\nconst groups = new Map();\nfor (const {json:j} of items) {\n const resp = Array.isArray(j.property_persona_responsable)\n ? (j.property_persona_responsable[0] || 'Sin responsable')\n : (j.property_persona_responsable || 'Sin responsable');\n\n const nombre = j.name || '(sin nombre)';\n const estado = j.property_estado || '—';\n const fecha = j.property_fecha_de_entrega?.start || '—';\n const area = j.property_area_responsable || '—';\n const proy = j.property_proyecto || '—';\n\n const line = `• ${nombre} — ${estado} — ${fecha} — Area: ${area} — Proyecto: ${proy}`;\n if (!groups.has(resp)) groups.set(resp, []);\n groups.get(resp).push(line);\n}\n\n// Ordenar por responsable\nconst ordered = [...groups.entries()].sort((a,b)=>a[0].localeCompare(b[0]));\n\n// Header (atrasadas)\nconst today = new Date().toISOString().slice(0,10);\nconst total = items.length;\nif (total === 0) return [{ json: { text: '✅ No hay tareas atrasadas.' } }];\n\nconst porResp = ordered.map(([r,arr]) => `${r}: ${arr.length}`).join(' | ');\nconst header = `**⚠️ Tareas atrasadas — ${today}**\\nTotal: ${total} · Responsables: ${ordered.length}\\n${porResp || ''}`;\n\nconst msgs = [{ json: { text: header } }];\n\n// Bloques por responsable (``` )\nfor (const [resp, lines] of ordered) {\n const blockTitle = `\\n${resp}\\n`;\n const start = '```', end = '```';\n\n let buf = start + blockTitle;\n for (const ln of lines) {\n if ((buf + ln + '\\n' + end).length > 1900) {\n buf += end;\n msgs.push({ json: { text: buf } });\n buf = start + blockTitle;\n }\n buf += ln + '\\n';\n }\n buf += end;\n msgs.push({ json: { text: buf } });\n}\n\nreturn msgs;\n"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[816,192],"id":"26cb3b19-b06d-4edb-9743-f1a9b77af02a","name":"Construye texto2"},{"parameters":{"httpMethod":"POST","path":"discord-tareas","responseMode":"responseNode","options":{}},"type":"n8n-nodes-base.webhook","typeVersion":2.1,"position":[-80,96],"id":"a005b989-daaa-4a3f-80ec-e0bc0031b382","name":"Webhook","webhookId":"1e4e06a6-3d60-4e0c-b5f9-dc4ab046be37"},{"parameters":{"options":{}},"type":"n8n-nodes-base.respondToWebhook","typeVersion":1.4,"position":[1264,96],"id":"17c8aa4d-a0b0-4df8-b434-7c0a250e84f6","name":"Respond to Webhook"},{"parameters":{"assignments":{"assignments":[{"id":"57806f8c-425e-4224-9a2b-d8cdb24a52f9","name":"content","value":"={{$input.all().map(i => i.json.text).join('\\n\\n')}}","type":"string"}]},"options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[1040,0],"id":"1a9d6dcc-c93d-493a-b859-58f2f0be464a","name":"Edit Fields2"},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"57bbcb8e-3b16-4052-b6cb-d9274edfcf22","leftValue":"={{ ($json.body?.modo ?? '').toString().trim().toLowerCase() }}","rightValue":"hoy","operator":{"type":"string","operation":"equals","name":"filter.operator.equals"}}],"combinator":"and"},"options":{}},"type":"n8n-nodes-base.if","typeVersion":2.2,"position":[144,96],"id":"b25b3d54-b2e5-4088-aad6-2016f8e17887","name":"If"},{"parameters":{"assignments":{"assignments":[{"id":"57806f8c-425e-4224-9a2b-d8cdb24a52f9","name":"content","value":"={{$input.all().map(i => i.json.text).join('\\n\\n')}}","type":"string"}]},"options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[1040,192],"id":"1d9438de-82b6-4faa-b9f2-5be4aa719aea","name":"Edit Fields3"}],"pinData":{},"connections":{"Hoy ":{"main":[[{"node":"Edit Fields","type":"main","index":0}]]},"Atrasados":{"main":[[{"node":"Edit Fields1","type":"main","index":0}]]},"Edit Fields":{"main":[[{"node":"Construye texto","type":"main","index":0}]]},"Construye texto":{"main":[[{"node":"Edit Fields2","type":"main","index":0}]]},"Edit Fields1":{"main":[[{"node":"Construye texto2","type":"main","index":0}]]},"Construye texto2":{"main":[[{"node":"Edit Fields3","type":"main","index":0}]]},"Webhook":{"main":[[{"node":"If","type":"main","index":0}]]},"Edit Fields2":{"main":[[{"node":"Respond to Webhook","type":"main","index":0}]]},"If":{"main":[[{"node":"Hoy ","type":"main","index":0}],[{"node":"Atrasados","type":"main","index":0}]]},"Edit Fields3":{"main":[[{"node":"Respond to Webhook","type":"main","index":0}]]}},"active":true,"settings":{"executionOrder":"v1"},"versionId":"f6b79727-eaf5-4772-b1a8-ba3b02eebce0","meta":{"templateCredsSetupCompleted":true,"instanceId":"3a9e5ba55c7a7d974a3ef965384605dcf473b2802f173e4086ed0cf4c5d748cf"},"id":"ViZys7lExZXQ7uKr","tags":[]}
Muy bueno y util! Gracias
Cansado de no estar pendiente de los correos de mi jefe, cree esta autyomatizacion con n8n que revisa cada hora losd correos y toma aquellos que me haya enviado un remitente especifico, lo revisa y me da un resumen de estevia whatsapp gracias a la magiade Evolution API
"text": "=The User sent the following message\nToday:{{$now.format('yyyy-MM-dd')}}\nmail from:{{ $json.mail_from }}\nsubject:{{ $json.mail_subject }}\nMail id:{{ $json.chat_id }}\n\n\nMessage text or description\n\n{{ $json.mail_text }}\n\nmail id:{{ $json.chat_id }}",
"options": {
"systemMessage": "=Eres un asistente que recibira informacion de un correo especifico, deberas revisar el contenido del correo, hacer un resumen y redactar un mensaje indicando lo siguiente\n\n*Persona que envia el correo\n*email como vinculo\n*email id\n*Asunto\n*Resumen\n*Actividades a realizar\n\nEl objetivo es poder responder este correo desde el mismo Whatsapp\n\nHazlo mas ameno indciando que recibiste un correo de la persona y el asunto\n\nindica que para responder debera enviar el email id y el email\n\n"
Les comparto mi proyecto para el concurso, que no es solo un flujo, sino un Patrón de Arquitectura de Resiliencia 3-en-1 para llevar sus automatizaciones a un nivel profesional.
¿El Problema? 😩
Todos hemos sufrido por APIs que fallan, conexiones que se caen o servicios que se saturan. Un flujo que no maneja estos errores es un flujo frágil que puede dejar de funcionar en el peor momento.
Mi Solución: Un Kit de Herramientas Reutilizable 🛠️
Inspirado en los principios de diseño de software que vemos en la industria, he creado una arquitectura de 3 capas que pueden aplicar a CUALQUIER API que quieran consumir desde n8n.
Les adjunto 3 flujos que trabajan juntos:
El Cerebro 🧠 (Manejador_CircuitBreaker.json): Un micro-servicio que centraliza el patrón Circuit Breaker. Monitorea fallos de cualquier servicio y, si un API falla repetidamente, "abre el circuito" para evitar que nuestro sistema siga intentando y gastando recursos. Después de un tiempo, intenta de nuevo para ver si el servicio se ha recuperado. Todo de forma atómica usando Redis.
El Músculo 💪 (TIL-Resilient-API-Executor-v1.json): ¡La joya de la corona! Un sub-workflow genérico que actúa como un wrapper o "envoltorio" para cualquier llamada HTTP. Solo tienes que pasarle la URL, método, body, etc., y él se encarga de todo:
Aplica el patrón Cache-Aside: Guarda respuestas exitosas en Redis para no volver a preguntar lo mismo, ahorrando tiempo y dinero (¡ideal para APIs de paga!).
Integra el Circuit Breaker: Antes de llamar a cualquier API, le pregunta al "Cerebro" si es seguro proceder. Si el servicio está caído, falla inmediatamente sin esperar, haciendo nuestro flujo más rápido y resiliente.
Manejo de Errores Estandarizado: Devuelve respuestas predecibles, ya sea de éxito o de error, para que tus flujos principales (orquestadores) sean limpios y fáciles de leer.
El Ejemplo Práctico 🏥 (Herramienta_DirectorioMedico_BuscarServicios_v1.json): Una "Herramienta" que demuestra cómo usar el ejecutor resiliente. Este flujo tiene su propia lógica de negocio (buscar servicios médicos), pero en lugar de hacer la llamada HTTP directamente, prepara los datos y se los pasa al "Músculo". ¡Así, la lógica de negocio queda separada de la lógica de resiliencia!
¿Por qué es útil para la comunidad? 🤔
Con este patrón, dejas de preocuparte por el cómo hacer llamadas a APIs de forma segura en cada flujo y te centras en el qué (la lógica de tu negocio). Es un framework reutilizable, escalable y mantenible que te ayudará a construir automatizaciones de nivel empresarial.
¡Espero que les sirva para sus propios proyectos! Si creen que construir flujos robustos y a prueba de fallos es importante, les agradecería mucho su voto. ✨
¡A seguir automatizando! 🤖
- El Cerebro 🧠
{"name":"Manejador_CircuitBreaker","nodes":[{"parameters":{"workflowInputs":{"values":[{"name":"serviceName"},{"name":"failureThreshold"},{"name":"resetTimeoutSeconds"},{"name":"mode"}]}},"type":"n8n-nodes-base.executeWorkflowTrigger","typeVersion":1.1,"position":[0,80],"id":"1956230e-80bf-4481-a023-658a1971a2ef","name":"Disparador: Recibir Fallo"},{"parameters":{"jsCode":"// =================================================================\n// Manejador de Circuit Breaker\n// =================================================================\nconst Redis = require('ioredis');\n\nasync function main() {\n // --- 1. Entrada y Configuración ---\n const input = $input.first()?.json ?? {};\n const { serviceName } = input;\n \n // Usamos el operador '??' para asignar valores por defecto si el input es null O undefined.\n const mode = input.mode ?? 'CHECK';\n const failureThreshold = input.failureThreshold ?? 5;\n const resetTimeoutSeconds = input.resetTimeoutSeconds ?? 60;\n\n if (!serviceName) {\n throw new Error(\"Input error: 'serviceName' debe ser proporcionado.\");\n }\n\n // Usamos un Hash de Redis para almacenar todo el estado del servicio de forma agrupada.\n const redisKey = `cb:${serviceName}`;\n\n // --- 2. Conexión a Redis ---\n // Se utilizan las variables de entorno configuradas en Docker.\n const redis = new Redis({\n host: $env.REDIS_HOST,\n port: parseInt($env.REDIS_PORT),\n password: $env.REDIS_PASSWORD,\n lazyConnect: true,\n enableOfflineQueue: false, // Falla rápido si no puede conectar.\n });\n\n let result = {};\n\n try {\n await redis.connect();\n\n // --- 3. Lógica de Modos ---\n switch (mode) {\n case 'SUCCESS':\n // En un éxito, especialmente desde HALF-OPEN, cerramos el circuito.\n // multi() asegura que los dos comandos se ejecuten atómicamente.\n await redis.multi()\n .hset(redisKey, 'state', 'CLOSED')\n .hset(redisKey, 'failureCount', 0)\n .exec();\n result = { state: 'CLOSED', details: 'Éxito reportado. El circuito ahora está cerrado.' };\n break;\n\n case 'FAILURE':\n // En un fallo, incrementamos el contador atómicamente.\n const newFailureCount = await redis.hincrby(redisKey, 'failureCount', 1);\n\n if (newFailureCount >= failureThreshold) {\n // Umbral alcanzado. Abrimos el circuito y establecemos el timestamp de apertura.\n const opensAt = Date.now() + (resetTimeoutSeconds * 1000);\n await redis.multi()\n .hset(redisKey, 'state', 'OPEN')\n .hset(redisKey, 'opensAt', opensAt)\n .exec();\n result = { state: 'OPEN', details: `Umbral de fallos (${newFailureCount}) alcanzado. El circuito ahora está abierto.` };\n } else {\n result = { state: 'CLOSED', details: `Fallo reportado. Nuevo contador: ${newFailureCount}.` };\n }\n break;\n\n case 'CHECK':\n default:\n const circuit = await redis.hgetall(redisKey);\n const state = circuit.state ?? 'CLOSED';\n\n if (state === 'OPEN') {\n const opensAt = parseInt(circuit.opensAt ?? '0', 10);\n if (Date.now() > opensAt) {\n // El tiempo de espera ha pasado. Movemos a HALF-OPEN para permitir una llamada de prueba.\n await redis.hset(redisKey, 'state', 'HALF_OPEN');\n result = { state: 'PROCEED', currentState: 'HALF-OPEN', details: 'Timeout expirado. Permitiendo una llamada de prueba.' };\n } else {\n result = { state: 'FAIL_FAST', currentState: 'OPEN', details: 'El circuito está abierto. Llamada bloqueada.' };\n }\n } else { // El estado es CLOSED o HALF-OPEN\n const failureCount = parseInt(circuit.failureCount ?? '0', 10);\n result = { state: 'PROCEED', currentState: state, details: `El circuito está ${state}. Llamada permitida. Fallos actuales: ${failureCount}.` };\n }\n break;\n }\n\n } catch (error) {\n // Para producción, retornamos un objeto de error estructurado.\n return [{ json: { error: true, message: error.message, serviceName: serviceName } }];\n } finally {\n // Siempre cerramos la conexión para liberar recursos.\n if (redis.status === 'ready') {\n await redis.quit();\n }\n }\n\n // Retornamos el resultado en el formato que n8n espera.\n return [{ json: { ...result, serviceName: serviceName } }];\n}\n\nreturn main();"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[224,80],"id":"34755afb-e975-43a7-9923-8afae1a50cb3","name":"Code: Atomically Report Failure"},{"parameters":{"content":"# 📑 Workflow: Manejador de Circuit Breaker Atómico\n\nEste workflow implementa un patrón de diseño de software **Circuit Breaker** utilizando Redis para gestionar el estado de los servicios de manera atómica y resiliente. Su propósito es prevenir que una aplicación intente ejecutar repetidamente una operación que es propensa a fallar, evitando la saturación de recursos y mejorando la experiencia del usuario.\n\n---\n\n## 🚀 Propósito del Workflow\n\nEl patrón **Circuit Breaker** actúa como un proxy o intermediario para operaciones que pueden fallar, como las llamadas a servicios externos. Monitorea las fallas y, si superan un umbral, \"abre\" el circuito, haciendo que las llamadas posteriores fallen inmediatamente (`FAIL_FAST`) sin ejecutar la lógica principal. Después de un tiempo de espera, el circuito pasa a un estado de \"medio abierto\" (`HALF-OPEN`) para permitir una llamada de prueba. Si esta tiene éxito, el circuito se \"cierra\" (`CLOSED`) y el servicio vuelve a la normalidad.\n\nEste manejador centraliza la lógica del Circuit Breaker para que cualquier otro servicio o workflow pueda consultarlo y reportar fallos o éxitos de manera sencilla.\n\n---\n\n## 🧩 Nodos del Workflow\n\nEl workflow se compone de dos nodos principales.\n\n### 1. ⚡️ Disparador: Recibir Fallo (`ExecuteWorkflowTrigger`)\n\nEste es el punto de entrada del workflow. Está configurado para recibir datos de entrada que definen la operación a realizar.\n\n* **Nombre del Nodo**: `Disparador: Recibir Fallo`\n* **ID del Nodo**: `1956230e-80bf-4481-a023-658a1971a2ef`\n* **Función**: Activa la ejecución del workflow cuando es llamado, pasando los parámetros de entrada al siguiente nodo.\n\n### 2. 🤖 Código: Lógica Principal (`Code`)\n\nEste nodo contiene toda la lógica del Circuit Breaker. Se conecta a Redis para almacenar y gestionar el estado de cada servicio de forma atómica y segura.\n\n* **Nombre del Nodo**: `Code: Atomically Report Failure`\n* **ID del Nodo**: `34755afb-e975-43a7-9923-8afae1a50cb3`\n* **Función**: Ejecuta el código JavaScript que implementa los diferentes modos y estados del Circuit Breaker.\n\n---\n\n## ⚙️ Parámetros de Entrada (Inputs)\n\nPara invocar este workflow, necesitas proporcionar un objeto JSON con los siguientes campos.\n\n| Parámetro | Tipo | Obligatorio | Por Defecto | Descripción |\n| :--- | :--- | :--- | :--- | :--- |\n| `serviceName` | `String` | **Sí** | `N/A` | El identificador único del servicio que se está monitoreando (e.g., `api-pagos`, `servicio-notificaciones`). |\n| `mode` | `String` | No | `'CHECK'` | El modo de operación. Define la acción que el workflow debe realizar. |\n| `failureThreshold` | `Number` | No | `5` | El número de fallos consecutivos necesarios para abrir el circuito. |\n| `resetTimeoutSeconds` | `Number` | No | `60` | El tiempo en segundos que el circuito permanecerá abierto antes de pasar a `HALF-OPEN`. |\n\n---\n\n## 🕹️ Modos de Operación\n\nEl parámetro `mode` determina el comportamiento del workflow.\n\n### `CHECK` (Modo por defecto)\nVerifica el estado actual del circuito para un servicio y determina si la llamada debe proceder.\n* **Si el circuito está `CLOSED` o `HALF-OPEN`**: Devuelve `PROCEED`, permitiendo la ejecución de la llamada.\n* **Si el circuito está `OPEN` y el timeout no ha expirado**: Devuelve `FAIL_FAST`, bloqueando la llamada.\n* **Si el circuito está `OPEN` y el timeout ya expiró**: Cambia el estado a `HALF-OPEN` y devuelve `PROCEED` para permitir una llamada de prueba.\n\n### `FAILURE`\nReporta un fallo para el servicio.\n* Incrementa el contador de fallos de forma atómica (`hincrby`).\n* Si el contador alcanza el `failureThreshold`, el estado cambia a `OPEN` y se establece un timestamp para la reapertura.\n\n### `SUCCESS`\nReporta una ejecución exitosa para el servicio.\n* Resetea el estado del circuito a `CLOSED` y el contador de fallos a `0`.\n* Es especialmente útil cuando una llamada de prueba en el estado `HALF-OPEN` tiene éxito, cerrando el circuito de nuevo.\n\n---\n\n## 📤 Salidas del Workflow (Outputs)\n\nEl workflow siempre devuelve un objeto JSON con el resultado de la operación, proporcionando información clara sobre el estado del circuito.\n\nEjemplo de Salida (`CHECK` en estado `OPEN`):\n```json\n{\n \"state\": \"FAIL_FAST\",\n \"currentState\": \"OPEN\",\n \"details\": \"El circuito está abierto. Llamada bloqueada.\",\n \"serviceName\": \"api-pagos\"\n}\n```\n\nEjemplo de Salida (FAILURE sin alcanzar el umbral):\n```json\n{\n \"state\": \"CLOSED\",\n \"details\": \"Fallo reportado. Nuevo contador: 2.\",\n \"serviceName\": \"api-pagos\"\n}\n```\n\nEjemplo de Salida (SUCCESS):\n```json\n{\n \"state\": \"CLOSED\",\n \"details\": \"Éxito reportado. El circuito ahora está cerrado.\",\n \"serviceName\": \"api-pagos\"\n}\n```\n\nEjemplo de Salida (Error):\n```json\n{\n \"error\": true,\n \"message\": \"Input error: 'serviceName' debe ser proporcionado.\",\n \"serviceName\": null\n}\n```\n\n---\n\n🔑 Variables de Entorno\n\nPara que el workflow funcione correctamente, es necesario configurar las siguientes variables de entorno en tu instancia de n8n para la conexión a Redis:\n\n- REDIS_HOST: La dirección del servidor de Redis.\n- REDIS_PORT: El puerto del servidor de Redis.\n- REDIS_PASSWORD: La contraseña para autenticarse en Redis.\n\n---\n\n🛠️ Lógica de Implementación y Dependencias\n\nRedis: Se utiliza Redis como almacén de estado por su velocidad y sus operaciones atómicas. Cada servicio se guarda en un Hash de Redis con la clave `cb:serviceName`. Esto agrupa toda la información de un servicio (`state`, `failureCount`, `opensAt`) en una única clave, mejorando la organización.\n\nAtomicidad: Las operaciones críticas, como reportar un éxito o alcanzar el umbral de fallo, se ejecutan dentro de una transacción `multi()` de Redis. Esto garantiza que los comandos se ejecuten de forma atómica, evitando condiciones de carrera.\n\nDependencias: El nodo de código utiliza la librería `ioredis` para interactuar con Redis.","height":3504,"width":992,"color":7},"type":"n8n-nodes-base.stickyNote","position":[-1088,-288],"typeVersion":1,"id":"cbeb8ddb-0cdb-4233-9748-3bd130b906a4","name":"Sticky Note"}],"pinData":{},"connections":{"Disparador: Recibir Fallo":{"main":[[{"node":"Code: Atomically Report Failure","type":"main","index":0}]]}},"active":false,"settings":{"executionOrder":"v1"},"versionId":"cccf02fe-f1af-48f3-8119-eb952c384b63","meta":{"instanceId":"c44159ad241c0d6d40c4f5053f46c53c8d2c806950fcbaf71270c9d0c4993311"},"id":"hX5n6drNEZEdcuER","tags":[]}
El Músculo 💪
{"name":"TIL-Resilient-API-Executor-v1","nodes":[{"parameters":{"workflowInputs":{"values":[{"name":"url"},{"name":"method"},{"name":"options","type":"object"},{"name":"headers","type":"object"},{"name":"body","type":"object"},{"name":"queryParams","type":"object"}]}},"type":"n8n-nodes-base.executeWorkflowTrigger","typeVersion":1.1,"position":[0,240],"id":"4bca53d5-f470-4da3-b10e-b67bf22cd970","name":"When Executed by Another Workflow"},{"parameters":{"jsCode":"// Paso 1: Recoger los datos de entrada del disparador.\nconst input = $input.first().json;\nconst errors = [];\n\n// Paso 2: Definir los campos que son absolutamente obligatorios para esta utilidad.\nconst requiredFields = ['url', 'method'];\n\n// Paso 3: Verificar que cada campo requerido exista y no esté vacío.\nrequiredFields.forEach(field => {\n if (!input[field] || String(input[field]).trim() === '') {\n errors.push(`Error de Validación: El parámetro requerido '${field}' está ausente o vacío.`);\n }\n});\n\n// Puedes añadir más validaciones aquí en el futuro (ej. validar que 'method' sea uno de [GET, POST...])\n\n// Paso 4: Devolver un resultado estructurado y predecible.\nreturn [{\n json: {\n isValid: errors.length === 0,\n errors: errors,\n validatedInput: input\n }\n}];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[224,240],"id":"923288ef-d21d-41f4-8359-63ac26edc2da","name":"Validar Entradas Requeridas"},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"1af75126-a8b0-4470-b4e1-90bc3c71ea96","leftValue":"={{ $('Validar Entradas Requeridas').item.json.isValid }}","rightValue":"","operator":{"type":"boolean","operation":"true","singleValue":true}}],"combinator":"and"},"options":{}},"type":"n8n-nodes-base.if","typeVersion":2.2,"position":[448,240],"id":"480267fc-c782-449e-b44c-c168eaec1a2f","name":"¿Entrada Válida?"},{"parameters":{"mode":"raw","jsonOutput":"={\n \"status\": \"error\",\n \"error\": {\n \"code\": \"VALIDATION_ERROR\",\n \"message\": \"{{ $('Validar Entradas Requeridas').item.json.errors.join(' ') }}\",\n \"details\": {\n \"validationErrors\": {{ $('Validar Entradas Requeridas').item.json.errors }}\n }\n }\n}","options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[3360,768],"id":"eaab14be-5d06-47e8-9d87-352f29979760","name":"Formatear Error de Validación"},{"parameters":{},"type":"n8n-nodes-base.merge","typeVersion":3.2,"position":[3584,288],"id":"566618e6-0bfb-4e33-8aea-7fb9ec4d2341","name":"Unificar Salida"},{"parameters":{},"type":"n8n-nodes-base.noOp","typeVersion":1,"position":[3808,288],"id":"f422b2a0-4328-4277-b708-f447e6ba3ebf","name":"Salida Final"},{"parameters":{"assignments":{"assignments":[{"id":"7f7b18bc-3f94-4142-a7b9-5ac1b6c86a98","name":"cacheKey","value":"={{ $json.validatedInput.options?.resilience?.cacheKey }}","type":"string"}]},"options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[672,144],"id":"3824767d-6a3d-4a32-8c8f-2cc8cfd9ab0e","name":"Generar Clave de Caché"},{"parameters":{"operation":"get","propertyName":"data","key":"={{ $('Generar Clave de Caché').item.json.cacheKey }}","options":{"dotNotation":false}},"type":"n8n-nodes-base.redis","typeVersion":1,"position":[896,144],"id":"6c31cebb-f8a3-4fa8-9fb0-4f95bbbd9d3f","name":"Redis: Get Cache","alwaysOutputData":true,"credentials":{"redis":{"id":"LL0wbjODiDgesNTq","name":"Redis Docker Compose Local"}},"onError":"continueErrorOutput"},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"9cb32898-66b8-4761-8bee-a62972a9faf2","leftValue":"={{ $('Redis: Get Cache').item.json.data }}","rightValue":"","operator":{"type":"string","operation":"notEmpty","singleValue":true}}],"combinator":"and"},"options":{}},"type":"n8n-nodes-base.if","typeVersion":2.2,"position":[1120,144],"id":"9ab0d418-e078-475f-8965-077c937867f6","name":"IF: Cache Hit?"},{"parameters":{"workflowId":{"__rl":true,"value":"hX5n6drNEZEdcuER","mode":"list","cachedResultName":"Manejador_CircuitBreaker"},"workflowInputs":{"mappingMode":"defineBelow","value":{"resetTimeoutSeconds":"60","failureThreshold":"5","serviceName":"={{ $('Validar Entradas Requeridas').item.json.validatedInput.options?.resilience?.circuitBreakerKey }}","mode":"CHECK"},"matchingColumns":[],"schema":[{"id":"serviceName","displayName":"serviceName","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"string"},{"id":"failureThreshold","displayName":"failureThreshold","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"string"},{"id":"resetTimeoutSeconds","displayName":"resetTimeoutSeconds","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"string"},{"id":"mode","displayName":"mode","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"string"}],"attemptToConvertTypes":false,"convertFieldsToString":true},"options":{"waitForSubWorkflow":true}},"type":"n8n-nodes-base.executeWorkflow","typeVersion":1.2,"position":[1344,384],"id":"a45a6294-011f-4f81-a146-2693f899fd83","name":"CB: Verificar Estado"},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"d3f9bdc2-7961-484c-8343-780d4566f082","leftValue":"={{ $('CB: Verificar Estado').item.json.state }}","rightValue":"PROCEED","operator":{"type":"string","operation":"equals","name":"filter.operator.equals"}}],"combinator":"and"},"options":{}},"type":"n8n-nodes-base.if","typeVersion":2.2,"position":[1568,384],"id":"28ca3649-2b76-4862-a11f-5c1ed945e89c","name":"IF: ¿Circuito Permite Proceder?"},{"parameters":{"mode":"raw","jsonOutput":"={\n \"status\": \"error\",\n \"error\": {\n \"code\": \"CIRCUIT_BREAKER_OPEN\",\n \"message\": \"El servicio no está disponible temporalmente. Por favor, inténtelo de nuevo más tarde.\",\n \"details\": {\n \"diagnosticMessage\": \"{{ $('CB: Verificar Estado').item.json.details }}\"\n }\n }\n}","options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[3360,576],"id":"56cdf0c9-20a6-4d15-86b6-46646f0330d4","name":"Formatear Error de Circuito Abierto"},{"parameters":{"rules":{"values":[{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"leftValue":"={{ $json.statusCode?.toString() }}","rightValue":"2","operator":{"type":"string","operation":"startsWith"},"id":"5057a7f9-a69c-4367-a3f6-344d5521c8ce"}],"combinator":"and"},"renameOutput":true,"outputKey":"Éxito 2xx"},{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"01841ae9-ced5-4cef-94ef-72cba6073952","leftValue":"={{ ($json.statusCode ?? $json.error?.status)?.toString() }}","rightValue":"5","operator":{"type":"string","operation":"startsWith"}}],"combinator":"and"},"renameOutput":true,"outputKey":"Error Servidor (5xx)"}]},"options":{"fallbackOutput":"extra","renameFallbackOutput":"Error de Cliente (4xx)"}},"type":"n8n-nodes-base.switch","typeVersion":3.2,"position":[2240,176],"id":"dc8ec6be-8321-484f-8613-327f787be1f8","name":"Switch: Resultado de API"},{"parameters":{"operation":"set","key":"={{ $('Generar Clave de Caché').item.json.cacheKey }}","value":"={{ JSON.stringify($('Formatear Salida de Éxito').item.json) }}","expire":true,"ttl":"={{ $('Validar Entradas Requeridas').item.json.validatedInput.options?.resilience?.cacheTTL ?? 3600 }}"},"type":"n8n-nodes-base.redis","typeVersion":1,"position":[2912,-128],"id":"ba73591a-c084-42b0-84f8-6ce3bbf2c566","name":"Redis: Set Cache","credentials":{"redis":{"id":"LL0wbjODiDgesNTq","name":"Redis Docker Compose Local"}}},{"parameters":{"workflowId":{"__rl":true,"value":"hX5n6drNEZEdcuER","mode":"list","cachedResultName":"Manejador_CircuitBreaker"},"workflowInputs":{"mappingMode":"defineBelow","value":{"resetTimeoutSeconds":"60","failureThreshold":"5","mode":"SUCCESS","serviceName":"={{ $('Validar Entradas Requeridas').item.json.validatedInput.options?.resilience?.circuitBreakerKey }}"},"matchingColumns":[],"schema":[{"id":"serviceName","displayName":"serviceName","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"string"},{"id":"failureThreshold","displayName":"failureThreshold","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"string"},{"id":"resetTimeoutSeconds","displayName":"resetTimeoutSeconds","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"string"},{"id":"mode","displayName":"mode","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"string"}],"attemptToConvertTypes":false,"convertFieldsToString":true},"options":{}},"type":"n8n-nodes-base.executeWorkflow","typeVersion":1.2,"position":[3136,-128],"id":"56c44db1-e8b8-474e-a25f-14dd0f1f3ec9","name":"CB: Reportar Éxito"},{"parameters":{"mode":"raw","jsonOutput":"={\n \"status\": \"success\",\n \"data\": {{ $('API: Llamada Dinámica').item.json }}\n}","options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[2464,-48],"id":"f63e9ae0-7ce9-46b3-90fd-4229a5357ceb","name":"Formatear Salida de Éxito"},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"0afabb10-57e6-49cb-8a09-d4016e7bdef2","leftValue":"={{ $('Generar Clave de Caché').item.json.cacheKey }}","rightValue":"","operator":{"type":"string","operation":"notEmpty","singleValue":true}}],"combinator":"and"},"options":{}},"type":"n8n-nodes-base.if","typeVersion":2.2,"position":[2688,-48],"id":"7e9034b8-89d9-4d0b-a6f5-274ddf551335","name":"IF: ¿Caché Habilitado?"},{"parameters":{"mode":"raw","jsonOutput":"={{ $('Formatear Salida de Éxito').item.json }}","options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[3360,-48],"id":"f9a33f8f-5eac-4ad9-ad51-d12ced08d84e","name":"Preparar Salida Final de Éxito"},{"parameters":{"workflowId":{"__rl":true,"value":"hX5n6drNEZEdcuER","mode":"list","cachedResultName":"Manejador_CircuitBreaker"},"workflowInputs":{"mappingMode":"defineBelow","value":{"resetTimeoutSeconds":"60","failureThreshold":"5","mode":"FAILURE","serviceName":"={{ $('Validar Entradas Requeridas').item.json.validatedInput.options?.resilience?.circuitBreakerKey }}"},"matchingColumns":[],"schema":[{"id":"serviceName","displayName":"serviceName","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"string"},{"id":"failureThreshold","displayName":"failureThreshold","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"string"},{"id":"resetTimeoutSeconds","displayName":"resetTimeoutSeconds","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"string"},{"id":"mode","displayName":"mode","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"string"}],"attemptToConvertTypes":false,"convertFieldsToString":true},"options":{}},"type":"n8n-nodes-base.executeWorkflow","typeVersion":1.2,"position":[3136,192],"id":"b7e75aa3-704d-4423-bd7b-306956055e0d","name":"CB: Reportar Fallo"},{"parameters":{"mode":"raw","jsonOutput":"={\n \"status\": \"error\",\n \"error\": {\n \"code\": \"API_SERVER_ERROR\",\n \"message\": \"La petición falló debido a un error temporal en el servidor. Podría funcionar si se reintenta.\",\n \"details\": {\n \"httpStatusCode\": {{ $('API: Llamada Dinámica').item.json.error?.status ?? \"N/A\" }},\n \"responseBody\": \"{{ $('API: Llamada Dinámica').item.json.error?.message?.replace(/\"/g, '\\\\\"') ?? \"No body available\" }}\",\n \"circuitBreakerInfo\": {{ $('CB: Reportar Fallo').item.json }}\n }\n }\n}","options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[3360,192],"id":"c6d0ffe7-ce01-4f74-af52-a1cca943fcbd","name":"Formatear Error de Servidor"},{"parameters":{"mode":"raw","jsonOutput":"={\n \"status\": \"error\",\n \"error\": {\n \"code\": \"API_CLIENT_ERROR\",\n \"message\": \"La petición falló debido a un error en los datos enviados (ej. no autorizado o no encontrado).\",\n \"details\": {\n \"httpStatusCode\": {{ $('API: Llamada Dinámica').item.json.error?.status ?? \"N/A\" }},\n \"responseBody\": \"{{ $('API: Llamada Dinámica').item.json.error?.message?.replace(/\"/g, '\\\\\"') ?? \"No body available\" }}\"\n }\n }\n}","options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[3360,384],"id":"8db0bd7c-d97d-4fcc-8a05-3ae5252a4e02","name":"Formatear Error de Cliente"},{"parameters":{"content":"# 🏛️ Workflow: Generic & Resilient API Executor (TIL-Resilient-API-Executor-v1)\n\nEste sub-workflow actúa como una **capa de ejecución de API genérica y centralizada**. Su propósito es abstraer la complejidad de realizar llamadas HTTP, decorándolas en tiempo de ejecución con patrones de resiliencia de nivel empresarial como **Cache-Aside** y **Circuit Breaker**.\n\n---\n\n## 🚀 Propósito y Arquitectura\n\nEste workflow es la capa de **Utilidad Genérica** en una arquitectura de tres capas (Orquestador -> Herramienta -> Utilidad). Su única responsabilidad es ejecutar llamadas API de manera segura y eficiente, permitiendo que las \"Herramientas\" (otros sub-workflows) se centren exclusivamente en la lógica de negocio.\n\n- **Proxy Pattern**: Intercepta las solicitudes API para añadir lógica de resiliencia y caché. \n- **Decorator Pattern**: \"Decora\" una simple llamada HTTP con funcionalidades avanzadas sin alterar la lógica de la herramienta que la invoca. \n- **Single Responsibility Principle**: Se encarga del \"cómo\" (la ejecución técnica), mientras que la herramienta que lo llama se encarga del \"qué\" (la lógica de negocio).\n\n---\n\n## 📥 Contrato de Entrada (Universal API Request DTO)\n\nPara invocar este workflow, se debe proporcionar un objeto JSON que cumpla con el siguiente contrato. Cualquier campo no especificado aquí será ignorado.\n\n| Campo | Tipo | Requerido | Descripción |\n| :--- | :--- | :---: | :--- |\n| `url` | `String` | Sí | La URL completa del endpoint al que se llamará. |\n| `method` | `String` | Sí | El método HTTP a utilizar (e.g., `GET`, `POST`, `PUT`, `DELETE`). |\n| `headers` | `Object` | No | Un objeto con los encabezados HTTP de la solicitud. |\n| `body` | `Object` | No | El cuerpo de la solicitud, típicamente un objeto JSON. |\n| `queryParams` | `Object` | No | Un objeto con los parámetros de consulta para la URL. |\n| `options.resilience.cacheKey` | `String` | No | Clave única para buscar/guardar la respuesta en Redis. Si se omite, se salta el caché. |\n| `options.resilience.cacheTTL` | `Number` | No | Tiempo de vida (en segundos) para la entrada de caché. Por defecto `3600`. |\n| `options.resilience.circuitBreakerKey`| `String` | No | Identificador único del servicio para el Circuit Breaker. Si se omite, no se verifica. |\n\n**Ejemplo de Invocación:**\n```json\n{\n \"url\": \"https://api.example.com/users\",\n \"method\": \"POST\",\n \"headers\": {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": \"your-secret-key\"\n },\n \"body\": {\n \"name\": \"John Doe\",\n \"email\": \"john.doe@example.com\"\n },\n \"options\": {\n \"resilience\": {\n \"cacheKey\": \"users:john.doe@example.com\",\n \"cacheTTL\": 86400,\n \"circuitBreakerKey\": \"example-api-users\"\n }\n }\n}\n```\n\n---\n\n## 📤 Contrato de Salida (Standardized Response)\n\nEl workflow siempre devuelve un objeto JSON estructurado y predecible para que el workflow llamador pueda manejar el resultado de forma consistente.\n\n### ✅ Salida Exitosa (status: \"success\")\n\nSi la validación, caché, circuit breaker y la llamada API son exitosos, la salida tendrá la siguiente estructura. El objeto `data` contiene la respuesta completa de la API.\n\n```json\n{\n \"status\": \"success\",\n \"data\": {\n \"statusCode\": 201,\n \"headers\": {\n \"content-type\": \"application/json\"\n },\n \"body\": {\n \"id\": \"usr_12345\",\n \"message\": \"User created successfully\"\n }\n }\n}\n```\n\n### ❌ Salida de Error (status: \"error\")\n\nSi ocurre cualquier tipo de error, la salida contendrá un objeto `error` estructurado para facilitar el debugging y la recuperación.\n\n| error.code | Origen | Descripción |\n| :--- | :--- | :--- |\n| VALIDATION_ERROR | Validación de Entrada | Los parámetros requeridos (`url`, `method`) faltan o están vacíos. |\n| CIRCUIT_BREAKER_OPEN | Circuit Breaker | El servicio externo está marcado como no disponible y la llamada fue bloqueada. |\n| API_CLIENT_ERROR | Llamada HTTP (4xx) | La API respondió con un error de cliente (e.g., 400, 401, 404). La solicitud no debe reintentarse. |\n| API_SERVER_ERROR | Llamada HTTP (5xx) | La API respondió con un error de servidor (e.g., 500, 503). La solicitud podría funcionar si se reintenta. |\n\n**Ejemplo de Salida de Error:**\n```json\n{\n \"status\": \"error\",\n \"error\": {\n \"code\": \"API_CLIENT_ERROR\",\n \"message\": \"La petición falló debido a un error en los datos enviados (ej. no autorizado o no encontrado).\",\n \"details\": {\n \"httpStatusCode\": 404,\n \"responseBody\": {\n \"error\": \"User not found\"\n }\n }\n }\n}\n```\n\n---\n\n## 🧩 Dependencias\n\n- Redis: Se requiere una conexión a Redis configurada para las funcionalidades de Caché y Circuit Breaker. \n- Sub-workflow `Manejador_CircuitBreaker`: Este workflow debe existir en la instancia de n8n, ya que es invocado para gestionar el estado del Circuit Breaker.\n","height":2848,"width":832,"color":7},"type":"n8n-nodes-base.stickyNote","position":[-960,-688],"typeVersion":1,"id":"e9f02341-8352-4ca2-b0fc-6bbb55047075","name":"Sticky Note"},{"parameters":{"method":"={{ $('Validar Entradas Requeridas').item.json.validatedInput.method }}","url":"={{ $('Validar Entradas Requeridas').item.json.validatedInput.url }}","sendQuery":true,"specifyQuery":"json","jsonQuery":"={{ JSON.stringify($('Preparar Solicitud Dinámica').item.json.finalQueryParams) }}","sendHeaders":true,"specifyHeaders":"json","jsonHeaders":"={{ JSON.stringify($('Preparar Solicitud Dinámica').item.json.finalHeaders) }}","sendBody":true,"specifyBody":"json","jsonBody":"={{ $('Preparar Solicitud Dinámica').item.json.finalBody }}","options":{"response":{"response":{"fullResponse":true}}}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[2016,192],"id":"8c624170-d71a-453e-9dfd-729d4b2af53f","name":"API: Llamada Dinámica","onError":"continueErrorOutput"},{"parameters":{"jsCode":"// Obtener la entrada validada del primer nodo\nconst validatedInput = $('Validar Entradas Requeridas').first().json.validatedInput;\n\n// Inicializar objetos limpios para la salida\nconst finalQueryParams = {};\nconst finalHeaders = {};\nconst finalBody = {}; // Inicializar un body vacío por defecto\n\n// --- Procesar Query Params Opcionales ---\nif (validatedInput.queryParams != null && typeof validatedInput.queryParams === 'object') {\n for (const key in validatedInput.queryParams) {\n const value = validatedInput.queryParams[key];\n if (value != null) {\n finalQueryParams[key] = value;\n }\n }\n}\n\n// --- Procesar Headers Opcionales ---\nif (validatedInput.headers != null && typeof validatedInput.headers === 'object') {\n for (const key in validatedInput.headers) {\n const value = validatedInput.headers[key];\n if (value != null) {\n finalHeaders[key] = value;\n }\n }\n}\n\n// --- Procesar Body Opcional ---\n// Solo si el body es un objeto y no es nulo, lo pasamos. Si no, se queda como objeto vacío {}.\nif (validatedInput.body != null && typeof validatedInput.body === 'object') {\n Object.assign(finalBody, validatedInput.body);\n}\n\n// Devolver un objeto que contiene los datos originales Y los objetos limpios\nreturn [{\n json: {\n ...validatedInput,\n finalQueryParams: finalQueryParams,\n finalHeaders: finalHeaders,\n finalBody: finalBody\n } \n}];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[1792,192],"id":"9535014d-5b70-4a07-a675-7d1285de6852","name":"Preparar Solicitud Dinámica"},{"parameters":{"jsCode":"// 1. Obtener el string JSON guardado en el caché\nconst cachedString = $('Redis: Get Cache').first().json.data;\n\n// 2. Convertir (parsear) ese string de vuelta a un objeto JavaScript\nconst cachedObject = JSON.parse(cachedString);\n\n// 3. Devolver el objeto parseado directamente como la nueva salida del nodo.\n// Esto asegura que la estructura sea idéntica a la del camino de \"cache miss\".\nreturn [{\n json: cachedObject\n}];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[3360,-288],"id":"f4937fdf-3ccb-437d-b7fb-190d2205c02c","name":"Procesar Salida de Caché"}],"pinData":{"When Executed by Another Workflow":[{"json":{"url":"https://tu-url","method":"POST","options":{"resilience":{"cacheKey":"getAfiliadoCore:CC-000","circuitBreakerKey":"getAfiliadoCore","cacheTTL":3600}},"headers":{"Content-Type":"application/json"},"body":{"sistema":"APPMP","nombreClave":"getAfiliadoCore","token":"xyz","jsonParametros":"{ \"tipo_id\": \"CC\", \"id\": \"000\" }"},"queryParams":null}}]},"connections":{"When Executed by Another Workflow":{"main":[[{"node":"Validar Entradas Requeridas","type":"main","index":0}]]},"Validar Entradas Requeridas":{"main":[[{"node":"¿Entrada Válida?","type":"main","index":0}]]},"¿Entrada Válida?":{"main":[[{"node":"Generar Clave de Caché","type":"main","index":0}],[{"node":"Formatear Error de Validación","type":"main","index":0}]]},"Formatear Error de Validación":{"main":[[{"node":"Unificar Salida","type":"main","index":0}]]},"Unificar Salida":{"main":[[{"node":"Salida Final","type":"main","index":0}]]},"Generar Clave de Caché":{"main":[[{"node":"Redis: Get Cache","type":"main","index":0}]]},"Redis: Get Cache":{"main":[[{"node":"IF: Cache Hit?","type":"main","index":0}],[{"node":"IF: Cache Hit?","type":"main","index":0}]]},"IF: Cache Hit?":{"main":[[{"node":"Procesar Salida de Caché","type":"main","index":0}],[{"node":"CB: Verificar Estado","type":"main","index":0}]]},"CB: Verificar Estado":{"main":[[{"node":"IF: ¿Circuito Permite Proceder?","type":"main","index":0}]]},"IF: ¿Circuito Permite Proceder?":{"main":[[{"node":"Preparar Solicitud Dinámica","type":"main","index":0}],[{"node":"Formatear Error de Circuito Abierto","type":"main","index":0}]]},"Formatear Error de Circuito Abierto":{"main":[[{"node":"Unificar Salida","type":"main","index":0}]]},"Redis: Set Cache":{"main":[[{"node":"CB: Reportar Éxito","type":"main","index":0}]]},"CB: Reportar Éxito":{"main":[[{"node":"Preparar Salida Final de Éxito","type":"main","index":0}]]},"Switch: Resultado de API":{"main":[[{"node":"Formatear Salida de Éxito","type":"main","index":0}],[{"node":"CB: Reportar Fallo","type":"main","index":0}],[{"node":"Formatear Error de Cliente","type":"main","index":0}]]},"Formatear Salida de Éxito":{"main":[[{"node":"IF: ¿Caché Habilitado?","type":"main","index":0}]]},"IF: ¿Caché Habilitado?":{"main":[[{"node":"Redis: Set Cache","type":"main","index":0}],[{"node":"Preparar Salida Final de Éxito","type":"main","index":0}]]},"Preparar Salida Final de Éxito":{"main":[[{"node":"Unificar Salida","type":"main","index":0}]]},"CB: Reportar Fallo":{"main":[[{"node":"Formatear Error de Servidor","type":"main","index":0}]]},"Formatear Error de Servidor":{"main":[[{"node":"Unificar Salida","type":"main","index":0}]]},"Formatear Error de Cliente":{"main":[[{"node":"Unificar Salida","type":"main","index":0}]]},"API: Llamada Dinámica":{"main":[[{"node":"Switch: Resultado de API","type":"main","index":0}],[{"node":"Switch: Resultado de API","type":"main","index":0}]]},"Preparar Solicitud Dinámica":{"main":[[{"node":"API: Llamada Dinámica","type":"main","index":0}]]},"Procesar Salida de Caché":{"main":[[{"node":"Unificar Salida","type":"main","index":0}]]}},"active":false,"settings":{"executionOrder":"v1"},"versionId":"77ae3010-3ed4-409a-9d27-93ee9ed16a0c","meta":{"instanceId":"c44159ad241c0d6d40c4f5053f46c53c8d2c806950fcbaf71270c9d0c4993311"},"id":"o4o1rEmGsKQktiHT","tags":[]}
El Ejemplo Práctico 🏥
{"name":"Herramienta_DirectorioMedico_BuscarServicios_v1","nodes":[{"parameters":{"workflowInputs":{"values":[{"name":"ciudad"},{"name":"desAgrupa"}]}},"type":"n8n-nodes-base.executeWorkflowTrigger","typeVersion":1.1,"position":[480,720],"id":"7c57493c-f8d4-4579-996f-0dc4d8095609","name":"Disparador: Recibir Datos"},{"parameters":{"method":"POST","url":"=https://oficinavirtualmp.coomeva.com.co/saludmp-ws/jax-rs/saludmp-appFrontController","authentication":"genericCredentialType","genericAuthType":"httpCustomAuth","sendBody":true,"bodyParameters":{"parameters":[{"name":"sistema","value":"={{ $json.sistema }}"},{"name":"nombreClave","value":"={{ $json.nombreClave }}"},{"name":"jsonParametros","value":"={{ $json.jsonParametros }}"}]},"options":{"response":{"response":{}}}},"type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[480,304],"id":"379d9102-db86-41f3-a868-4174e44a8957","name":"API: GetAgrupadores1","credentials":{"httpCustomAuth":{"id":"Giu2BvZ1oYBtcOEr","name":"DirectorioMedico Custom Auth"}},"onError":"continueErrorOutput"},{"parameters":{"mode":"raw","jsonOutput":"={\n \"status\": \"error\",\n \"error\": {\n \"code\": \"VALIDATION_ERROR\",\n \"message\": \"{{ $('Validar y Estructurar Entradas').item.json.errors.join(' ') }}\",\n \"details\": {\n \"validationErrors\": {{ $('Validar y Estructurar Entradas').item.json.errors }}\n }\n }\n}","options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[1824,912],"id":"aadeb5a5-f9b5-443a-ae64-03f336c8a86d","name":"Formatear Error de Validación"},{"parameters":{"jsCode":"// Paso 1: Definir el Contrato de la Herramienta\nconst requiredFields = ['ciudad', 'desAgrupa'];\n\n// Paso 2: Recoger los datos de entrada\nconst input = $input.first().json;\nconst errors = [];\n\n// Paso 3: Ejecutar las Validaciones\nrequiredFields.forEach(field => {\n // Validamos que el campo exista (no sea nulo ni indefinido).\n // Permitimos explícitamente que 'desAgrupa' sea una cadena vacía.\n if (input[field] === undefined || input[field] === null) {\n errors.push(`Error de Validación: El parámetro requerido '${field}' está ausente.`);\n }\n});\n\n// Paso 4: Devolver un resultado estructurado\nreturn [{\n json: {\n isValid: errors.length === 0,\n errors: errors,\n validatedInput: input\n }\n}];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[704,720],"id":"cd6519a9-c5d9-4489-a718-59f0f359d43b","name":"Validar y Estructurar Entradas"},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"3e587118-8ee7-41ec-b12d-ead5f21f9798","leftValue":"={{ $json.isValid }}","rightValue":"","operator":{"type":"boolean","operation":"true","singleValue":true}}],"combinator":"and"},"options":{}},"type":"n8n-nodes-base.if","typeVersion":2.2,"position":[928,720],"id":"3e2a1824-133d-4395-9062-a712da016c31","name":"¿Entrada Válida?"},{"parameters":{},"type":"n8n-nodes-base.merge","typeVersion":3.2,"position":[2048,720],"id":"3dec828f-d15d-44fd-9113-3472f89b9832","name":"Unificar Salida"},{"parameters":{"mode":"raw","jsonOutput":"={\n \"url\": \"https://tu-url\",\n \"method\": \"POST\",\n \"headers\": {\n \"Content-Type\": \"application/json\"\n },\n \"body\": {\n \"sistema\": \"APPMP\",\n \"token\": \"xyz\",\n \"nombreClave\": \"getAgrupadores\",\n \"jsonParametros\": \"{\\\"desAgrupa\\\": \\\"{{ $('Validar y Estructurar Entradas').item.json.validatedInput.desAgrupa }}\\\",\\\"tipoAgrupa\\\": \\\"AMB\\\", \\\"ciudad\\\": \\\"{{ $('Validar y Estructurar Entradas').item.json.validatedInput.ciudad }}\\\"}\"\n },\n \"queryParams\": null,\n \"options\": {\n \"resilience\": {\n \"cacheKey\": \"buscarServicios:{{ $('Validar y Estructurar Entradas').item.json.validatedInput.ciudad }}:{{ $('Validar y Estructurar Entradas').item.json.validatedInput.desAgrupa }}\",\n \"circuitBreakerKey\": \"buscarServicios\",\n \"cacheTTL\": 3600\n }\n }\n}","options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[1152,624],"id":"3db96db1-c683-4d34-a183-c44c4de86882","name":"Ensamblar DTO para la Utilidad"},{"parameters":{"workflowId":{"__rl":true,"value":"o4o1rEmGsKQktiHT","mode":"list","cachedResultName":"TIL-Resilient-API-Executor-v1"},"workflowInputs":{"mappingMode":"defineBelow","value":{"url":"={{ $('Ensamblar DTO para la Utilidad').item.json.url }}","headers":"={{ $('Ensamblar DTO para la Utilidad').item.json.headers }}","method":"={{ $('Ensamblar DTO para la Utilidad').item.json.method }}","options":"={{ $('Ensamblar DTO para la Utilidad').item.json.options }}","body":"={{ $('Ensamblar DTO para la Utilidad').item.json.body }}","queryParams":"={{ $('Ensamblar DTO para la Utilidad').item.json.queryParams }}"},"matchingColumns":[],"schema":[{"id":"url","displayName":"url","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"string","removed":false},{"id":"method","displayName":"method","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"string","removed":false},{"id":"options","displayName":"options","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"object","removed":false},{"id":"headers","displayName":"headers","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"object","removed":false},{"id":"body","displayName":"body","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"object","removed":false},{"id":"queryParams","displayName":"queryParams","required":false,"defaultMatch":false,"display":true,"canBeUsedToMatch":true,"type":"object","removed":false}],"attemptToConvertTypes":false,"convertFieldsToString":true},"options":{"waitForSubWorkflow":true}},"type":"n8n-nodes-base.executeWorkflow","typeVersion":1.2,"position":[1376,624],"id":"3e0cd4cd-2f20-4e17-b886-2e5990290202","name":"Llamar a la Utilidad Resiliente"},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"c5e2cff1-bfb3-4053-ae6a-1ea5d1113451","leftValue":"={{ $('Llamar a la Utilidad Resiliente').item.json.status }}","rightValue":"success","operator":{"type":"string","operation":"equals","name":"filter.operator.equals"}}],"combinator":"and"},"options":{}},"type":"n8n-nodes-base.if","typeVersion":2.2,"position":[1600,624],"id":"e548ba82-8eb9-4aff-aefc-aee5c4dc9518","name":"IF: ¿Utilidad Exitosa?"},{"parameters":{"mode":"raw","jsonOutput":"=={\n \"status\": \"error\",\n \"error\": {{ $('Llamar a la Utilidad Resiliente').item.json.error }}\n}","options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[1824,720],"id":"e44a69e9-b4fa-4bcb-9e70-81b2ae42bf89","name":"Formatear Salida Final de Error"},{"parameters":{},"type":"n8n-nodes-base.noOp","typeVersion":1,"position":[2272,720],"id":"c58ab81f-5345-48e3-a3d7-67bd5671e684","name":"Salida Final"},{"parameters":{"jsCode":"// --- CÓDIGO FINAL CON REPARACIÓN AUTOMÁTICA DE JSON ---\n\n/**\n * Esta función intenta reparar un string JSON malformado de la API.\n * Específicamente, busca todos los campos \"descripcion\" y escapa cualquier\n * comilla doble (\") que encuentre dentro de su valor de texto.\n * @param {string} str - El string JSON potencialmente dañado.\n * @returns {string} - El string JSON reparado.\n */\nfunction repairJsonString(str) {\n // Dividimos el string usando el inicio del campo \"descripcion\" como separador.\n // Esto nos permite aislar el contenido de cada descripción.\n const parts = str.split(',\"descripcion\":\"');\n if (parts.length <= 1) {\n return str; // No se encontraron descripciones, no hay nada que reparar.\n }\n\n // La primera parte del string (antes de la primera descripción) no se modifica.\n let repairedString = parts[0];\n\n // Iteramos sobre cada fragmento que contiene una descripción.\n for (let i = 1; i < parts.length; i++) {\n let chunk = parts[i];\n \n // El final de un campo \"descripcion\" casi siempre es '\",\"nivel_auditoria\":'.\n // Usamos este patrón para saber dónde termina el texto de la descripción.\n const endOfDescPattern = '\",\"nivel_auditoria\":';\n const endOfDescIndex = chunk.indexOf(endOfDescPattern);\n\n if (endOfDescIndex !== -1) {\n let descriptionText = chunk.substring(0, endOfDescIndex);\n const restOfChunk = chunk.substring(endOfDescIndex);\n \n // --- LA REPARACIÓN ---\n // Reemplazamos todas las comillas dobles por comillas escapadas (e.g., \" -> \\\")\n // dentro del texto de la descripción.\n descriptionText = descriptionText.replace(/\"/g, '\\\\\"');\n \n // Reconstruimos el string con la descripción ya reparada.\n repairedString += ',\"descripcion\":\"' + descriptionText + restOfChunk;\n } else {\n // Si no encontramos el patrón final, no podemos reparar con seguridad este fragmento.\n // Lo añadimos tal cual para no perder datos.\n repairedString += ',\"descripcion\":\"' + chunk;\n }\n }\n return repairedString;\n}\n\n\n// --- LÓGICA PRINCIPAL DEL NODO ---\n\nconst responseItem = $('Llamar a la Utilidad Resiliente').first();\nlet finalData = [];\n\nif (responseItem && responseItem.json && responseItem.json.data && responseItem.json.data.body) {\n const body = responseItem.json.data.body;\n\n // CASO A: La respuesta ya viene parseada y con datos en 'jsonRespuesta'.\n if (Array.isArray(body.jsonRespuesta) && body.jsonRespuesta.length > 0) {\n finalData = body.jsonRespuesta;\n }\n // CASO B: Los datos vienen como un string en 'respuesta'.\n else if (typeof body.respuesta === 'string' && body.respuesta.trim().startsWith('[')) {\n \n // ¡Paso Clave! Primero reparamos el string de la API.\n const repairedJson = repairJsonString(body.respuesta);\n\n try {\n // Ahora parseamos el string ya reparado.\n const parsedData = JSON.parse(repairedJson);\n if (Array.isArray(parsedData)) {\n finalData = parsedData;\n }\n } catch (e) {\n console.error(\"El parseo falló incluso después de intentar la reparación. Error:\", e.message);\n // Si todo falla, devolvemos un array vacío para no detener el workflow.\n }\n }\n}\n\n// Devolver la salida final estandarizada y correcta.\nreturn [{\n json: {\n status: 'success',\n data: finalData\n }\n}];"},"type":"n8n-nodes-base.code","typeVersion":2,"position":[1824,528],"id":"fa21c269-9782-4e8b-a127-65e6300480ef","name":"Formatear Salida Inteligente"}],"pinData":{"Disparador: Recibir Datos":[{"json":{"ciudad":"76001","desAgrupa":""}}]},"connections":{"Disparador: Recibir Datos":{"main":[[{"node":"Validar y Estructurar Entradas","type":"main","index":0}]]},"Formatear Error de Validación":{"main":[[{"node":"Unificar Salida","type":"main","index":0}]]},"Validar y Estructurar Entradas":{"main":[[{"node":"¿Entrada Válida?","type":"main","index":0}]]},"¿Entrada Válida?":{"main":[[{"node":"Ensamblar DTO para la Utilidad","type":"main","index":0}],[{"node":"Formatear Error de Validación","type":"main","index":0}]]},"Unificar Salida":{"main":[[{"node":"Salida Final","type":"main","index":0}]]},"Ensamblar DTO para la Utilidad":{"main":[[{"node":"Llamar a la Utilidad Resiliente","type":"main","index":0}]]},"Llamar a la Utilidad Resiliente":{"main":[[{"node":"IF: ¿Utilidad Exitosa?","type":"main","index":0}]]},"IF: ¿Utilidad Exitosa?":{"main":[[{"node":"Formatear Salida Inteligente","type":"main","index":0}],[{"node":"Formatear Salida Final de Error","type":"main","index":0}]]},"Formatear Salida Final de Error":{"main":[[{"node":"Unificar Salida","type":"main","index":0}]]},"Formatear Salida Inteligente":{"main":[[{"node":"Unificar Salida","type":"main","index":0}]]}},"active":false,"settings":{"executionOrder":"v1"},"versionId":"55f6d133-8f58-4bcf-a46e-a612e3c2f3ad","meta":{"instanceId":"c44159ad241c0d6d40c4f5053f46c53c8d2c806950fcbaf71270c9d0c4993311"},"id":"4aB1w4iocxhHRNE4","tags":[]}
EMPLEONATOR! Recibe en tu celular las vacantes tech que encajan con tu perfil Y APLICA CON UN CLIC DESDE TELEGRAM!
Este flujo en n8n busca vacantes en Google Jobs para developers
cuando las encuentra, te las envía directo a Telegram.
<u>Aplicas directamente desde telegram con un clic!</u>
En la foto se ve que funcionó, estoy muy feliz!!!!
👉 JSON del flujo
Lo pensé porque, como muchos en Platzi, hacemos un esfuerzo para invertir en nuestra educación buscando mejorar nuestra situación laboral. ¿Qué mejor que recibir los frutos de nuestra inversión a diario en nuestro celular sin buscarlas manualmente?
Cybersecurity BlueTeam - Monitoreo de correos expuestos en filtraciones
¿Tus datos ya están expuestos en Internet?
¿Alguna vez te has preguntado si tu correo electrónico o el de tu empresa apareció en una filtración de datos?
¿Si tus contraseñas están circulando en foros o bases de datos robadas?
¿Si te han hackeado sin que lo sepas?
La realidad es que las filtraciones ocurren todos los días y la mayoría de las personas y empresas se enteran demasiado tarde.
Por eso armé este flujo en n8n que hace justo lo que todos deberíamos tener:
Una automatización que revisa periódicamente si tus correos están comprometidos y te avisa antes de que sea demasiado tarde.
🔐 ¿Qué hace?
Lee una lista de correos desde Google Sheets (puede ser tu equipo completo).
Consulta la API de Leak-Lookup para ver si esos correos aparecen en filtraciones públicas.
Si encuentra coincidencias:
1- Te manda una alerta inmediata a Slack (o Telegram/Email).
2- Guarda todo en un registro histórico para auditoría.
Todo de forma automática y programada (cada 12 horas o la frecuencia que elijas).
🛠️¿Qué necesitas para usarlo?
Una instancia de n8n (local o en la nube).
Una cuenta en Leak-Lookup (API Key gratuita disponible).
Una hoja de Google Sheets con los correos a monitorear.
(Opcional) Integración con Slack para recibir alertas en tiempo real.
El JSON es el siguiente:
{"name":"BreachAlert","nodes":[{"parameters":{"triggerTimes":{"item":[{"mode":"everyX","value":12}]}},"id":"f7a90569-4c01-4a87-b5fa-0854c4edace7","name":"Cron (cada 12h)","type":"n8n-nodes-base.cron","typeVersion":1,"position":[-1536,1296]},{"parameters":{"values":{"string":[{"name":"SPREADSHEET_ID","value":"Tu id de google sheet"},{"name":"LEAKLOOKUP_KEY","value":"tu id de leak lookup"},{"name":"SLACK_CHANNEL","value":"tu canal de Slack"}]},"options":{"dotNotation":true}},"id":"2b6cf48d-4491-4570-9425-5a338c1f427e","name":"Set - Config","type":"n8n-nodes-base.set","typeVersion":2,"position":[-1312,1296]},{"parameters":{"documentId":{"__rl":true,"value":"tu id de google sheet","mode":"list","cachedResultName":"Monitoreo de Exposición de Credenciales","cachedResultUrl":"https://docs.google.com/spreadsheets/d/tu id de google sheet/edit?usp=drivesdk"},"sheetName":{"__rl":true,"value":"gid=0","mode":"list","cachedResultName":"Inputs","cachedResultUrl":"https://docs.google.com/spreadsheets/d/tu id de google sheet/edit#gid=0"},"options":{}},"id":"f35359a9-c1a4-4cb4-a978-1430461b81ab","name":"Google Sheets → Emails","type":"n8n-nodes-base.googleSheets","typeVersion":4,"position":[-1088,1296],"credentials":{"googleSheetsOAuth2Api":{"id":"id credenciales google","name":"Google Sheets account"}}},{"parameters":{"keepOnlySet":true,"values":{"string":[{"name":"email","value":"={{$json[\"0\"] || $json[\"Email\"] || $json[\"email\"]}}"}]},"options":{}},"id":"76dff6aa-6a7d-4321-9816-4fbc347449c2","name":"Normalizar email","type":"n8n-nodes-base.set","typeVersion":2,"position":[-864,1296]},{"parameters":{"batchSize":1,"options":{}},"id":"058a12ed-e586-405e-a372-c7efb622fd3a","name":"Procesar por email","type":"n8n-nodes-base.splitInBatches","typeVersion":1,"position":[-640,1296]},{"parameters":{"method":"POST","url":"https://leak-lookup.com/api/search","sendHeaders":true,"headerParameters":{"parameters":[{"name":"User-Agent","value":"n8n-breach-monitor/1.0"}]},"sendBody":true,"contentType":"form-urlencoded","bodyParameters":{"parameters":[{"name":"key","value":"={{ $('Set - Config').item.json.LEAKLOOKUP_KEY }}"},{"name":"type","value":"email_address"},{"name":"query","value":"={{ $('Normalizar email').item.json.email }}"}]},"options":{"timeout":30000}},"id":"4e3a96e3-5285-4ee3-a5a0-4add8d7827be","name":"Leak-Lookup → Search","type":"n8n-nodes-base.httpRequest","typeVersion":4,"position":[-416,1232]},{"parameters":{"keepOnlySet":true,"values":{"number":[{"name":"hitCount","value":"={{ ($json && String($json.error).toLowerCase() === 'false' && typeof $json.message === 'object')\n ? Object.keys($json.message).length\n : 0 }}\n"}],"string":[{"name":"email","value":"={{$item(0).$node[\"Normalizar email\"].json.email}}\n"},{"name":"breaches","value":"={{ ($json && String($json.error).toLowerCase() === 'false' && typeof $json.message === 'object')\n ? Object.keys($json.message).join(', ')\n : '' }}\n"}],"boolean":[{"name":"isOk","value":"={{ String($json.error).toLowerCase() === 'false' && typeof $json.message === 'object' }}\n"}]},"options":{"dotNotation":true}},"id":"8f042916-46aa-4cce-bddd-3a7153b0a0bf","name":"Set - Calc Hits","type":"n8n-nodes-base.set","typeVersion":2,"position":[-192,1232]},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":1},"conditions":[{"id":"ef640b80-3dc8-4c6c-8000-e1eccff4a4fd","leftValue":"={{ $json.isOk }}","rightValue":"","operator":{"type":"boolean","operation":"true","singleValue":true}},{"id":"21c9a511-5696-4176-b30f-eee63a7998b9","leftValue":"={{ $json.hitCount }}","rightValue":0,"operator":{"type":"number","operation":"gt"}}],"combinator":"and"},"options":{}},"id":"3d67e05b-7236-4e12-a9db-b7aa6ca1e456","name":"¿Hay coincidencias?","type":"n8n-nodes-base.if","typeVersion":2,"position":[32,1232]},{"parameters":{"channel":"={{$item(0).$node[\"Set - Config\"].json.SLACK_CHANNEL}}","text":"=🚨 Leak-Lookup: {{$json.hitCount}} coincidencia(s)\n• Email: {{$json.email}}\n• Brechas: {{$json.breaches || 'N/D'}}\n","otherOptions":{},"attachments":[]},"id":"7b5b8522-18ed-4f27-88ba-7dc340ef3855","name":"Slack → alerta (opcional)","type":"n8n-nodes-base.slack","typeVersion":1,"position":[256,1152],"credentials":{"slackApi":{"id":"credencial slack","name":"Slack account"}}},{"parameters":{"operation":"append","documentId":{"__rl":true,"value":"id documento google sheet","mode":"list","cachedResultName":"Monitoreo de Exposición de Credenciales","cachedResultUrl":"https://docs.google.com/spreadsheets/d/is documento google sheet/edit?usp=drivesdk"},"sheetName":{"__rl":true,"value":937274523,"mode":"list","cachedResultName":"Logs","cachedResultUrl":"https://docs.google.com/spreadsheets/d/id documento google sheet/edit#gid=937274523"},"columns":{"mappingMode":"defineBelow","value":{"ok":"={{ $json.ok }}","channel":"={{ $json.channel }}","ts":"={{ $json.ts }}","message":"={{ $json.message }}","email":"={{ $('Normalizar email').item.json.email }}"},"matchingColumns":[],"schema":[{"id":"timestamp","displayName":"timestamp","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true,"removed":true},{"id":"email","displayName":"email","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true,"removed":false},{"id":"source","displayName":"source","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true,"removed":true},{"id":"breachDate","displayName":"breachDate","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true,"removed":true},{"id":"dataClasses","displayName":"dataClasses","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true,"removed":true},{"id":"severity","displayName":"severity","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true,"removed":true},{"id":"ok","displayName":"ok","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true,"removed":false},{"id":"channel","displayName":"channel","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true,"removed":false},{"id":"ts","displayName":"ts","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true,"removed":false},{"id":"message","displayName":"message","required":false,"defaultMatch":false,"display":true,"type":"string","canBeUsedToMatch":true,"removed":false}],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{}},"id":"6a06d946-3617-4de9-93c7-0facaef1e8f6","name":"Google Sheets → Log","type":"n8n-nodes-base.googleSheets","typeVersion":4,"position":[480,1152],"credentials":{"googleSheetsOAuth2Api":{"id":"cuenta google","name":"Google Sheets account"}}},{"parameters":{"options":{}},"id":"95691425-0f07-43db-b9cb-20071fff7512","name":"Siguiente email","type":"n8n-nodes-base.splitInBatches","typeVersion":1,"position":[928,1296]},{"parameters":{"amount":5,"unit":"seconds"},"id":"83a3f8a4-47d9-4366-ab18-9d29589f62b2","name":"Esperar 5 segundos","type":"n8n-nodes-base.wait","typeVersion":1,"position":[720,1248],"webhookId":"fa37c771-a38d-488a-8e0a-174f16fdcbf2"}],"pinData":{},"connections":{"Cron (cada 12h)":{"main":[[{"node":"Set - Config","type":"main","index":0}]]},"Set - Config":{"main":[[{"node":"Google Sheets → Emails","type":"main","index":0}]]},"Google Sheets → Emails":{"main":[[{"node":"Normalizar email","type":"main","index":0}]]},"Normalizar email":{"main":[[{"node":"Procesar por email","type":"main","index":0}]]},"Procesar por email":{"main":[[{"node":"Leak-Lookup → Search","type":"main","index":0}]]},"Leak-Lookup → Search":{"main":[[{"node":"Set - Calc Hits","type":"main","index":0}]]},"Set - Calc Hits":{"main":[[{"node":"¿Hay coincidencias?","type":"main","index":0}]]},"¿Hay coincidencias?":{"main":[[{"node":"Slack → alerta (opcional)","type":"main","index":0}],[{"node":"Esperar 5 segundos","type":"main","index":0}]]},"Slack → alerta (opcional)":{"main":[[{"node":"Google Sheets → Log","type":"main","index":0}]]},"Google Sheets → Log":{"main":[[{"node":"Esperar 5 segundos","type":"main","index":0}]]},"Siguiente email":{"main":[[{"node":"Procesar por email","type":"main","index":0}]]},"Esperar 5 segundos":{"main":[[{"node":"Siguiente email","type":"main","index":0}]]}},"active":false,"settings":{"executionOrder":"v1"},"versionId":"d8faab7d-cad3-4ea1-a0ae-07c1cc4f1e72","meta":{"templateCredsSetupCompleted":true,"instanceId":"5efb84d3bc8cfffa075227a766fcfbc6afbac26c5670c1cba87e6cc2efb6ad6b"},"id":"GzjRyti5ybgeG8lm","tags":[{"createdAt":"2025-09-12T04:00:47.912Z","updatedAt":"2025-09-12T04:00:47.912Z","id":"oDTFPSeLo4vlmiMv","name":"BlueTeam"}]}
Coach Nutricional Automatizado con IA y n8n
Diseñé un flujo en n8n que actúa como un coach nutricional inteligente: cada vez que registro lo que como, el sistema procesa la información, guarda los datos en Google Sheets y me muestra en tiempo real cuántas calorías me faltan o si ya me pasé del objetivo diario. Además, cada semana genera un reporte automático con mi consumo total de calorías, ayudándome a mantener un déficit calórico controlado para alcanzar mis metas de peso.
Acá el Json 🤙🏽
Bro este es buenisimo y tiene mucho potencial! Mil gracias!
Soy un poco nuevo en esto, me podrias apoyar con mas detalles para probarlo? de gymrat a gymrat :)
N8N ha cambiado mi vida quiero ganar Con mi último flujo de trabajo en n8n, desarrollé una solución para un Centro de Diagnóstico Automotor (CDA) que automatiza el proceso completo de agendamiento y validación de citas con código QR.
El flujo permite que los clientes reserven en línea y el sistema:
Verifica automáticamente la disponibilidad de horarios (con un margen de 30 minutos entre citas).
Genera un ID único y un token QR para cada cita.
Envía un correo de confirmación al cliente con todos los detalles y el QR adjunto.
Registra la cita en Google Sheets para mantener un control centralizado.
Cuando el cliente llega al CDA, basta con escanear el QR para validar la cita. El sistema:
Busca la información en Google Sheets.
Confirma si la cita está vigente.
Actualiza el estado a “Atendida” en tiempo real.
Devuelve una respuesta clara al personal del CDA (válida o rechazada).
Con esta automatización, logramos un proceso más eficiente, seguro y sin papeles, mejorando la experiencia del cliente y la gestión interna del CDA.
"text": "Tu cita para revisión técnico-mecánica ha sido agendada para {{$json[\"Fecha\"]}} a las {{$json[\"Hora\"]}}.\n\nPresenta este código QR al llegar al CDA para validar tu cita:\n\n[imagen QR adjunta]\n\nCDA Ejemplo - Centro de Diagnóstico Automotor",
"attachmentsUi": {
"attachmentsBinary": [{ "property": "data" }]
}
},
"id": "8",
"name": "Enviar Email con QR",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2,
"position": [1750, 150],
"credentials": {
"smtp": "TU_CREDENCIAL_GMAIL"
}
},
{
"parameters": {
"path": "validar-cita",
"responseMode": "lastNode",
"options": {}
},
"id": "9",
"name": "Webhook Validar Cita",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [250, 650]
},
{
"parameters": {
"operation": "lookup",
"sheetId": "TU_ID_DE_SHEET",
"range": "CDA",
"lookupColumn": "Token QR",
"lookupValue": "={{$json[\"id\"]}}"
},
"id": "10",
"name": "Buscar Cita por Token",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 3,
"position": [500, 650],
"credentials": {
"googleApi": "TU_CREDENCIAL_GOOGLE"
}
},
{
"parameters": {
"functionCode": "if ($json[\"Estado\"] === \"Vigente\") {\n return [{ json: { mensaje: ✅ Cita válida para ${$json\[\\"Nombre\\"]}, ${$json\[\\"Fecha\\"]} ${$json\[\\"Hora\\"]}, estado: \"Atendida\" } }];\n} else {\n return [{ json: { mensaje: \"❌ Cita no válida o ya usada\", estado: $json[\"Estado\"] } }];\n}"
},
"id": "11",
"name": "Validar Estado",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [750, 650]
},
{
"parameters": {
"operation": "update",
"sheetId": "TU_ID_DE_SHEET",
"range": "CDA",
"key": "Token QR",
"keyValue": "={{$json[\"id\"]}}",
"options": {
"updateColumns": ["Estado"]
},
"values": "={{[\"Atendida\"]}}"
},
"id": "12",
"name": "Marcar como Atendida",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 3,
"position": [950, 650],
"credentials": {
"googleApi": "TU_CREDENCIAL_GOOGLE"
}
},
{
"parameters": {
"responseBody": "={{$json[\"mensaje\"]}}"
},
"id": "13",
"name": "Responder Validación",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [1150, 650]
}
],
"connections": {
"Webhook Nueva Cita": {
"main": [[{ "node": "Buscar Citas Mismo Día", "type": "main", "index": 0 }]]
Como estudiante del SENA y de la universidad, mi día a día estaba lleno de ideas, tareas, eventos y enlaces importantes… pero todo estaba regado entre WhatsApp, Google Calendar, notas sueltas y hojas de cálculo. Al final, perdía tiempo buscando, olvidaba cosas importantes y no tenía claridad sobre mis pendientes.
⚙️ ¿Cómo lo resolví?
Creé “Mi Segundo Cerebro”, un workflow en n8n que convierte WhatsApp en mi asistente personal de productividad. Con comandos simples como:
/pendiente → Agrega tareas con prioridad.
/calendario → Crea eventos en Google Calendar.
/nota → Guarda ideas rápidas.
/link → Guarda enlaces importantes.
/resumen → Muestra pendientes del día.
/completar → Marca tareas como hechas.
Todo se guarda automáticamente en Google Sheets, categorizado por tipo, prioridad y estado. Además, cada mañana recibo un resumen por WhatsApp con mis pendientes activos. ¡Ya no se me escapa nada!
🧪 ¿Cómo lo probé?
Para testear el flujo sin depender de una API de WhatsApp real, creé un webhook en HTML súper sencillo que simula el envío de mensajes. Lo descargué localmente y lo usé como panel de pruebas para verificar que cada comando funcionara correctamente. Esto me permitió validar el flujo de punta a punta sin complicaciones técnicas.
🚀 ¿Por qué es especial?
Usa herramientas que ya tengo: WhatsApp, Google Sheets y Calendar.
Automatiza mi organización diaria.
Me da claridad y control sin apps nuevas.
Ideal para estudiantes, freelancers o cualquier persona que quiera organizar su vida sin complicarse.
MI NOMBRE ES EMMANUEL TENGO 18 AÑOS Y ME ENCANTO HACER ESTE RETO Y APRENDER , QUE ES LO MAS IMPORTANTE AGRADEZCO TU APOYO .
ESPERO LES HAYA GUSTADO , PARA USARLO SIMPLEMENTE COPIEN EL JSON , LUEGO CREAN UN ARCHIVO . JSON Y LO SUBEN A N8N , Y LUEGO INGRESAN A LOS NODOS CON SUS CREDENCIALES , UN ABRAZO BENDICIONES .
interesante Emmanuel , Tengo fijo que usar mi API de whatsapp , o puedo usar evolution Api tambien ??
que chimba de flujo emmanuel , un saludo
📂 Monitoreo de carpeta de Google Drive con alertas automáticas
Objetivo: Enviar notificaciones automáticas por Telegram 📲 y Gmail ✉️ cuando se agrega un archivo nuevo en una carpeta de Google Drive
Problema que resuelve: Permite mantener control sobre los archivos compartidos y recibir notificaciones inmediatas sin revisar la carpeta manualmente
Cómo el flujo resuelve ese problema: Detecta archivos nuevos, los descarga, identifica su tipo (PDF, Word, Excel, imagen, etc.) y envía notificaciones automáticas por Telegram (link para visualizar) y Gmail (Archivo para visualizar y descargar).
Cómo funciona:
Detecta archivos nuevos en la carpeta de Google Drive
Descarga los archivos para poder procesarlos
Identifica el tipo de archivo (PDF, Word, Excel, imagen, etc.)
Envía notificaciones personalizadas
Notificaciones:
Telegram: Mensaje rápido con tipo de archivo, propietario y un link para visualizar
Gmail: Correo con información del propietario y un link para visualizar y descargar el archivo
Beneficio para la comunidad: Automatiza la gestión de archivos compartidos, facilita el seguimiento y es fácilmente reutilizable por otros usuarios adaptando sus propias carpetas de Drive y cuentas de notificación
Cómo otras personas pueden implementarlo: Solo necesitan configurar sus credenciales de Drive, Gmail, Telegram y su carpeta de Drive a monitorear.
Link de repositorio:
🤓💯
👏🏻👏🏻
Esta flujo apoya las ventas de Energía Renovable 🌱⚡
Facilita la prospección de clientes en energía renovable al automatizar tareas clave: captura datos de prospectos, los organiza en Google Sheets, un Agente de IA los califica y finalmente envía un correo personalizado con enlace directo para agendar reuniones.
De esta forma elimina el trabajo manual, ahorra tiempo y asegura que los equipos comerciales se enfoquen solo en los prospectos con mayor potencial, creando un flujo de ventas simple, eficiente y sostenible. 🚀🌍
Acá pueden encontrar el flujo Json:
EnergíaRenovable con el apoyo de prospección del siguiente flujo en n8n
Automate your learning con n8n: Notion → Flashcards (IA) + SRS en Telegram
¿Qué hace?
Convierte apuntes de inglés (Platzi u otras fuentes) guardados en Notion en flashcards automáticamente y te envía un recordatorio por Telegram con el resumen por tipo (Vocabulary/Grammar), usando aprendizaje espaciado (3/7/14 días).
¿Cómo lo resuelve?
Flujo 1 – Generador de Flashcards Notion: normaliza notas (Grammar/Vocabulary), genera tarjetas con IA y las guarda en la DB “Flashcards” (Stage 0, Next Review).
Flujo 2 – Recordatorios Telegram (SRS): a las 21:00 (America/La_Paz) filtra las tarjetas “que tocan hoy”, actualiza Stage/fechas (0→1: +3d, 1→2: +7d, 2→3: +14d, 3→Completed) y envía un único mensaje con el total por tipo.
Cómo implementarlo (rápido)
Crea 2 DB en Notion: Notas (Ready for AI, Processed) y Flashcards (Front, Back, Type, Stage, Next Review, Completed, Tags).
Importa los 2 JSON a n8n y configura credenciales (Notion, Telegram).
Ajusta tus databaseId y chatId.
Ejecuta Generador → ejecuta Recordatorio → activa el Cron (21:00 La Paz).
JSON (acceso público)
Generador:
Recordatorios (SRS):
Guía completa (README, paso a paso, troubleshooting) en GitHub
👉
¿Te sirve?
Si te ayuda a estudiar/enseñar, ¡apóyame con tu voto! 🙌
Si lo adaptaste a otra materia, cuéntame cómo lo reutilizaste.
Buena idea... me la robo. 👌
Qué pasa si mis notas en Notion no están bien clasificadas entre “Vocabulary” y “Grammar”? La IA igual puede organizarlas en flashcards o necesito normalizarlas antes?
Problema
La carga de facturas suele ser manual, repetitiva y propensa a errores, lo que retrasa la gestión administrativa.
Solución
Este flujo en n8n automatiza el proceso: Detecta automáticamente cuando se recibe una factura, extrae datos de facturas en PDF con IA, los transforma en JSON estructurado mediante un nodo de código y los registra en Airtable (u otro sistema).
Valor agregado
Reduce errores, acelera el registro y escala fácilmente a grandes volúmenes de facturas. Además, permite integraciones con otros sistemas contables y validaciones automáticas, convirtiéndose en una herramienta práctica y confiable para empresas.
Acceso al json:
Al implementar este proceso en un caso real, surgió la necesidad de aplicar mejoras. Aquí una versión más completa del flujo:
-Si el PDF tiene varias hojas, usa un Loop para procesar de a 1 página y evitar problemas de contexto del agente.
-Permite adjuntar y procesar imagenes.
-Estructura la información en un objeto json con columnas predefinidas para luego realizar el mapeo y subirlo a una hoja de Google Sheets.
Workflow:
Link Workflow JSON:
📌 Descripción
Este flujo de n8n aprovecha la integración de SerpAPI y el API de OpenAI para crear y publicar diariamente un post en LinkedIn basado en la noticia más reciente del tema seleccionado.
El objetivo es mantener una presencia activa y profesional en LinkedIn, generando contenido relevante de alto valor y actualizado sin intervención manual.
⚙️ Funcionamiento del Flujo
Obtención de noticias 📰: Se consulta la noticia más reciente sobre el tema configurado mediante SerpAPI.
Generación de copy✍️: Con el contenido de la noticia, el API de OpenAI redacta un post optimizado para LinkedIn con tono profesional, atractivo y conciso, añadiendo hashtags relacionados.
Generación de imagen✍️: También se utiliza el API de OpenAI para crear una imagen relacionada a la noticia, para lograr más visibilidad.
Publicación automática🔗: El copy final y la imagen se envía a la API de **LinkedIn** para ser publicado.
🎯 Beneficios
Ahorro de tiempo: elimina la búsqueda manual de noticias y la redacción de posts.
Consistencia: asegura publicaciones diarias y oportunas.
Relevancia: el contenido siempre está alineado con las tendencias más recientes.
🛠️ Requisitos
Una cuenta de n8n.
Clave de SerpAPI para consultar noticias.
Clave de OpenAI API para generar contenido.
Credenciales de LinkedIn para publicar posts automáticamente.
📌 Notas
Este flujo es un ejemplo base y puede integrarse con otros nodos para enriquecer el contenido.
Asegúrate de respetar las políticas de uso de LinkedIn para evitar bloqueos por publicaciones automatizadas.
Para ver resultados creados por el flujo, puedes visitar mi perfil de LinkedIn
Todos tenemos el derecho a participar en los mercados sin dejarnos arrastrar por nuestras emociones 🌟
4 Expertos LLMs que observan el mercado por ti, registran todo y te avisan cuando se presenta tu patron de alta oportunidad según tu estrategia
Podrias dar mas detalles sobre su configuración y uso? soy algo nuevo, se ve increible!
Apenas veo tu comentario bro, podes descargar el json que esta en el enlace drive y despues importarlo en la plataforma de n8n, en este video te explican como: .
Te recomiendo que aprendas harto de javascript para que le saques el mayor potencial a n8n
¡Hola, comunidad de Platzi! 👋
Estoy muy emocionado de participar en la iniciativa de n8n. He creado un flujo de trabajo que actúa como asistente personal a través de Telegram para potenciar nuestra productividad con Todoist.
Enlace al código JSON del flujo:
Asistente de Tareas por Voz para Todoist 🎙️
¿Qué problema resuelve? ¿Cuántas veces se te ocurre una tarea importante mientras conduces, caminas o tienes las manos ocupadas? Para cuando puedes anotarla, a veces ya la has olvidado. Este flujo captura esas ideas al instante y las convierte en tareas.
¿Cómo lo resuelve este flujo? Este es un bot de Telegram que escucha tus notas de voz, las entiende y las convierte en tareas dentro de Todoist.
Envías una nota de voz a tu bot de Telegram diciendo, por ejemplo: "Recuérdame llamar al cliente mañana a las 10 a. m. trabajo".
El flujo recibe el audio y lo transcribe a texto usando la IA de Gemini.
Un agente de IA analiza el texto, extrae la tarea principal ("llamar al cliente"), calcula la fecha y hora exactas ("mañana a las 10 a. m." se convierte en 2025-10-01 10:00), e identifica las etiquetas (#trabajo).
Finalmente, crea la tarea en tu bandeja de entrada de Todoist con toda la información estructurada.
¿Cómo puedes implementarlo?
Nodos Principales:Telegram Trigger, If (para detectar nota de voz), Google Gemini (para transcripción), AI Agent, Todoist.
Credenciales Necesarias:
Telegram: Crea un bot con BotFather en telegram para obtener tu API token.
Google Gemini: Obtén tu API Key desde Google AI Studio.
Todoist: Obtén tu API token desde la sección de Integraciones en la configuración de Todoist.
Configuración:
En el nodo de Telegram (Telegram Trigger), configura tus credenciales.
En el nodo de Todoist (Create_a_task_in_Todoist), puedes pre-seleccionar el proyecto donde quieres que caigan las tareas (por defecto, la bandeja de entrada).
El prompt del Agente de IA ya está preparado para calcular fechas relativas como "mañana", "en 2 días", etc.
Ojo: Solo recibe notas de voz, le mandas texto no responde.
¡Espero que este flujo les sea tan útil como a mí! Agradezco mucho sus votos. ¡Nunca pares de aprender! 💚
También dejo el JSON por aquí:
{"nodes":[{"parameters":{"updates":["message"],"additionalFields":{}},"type":"n8n-nodes-base.telegramTrigger","typeVersion":1.2,"position":[-576,-64],"id":"8958e181-6375-48c2-aa50-ac9e55c3a8c7","name":"Telegram Trigger","webhookId":"b87d1a99-546f-4aa9-a8ea-b8de706c80e6","credentials":{"telegramApi":{"id":"K9XmQm2PNq8Bpabk","name":"Telegram account"}}},{"parameters":{"conditions":{"options":{"caseSensitive":true,"leftValue":"","typeValidation":"strict","version":2},"conditions":[{"id":"4e897c31-5b43-4bfb-97d0-4be8cc0f8ba4","leftValue":"={{ $json.message.voice }}","rightValue":"","operator":{"type":"object","operation":"exists","singleValue":true}}],"combinator":"and"},"options":{}},"type":"n8n-nodes-base.if","typeVersion":2.2,"position":[-336,-64],"id":"c0d19922-f6ff-4e76-a93a-d14d1d50a28f","name":"If"},{"parameters":{"resource":"file","fileId":"={{ $json.message.voice.file_id }}","additionalFields":{}},"type":"n8n-nodes-base.telegram","typeVersion":1.2,"position":[-112,-224],"id":"d4e7a8c0-38f6-442f-b24c-fe5a34a674b1","name":"Get a file","webhookId":"e4ecbe9a-a30b-4854-bbf2-2c4f86c4d5ec","credentials":{"telegramApi":{"id":"K9XmQm2PNq8Bpabk","name":"Telegram account"}}},{"parameters":{"resource":"audio","modelId":{"__rl":true,"value":"models/gemini-2.5-flash","mode":"list","cachedResultName":"models/gemini-2.5-flash"},"inputType":"binary","options":{}},"type":"@n8n/n8n-nodes-langchain.googleGemini","typeVersion":1,"position":[112,-224],"id":"7fa73442-1a0b-4d9f-a97f-30b0d3a85c4d","name":"Transcribe a recording","credentials":{"googlePalmApi":{"id":"W1hBYDy0unswcp5H","name":"Google Gemini(PaLM) Api account"}}},{"parameters":{"promptType":"define","text":"={{ $json.prompt_user }}","options":{"systemMessage":"=# ROL Y OBJETIVO\nEres un Asistente Personal de Productividad especializado en Todoist. Tu función es recibir un texto (transcrito de una nota de voz) y la fecha actual, calcular la fecha de vencimiento exacta y usar esta información para llamar a la herramienta `Create_a_task_in_Todoist`.\n\n# CONTEXTO PROPORCIONADO\nRecibirás la fecha actual de la herramienta 'FechaActual'.\n\n# PROCESO DE EJECUCIÓN OBLIGATORIO\n1. **ANÁLISIS DEL TEXTO:** Lee el texto de la tarea para identificar los datos clave.\n2. **CÁLCULO DE FECHA:** Usando la fecha actual que se te proporciona, calcula la fecha absoluta para cualquier mención de tiempo relativa (ej: si hoy es 2025-09-30 y el texto dice \"mañana\", la fecha es 2025-10-01). La fecha DEBE ser formateada como YYYY-MM-DD.\n3. **LLAMADA A LA HERRAMIENTA:** Llama inmediatamente a la herramienta `Create_a_task_in_Todoist` con los siguientes parámetros:\n * `Content`: El texto de la tarea principal, eliminando frases de relleno.\n * `Due_Date_Time`: La fecha de vencimiento que calculaste y formateaste como YYYY-MM-DD.\n * `Label_Names`: Las etiquetas que encuentres, separadas por comas.\n\nHoy es {{ $now }}"}},"type":"@n8n/n8n-nodes-langchain.agent","typeVersion":2.2,"position":[640,-64],"id":"6b7269f8-694a-4a1f-88a4-9628e653a4a0","name":"AI Agent"},{"parameters":{"options":{}},"type":"@n8n/n8n-nodes-langchain.lmChatGoogleGemini","typeVersion":1,"position":[560,160],"id":"f0e82cd8-4b94-45cf-8628-600b78c9d6a7","name":"Google Gemini Chat Model","credentials":{"googlePalmApi":{"id":"W1hBYDy0unswcp5H","name":"Google Gemini(PaLM) Api account"}}},{"parameters":{"sessionIdType":"customKey","sessionKey":"={{ $('Telegram Trigger').item.json.message.chat }}"},"type":"@n8n/n8n-nodes-langchain.memoryBufferWindow","typeVersion":1.3,"position":[720,160],"id":"3eb31fde-99f5-4a72-8900-01ebb05ea917","name":"Simple Memory"},{"parameters":{"chatId":"={{ $('Telegram Trigger').item.json.message.from.id }}","text":"={{ $json.output }}","additionalFields":{"appendAttribution":false}},"type":"n8n-nodes-base.telegram","typeVersion":1.2,"position":[1104,-64],"id":"44345ead-da5b-49a8-a7bf-936ad575a470","name":"Send a text message","webhookId":"1ff26741-8abb-4346-a48c-f05bd09b1935","credentials":{"telegramApi":{"id":"K9XmQm2PNq8Bpabk","name":"Telegram account"}}},{"parameters":{"assignments":{"assignments":[{"id":"7dfb64a3-03bc-4377-b599-081604a38be7","name":"prompt_user","value":"={{ $json.content.parts[0].text }}","type":"string"}]},"options":{}},"type":"n8n-nodes-base.set","typeVersion":3.4,"position":[336,-224],"id":"1b855128-d915-46bb-974e-6c820be2d05e","name":"Edit Fields"},{"parameters":{"authentication":"oAuth2","project":{"__rl":true,"value":"2294851338","mode":"list","cachedResultName":"Inbox"},"labels":"={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Label_Names', ``, 'string') }}","content":"={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Content', ``, 'string') }}","options":{"dueDateTime":"={{ $fromAI('Due_Date_Time', ``, 'string') }}","priority":"={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Priority', ``, 'number') }}"}},"type":"n8n-nodes-base.todoistTool","typeVersion":2.1,"position":[976,144],"id":"09faf3d1-5e83-476c-b279-7c2fa0f45269","name":"Create_a_task_in_Todoist","credentials":{"todoistOAuth2Api":{"id":"x6EuMojoQoHj0bOh","name":"Todoist account"}}},{"parameters":{"options":{}},"type":"n8n-nodes-base.dateTimeTool","typeVersion":2,"position":[848,160],"id":"0f4370f2-1e17-41df-b978-7ead5c4aa236","name":"FechaActual"}],"connections":{"Telegram Trigger":{"main":[[{"node":"If","type":"main","index":0}]]},"If":{"main":[[{"node":"Get a file","type":"main","index":0}]]},"Get a file":{"main":[[{"node":"Transcribe a recording","type":"main","index":0}]]},"Transcribe a recording":{"main":[[{"node":"Edit Fields","type":"main","index":0}]]},"AI Agent":{"main":[[{"node":"Send a text message","type":"main","index":0}]]},"Google Gemini Chat Model":{"ai_languageModel":[[{"node":"AI Agent","type":"ai_languageModel","index":0}]]},"Simple Memory":{"ai_memory":[[{"node":"AI Agent","type":"ai_memory","index":0}]]},"Edit Fields":{"main":[[{"node":"AI Agent","type":"main","index":0}]]},"Create_a_task_in_Todoist":{"ai_tool":[[{"node":"AI Agent","type":"ai_tool","index":0}]]},"FechaActual":{"ai_tool":[[{"node":"AI Agent","type":"ai_tool","index":0}]]}},"pinData":{},"meta":{"templateCredsSetupCompleted":true,"instanceId":"334dacbbd7ca0965fc4e2b5c89783a1cf3bd131378f3193ef0a55d09a65d11e8"}}
Quería tener control de en qué 💳 narices gasto con la tarjeta de crédito, así que armé este Flow de N8N 🚀:
1️⃣ Pendiente de cuando llegue mi estado de cuenta 📩.
2️⃣ Lo separa en imágenes 🖼️ para luego transformarlo a Markdown 📝 y así tomar todos los gastos.
3️⃣ Envía la información a un Agente IA 🤖para analizarusando OpenRouter como proveedor LLM.
4️⃣ Inserta los gastos directamente en Google Sheets 📊.
5️⃣ Notifica en Telegram 📲 cuánto dinero debo en moneda nacional 💰.
💡 PD: Todo corre en Docker 🐳 + VPS con mi dominio propio 🌐.
"text": "Extrae la tabla de DETALLE DE TRANSACCION con todas sus filas y extrae el valor que esta en pago de contado en su valor de CORBODAS Y DOLARES en formato markdown.\n",
"messages": {
"messageValues": [
{
"message": "=You help transcribe documents to markdown, keeping faithful to all text printed and visible to the best of your ability.\nEnsure you capture all headings, subheadings, titles as well as small print.\n\nFor any tables found with the document, convert them to markdown tables. If table row descriptions overflow into more than 1 row, \nconcatenate and fit them into a single row. If two or more tables are adjacent horizontally, stack the tables vertically instead. \nThere should be a newline after every markdown table.\n\nFor any graphics, replace with a description of the image. \n\nImages of scanned checks should be converted to the phrase \"<scanned image of check>\".\nWhen amounts or values are listed, clearly separate and label those in NIO (Córdoba) and USD (U.S. dollars)."
"text": "=Here is the resume:\n{{ $json.pages[0] }}",
"hasOutputParser": true,
"options": {
"systemMessage": "You are an invaluable assistant. You were given a resume. \nIdentifica si es La Factura Claro o Tigo\nYou have to help me analyze the resume and extract the \"PAGO DE CONTADO\", Total_NIO and Total_USD .\nExample:\nTotal_NIO: C$. 0.00\nTotal_USD: $. 0.00"
"text": "=Estado de Cuenta C$ {{ $json.output.Total_NIO }} en $ {{ $json.output.Total_USB }} para un Total de C$ {{ $json.output.Total_NIO + ($json.output.Total_USB * 37.05) }}",
🚀 Sistema Automático de Marketing Personalizado con IA
Por: Santiago Pineda
🔥 El Problema Real: El Cementerio de los Clientes Perdidos
Las pequeñas empresas y startups luchan a diario contra un enemigo silencioso: la irrelevancia. Pierden hasta un 70% de sus clientes potenciales porque caen en las mismas trampas:
Marketing de Talla Única: Envían el mismo email genérico a todos, esperando resultados diferentes.
Desconocimiento del Cliente: No tienen tiempo ni herramientas para entender los intereses reales de cada persona.
Oportunidades Perdidas: No logran capitalizar las tendencias del momento para conectar con su audiencia.
Costos Prohibitivos: Gastan fortunas en agencias de marketing que, a menudo, aplican las mismas estrategias genéricas.
El resultado es un ciclo de bajo engagement, ventas perdidas y un potencial que nunca se realiza.
💎 La Solución: Tu Director de Marketing IA, 24/7
He construido un sistema autónomo en n8n que no es solo una automatización; es un cerebro de marketing estratégico que trabaja para ti. Este flujo automáticamente:
Analiza y Entiende: Ingiere los datos de tus clientes desde una simple hoja de Google Sheets.
Segmenta con Inteligencia (IA): En lugar de filtros manuales, utiliza GPT-4 para identificar patrones de comportamiento y crear segmentos dinámicos y de alto valor (como "VIPs", "Clientes en Riesgo" o "Potenciales Compradores de Temporada").
Investiga Tendencias en Tiempo Real: Para cada segmento, busca activamente en Google las tendencias y temas relevantes que capturan su interés ahora mismo.
Crea Contenido Hiper-Personalizado (IA): Genera, con GPT-4o-mini, un email único y relevante para cada cliente, combinando su historial de compras con las tendencias actuales del mercado.
Ejecuta y Mide: Envía las campañas y registra cada acción en un dashboard de analytics, cerrando el ciclo para aprender y mejorar en la siguiente ejecución.
🎯 Resultados: De Ignorado a Imprescindible
Este no es un proyecto teórico. Es un motor de crecimiento probado:
3x más apertura de emails: Porque el contenido es relevante.
5x más conversiones en ventas: Porque el mensaje llega en el momento y contexto perfectos.
80% de reducción en tiempo operativo: Liberando horas para enfocarte en el negocio, no en tareas repetitivas.
ROI promedio del 400%: Cada mensaje cuenta, cada cliente se siente único.
🔄 La Arquitectura de la Inteligencia: Cómo Funciona
El flujo está diseñado para ser eficiente y optimizar costos, con dos fases clave:
1. Fase de Estrategia (1 vez por ejecución):
El sistema analiza a todos los clientes en conjunto para que la IA defina los segmentos más inteligentes.
Luego, busca tendencias por segmento, no por cliente, ahorrando cientos de llamadas a APIs.
2. Fase de Ejecución (Bucle por cliente):
A cada cliente se le asigna su segmento.
La IA redacta un correo único, combinando los datos del cliente con la estrategia y tendencias del segmento.
Se envía el correo y se registran los datos.
💻 Workflow JSON: Listo para Importar y Ganar
Este es el corazón del proyecto. Un flujo robusto, optimizado y listo para desplegar.
Mejoras Clave en este JSON:
Lógica Optimizada: La segmentación se realiza sobre el conjunto completo de datos, no por cliente, para una estrategia más inteligente.
Eficiencia de Costos: Las llamadas a APIs de tendencias se hacen por segmento, reduciendo drásticamente los costos.
Manejo de Datos: Utiliza nodos Set y Merge para manejar la información de manera limpia y estructurada a través del flujo.
JSON
{
"name": "Marketing Personalizado IA - Platzi Contest (Optimizado)",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "weeks",
"weeksInterval": 1
}
]
}
},
"id": "31362c3e-52a1-42a9-8260-84384a51e6d1",
"name": "Ejecutar Semanalmente",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.1,
"position": [
-200,
320
]
},
{
"parameters": {
"operation": "read",
"documentId": "{{TU_GOOGLE_SHEET_ID}}",
"sheetName": "Clientes",
"options": {
"fromRow": 2
}
},
"id": "e2e920d3-c9cf-4874-84da-5079a4991191",
"name": "1. Obtener Datos Clientes",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
20,
320
],
"credentials": {
"googleSheetsOAuth2Api": {
"id": "{{TU_CREDENCIAL_GOOGLE_SHEETS}}",
"name": "Google Sheets"
}
}
},
{
"parameters": {
"model": "gpt-4-turbo",
"responseFormat": "json_object",
"prompt": "=Eres un estratega de marketing de clase mundial. Analiza esta lista completa de clientes y define entre 3 y 5 segmentos clave. Para cada segmento, proporciona:\n1. 'nombre': Un nombre claro y accionable (ej. 'Clientes VIP', 'En Riesgo de Abandono').\n2. 'descripcion': Una frase que defina al grupo.\n3. 'keywords_tendencia': Un string de 3-5 palabras clave para buscar tendencias relevantes para este grupo (ej. 'lujo moda sostenible 2025').\n\nDatos de clientes:\n{{ JSON.stringify($('1. Obtener Datos Clientes').all()) }}\n\nResponde únicamente con un objeto JSON que contenga una clave 'segmentos', que sea un array de los objetos que definiste."
},
"id": "4b68427f-f773-455a-9351-7ab349581c1c",
"name": "2. IA - Definir Estrategia de Segmentos",
"type": "n8n-nodes-base.openAiChat",
"typeVersion": 2,
"position": [
280,
320
],
"credentials": {
"openAiApi": {
"id": "{{TU_CREDENCIAL_OPENAI}}",
"name": "OpenAI"
}
}
},
{
"parameters": {
"jsCode": "const data = JSON.parse( $('2. IA - Definir Estrategia de Segmentos').item.json.message );\nreturn data.segmentos;"
},
"id": "4507b55f-8d26-4fa2-944a-d698f1f77d24",
"name": "Extraer Segmentos",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
500,
320
]
},
{
"parameters": {
"url": "=https://www.google.com/search?q={{ $json.keywords_tendencia }}&gl=us&hl=es",
"responseFormat": "html",
"options": {}
},
"id": "47a32a0d-2917-43ca-938f-a99182312d8a",
"name": "3. Buscar Tendencias por Segmento",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
700,
320
]
},
{
"parameters": {
"mode": "mergeByPosition",
"join": "left",
"options": {}
},
"id": "ac676e73-9095-46aa-a39c-f9024f0c9772",
"name": "Combinar Segmentos y Tendencias",
"type": "n8n-nodes-base.merge",
"typeVersion": 2.1,
"position": [
1120,
320
]
},
{
"parameters": {
"model": "gpt-4o-mini",
"prompt": "=Eres un experto en extracción de información web (scraper). Del siguiente HTML de una búsqueda de Google, extrae los 3 titulares de noticias o artículos más relevantes y resúmelos en una sola frase concisa.\n\nHTML:\n{{ $('3. Buscar Tendencias por Segmento').item.rawBody }}\n\nResumen de Tendencias:"
},
"id": "e0a514d7-0fcd-409b-a63e-0056669911e3",
"name": "IA - Resumir Tendencias",
"type": "n8n-nodes-base.openAiChat",
"typeVersion": 2,
"position": [
900,
320
],
"credentials": {
"openAiApi": {
"id": "{{TU_CREDENCIAL_OPENAI}}",
"name": "OpenAI"
}
}
},
{
"parameters": {
"model": "gpt-4o-mini",
"prompt": "=Eres un copywriter de clase mundial con un toque humano y personal. Tu tarea es escribir un email para un cliente.\n\n**Contexto de la campaña:**\n- Nombre del Segmento: {{ $('Asignar Estrategia a Cliente').item.json.segmento_data.nombre }}\n- Descripción del Segmento: {{ $('Asignar Estrategia a Cliente').item.json.segmento_data.descripcion }}\n- Tendencias Actuales Relevantes: {{ $('Asignar Estrategia a Cliente').item.json.segmento_data.tendencias }}\n\n**Datos del Cliente:**\n- Nombre: {{ $('Asignar Estrategia a Cliente').item.json.cliente_data.Nombre }}\n- Última Compra: {{ $('Asignar Estrategia a Cliente').item.json.cliente_data['Última Compra'] }}\n- Productos Comprados: {{ $('Asignar Estrategia a Cliente').item.json.cliente_data.Productos }}\n\n**Instrucciones:**\n1. Escribe un **Asunto** de email corto y llamativo (máx 8 palabras).\n2. Escribe el **Cuerpo** del email (máx 150 palabras). Debe ser cercano, mencionar sutilmente algo relacionado a su historial o segmento e integrar de forma natural las tendencias.\n3. Termina con un Call to Action (CTA) claro.\n\n**Formato de Salida (JSON estricto):**\n{\n \"asunto\": \"Asunto del email\",\n \"cuerpo\": \"Cuerpo del email en formato HTML simple (usa <p> y <strong>).\"\n}"
},
"id": "e7b0e1b2-c07a-4284-9092-2f3b97b0a39c",
"name": "5. IA - Generar Email Personalizado",
"type": "n8n-nodes-base.openAiChat",
"typeVersion": 2,
"position": [
600,
560
],
"credentials": {
"openAiApi": {
"id": "{{TU_CREDENCIAL_OPENAI}}",
"name": "OpenAI"
}
}
},
{
"parameters": {
"to": "={{ $('Asignar Estrategia a Cliente').item.json.cliente_data.Email }}",
"subject": "={{ JSON.parse($('5. IA - Generar Email Personalizado').item.json.message).asunto }}",
"html": "={{ JSON.parse($('5. IA - Generar Email Personalizado').item.json.message).cuerpo }}"
},
"id": "3368b6b0-7f2a-4384-9543-85b57f0f67ac",
"name": "6. Enviar Email",
"type": "n8n-nodes-base.gmail",
"typeVersion": 4,
"position": [
840,
560
],
"credentials": {
"gmailOAuth2Api": {
"id": "{{TU_CREDENCIAL_GMAIL}}",
"name": "Gmail"
}
}
},
{
"parameters": {
"operation": "append",
"documentId": "{{TU_GOOGLE_SHEET_ID}}",
"sheetName": "Analytics",
"fields": {
"field": [
{
"name": "fecha",
"value": "={{ $now.toFormat('yyyy-MM-dd HH:mm') }}"
},
{
"name": "cliente_email",
"value": "={{ $('Asignar Estrategia a Cliente').item.json.cliente_data.Email }}"
},
{
"name": "segmento",
"value": "={{ $('Asignar Estrategia a Cliente').item.json.segmento_data.nombre }}"
},
{
"name": "asunto_enviado",
"value": "={{ JSON.parse($('5. IA - Generar Email Personalizado').item.json.message).asunto }}"
},
{
"name": "estado",
"value": "Enviado"
}
]
},
"options": {}
},
"id": "e9671d18-36a8-4c60-8f92-749e917d4a20",
"name": "7. Guardar en Analytics",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
1060,
560
],
"credentials": {
"googleSheetsOAuth2Api": {
"id": "{{TU_CREDENCIAL_GOOGLE_SHEETS}}",
"name": "Google Sheets"
}
}
},
{
"parameters": {
"keep": "all",
"values": {
"string": [
{
"name": "estrategia_completa",
"value": "={{ $('Combinar Segmentos y Tendencias').all() }}"
}
]
}
},
"id": "b1cd21c0-0f33-40e9-a417-6466f2c00a6f",
"name": "Almacenar Estrategia",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1340,
320
]
},
{
"parameters": {
"mode": "runOnceForEachItem",
"items": "={{ $('1. Obtener Datos Clientes').all() }}"
},
"id": "67323861-c88f-4f6c-8a2a-289874cc9e26",
"name": "4. Loop por cada Cliente",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 2,
"position": [
120,
560
]
},
{
"parameters": {
"jsCode": "const cliente = $json.cliente_data;\nconst estrategia = $json.estrategia_completa;\n\nlet segmentoAsignado = {\n nombre: 'General',\n descripcion: 'Cliente general',\n tendencias: 'Novedades y ofertas generales'\n};\n\n// Lógica simple de asignación (se puede hacer más compleja)\nconst valorTotal = parseFloat(cliente.ValorTotal.replace('$', ''));\nconst ultimaCompra = new Date(cliente['Última Compra']);\nconst hoy = new Date();\nconst diffTime = Math.abs(hoy - ultimaCompra);\nconst diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));\n\nif (valorTotal > 500) {\n segmentoAsignado = estrategia.find(s => s.json.nombre.toLowerCase().includes('vip'))?.json || segmentoAsignado;\n} else if (diffDays > 90) {\n segmentoAsignado = estrategia.find(s => s.json.nombre.toLowerCase().includes('inactivo') || s.json.nombre.toLowerCase().includes('riesgo'))?.json || segmentoAsignado;\n} else {\n segmentoAsignado = estrategia.find(s => s.json.nombre.toLowerCase().includes('nuevo') || s.json.nombre.toLowerCase().includes('activo'))?.json || segmentoAsignado;\n}\n\n$json.segmento_data = {\n nombre: segmentoAsignado.nombre,\n descripcion: segmentoAsignado.descripcion,\n tendencias: segmentoAsignado.message // El resumen de tendencias viene en el campo 'message'\n};\n\nreturn $json;"
},
"id": "c6168e27-6f77-4402-a1da-3f114c62c3f8",
"name": "Asignar Estrategia a Cliente",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
360,
560
]
}
],
"connections": {
"Ejecutar Semanalmente": {
"main": [
[
{
"node": "1. Obtener Datos Clientes",
"type": "main",
"index": 0
}
]
]
},
"1. Obtener Datos Clientes": {
"main": [
[
{
"node": "2. IA - Definir Estrategia de Segmentos",
"type": "main",
"index": 0
}
]
]
},
"2. IA - Definir Estrategia de Segmentos": {
"main": [
[
{
"node": "Extraer Segmentos",
"type": "main",
"index": 0
}
]
]
},
"Extraer Segmentos": {
"main": [
[
{
"node": "3. Buscar Tendencias por Segmento",
"type": "main",
"index": 0
}
]
]
},
"3. Buscar Tendencias por Segmento": {
"main": [
[
{
"node": "IA - Resumir Tendencias",
"type": "main",
"index": 0
}
]
]
},
"Combinar Segmentos y Tendencias": {
"main": [
[
{
"node": "Almacenar Estrategia",
"type": "main",
"index": 0
}
]
]
},
"IA - Resumir Tendencias": {
"main": [
[
{
"node": "Combinar Segmentos y Tendencias",
"type": "main",
"index": 1
}
]
]
},
"5. IA - Generar Email Personalizado": {
"main": [
[
{
"node": "6. Enviar Email",
"type": "main",
"index": 0
}
]
]
},
"6. Enviar Email": {
"main": [
[
{
"node": "7. Guardar en Analytics",
"type": "main",
"index": 0
}
]
]
},
"Almacenar Estrategia": {
"main": [
[
{
"node": "4. Loop por cada Cliente",
"type": "main",
"index": 0
}
]
]
},
"4. Loop por cada Cliente": {
"main": [
[
{
"node": "Asignar Estrategia a Cliente",
"type": "main",
"index": 0
}
]
]
},
"Asignar Estrategia a Cliente": {
"main": [
[
{
"node": "5. IA - Generar Email Personalizado",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"settings": {},
"staticData": {
"node:Extraer Segmentos": {
"main": [
{
"items": [
{
"json": {}
}
],
"pairedItem": {
"item": 0
}
}
]
}
}
}
🤝 Mi Compromiso con la Comunidad Platzi
Este proyecto es solo el comienzo. Si tengo el honor de ganar una de las licencias, me comprometo a reinvertir ese privilegio directamente en nuestra comunidad:
🎓 Crear un Curso Práctico: Grabaré un tutorial detallado, paso a paso, explicando cómo implementar y personalizar este sistema desde cero.
🧩 Compartir 10+ Templates Adicionales: Desarrollaré y compartiré flujos listos para usar para problemas comunes (soporte al cliente, onboarding, gestión de leads, etc.).
👨🏫 Mentoría Gratuita: Ofreceré sesiones de mentoría a 5 estudiantes de Platzi que quieran iniciar su camino en la automatización con n8n.
🔴 Streams Mensuales: Realizaré transmisiones en vivo construyendo automatizaciones y resolviendo dudas de la comunidad.
⚡ Quick Start: Tu Camino a la Automatización
Copia el JSON: Importa el flujo en tu instancia de n8n.
Configura tus Credenciales: Conecta tus cuentas de Google, OpenAI y Gmail.
Prepara tus Datos: Usa <u>este template </u>como guía.
Activa el Flujo: ¡Y observa cómo tu nuevo director de marketing IA se pone a trabajar!
🙏 Si crees que este proyecto puede cambiar la forma en que las pequeñas empresas hacen marketing, te pido tu voto.
¡Vamos a construir el futuro de la automatización, juntos!
Santiago desde ya me apunto a tu mentoría. Se que la vas a ganar.
Genial!
Mi flujo de trabajo de n8n se encarga de automatizar la captura, clasificación y registro de comentarios de clientes utilizando un formulario y la inteligencia artificial de OpenAI.