This workflow follows the Google Sheets → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Propulsar \u2014 Content Engine v3",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "propulsar-content",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-trigger",
"name": "\ud83c\udfaf Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
208,
304
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={ \"status\": \"received\" }",
"options": {}
},
"id": "respond-wizard",
"name": "\u2705 Responder al Wizard",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
432,
160
]
},
{
"parameters": {
"modelId": {
"__rl": true,
"value": "gpt-4o",
"mode": "list",
"cachedResultName": "GPT-4O"
},
"messages": {
"values": [
{
"content": "Eres el director creativo de Propulsar.ai, agencia de automatizaciones IA para PYMEs.\n\nTu tarea es generar prompts de imagen para cada slide de un carrusel de Instagram/Facebook, m\u00e1s los captions para ambas plataformas.\n\nPRIMERO elige la estructura de carrusel m\u00e1s adecuada para el tema y tipo:\n- narrativo: cuenta una historia con arco (inicio \u2192 desarrollo \u2192 resoluci\u00f3n)\n- listicle: lista de tips, herramientas o beneficios numerados\n- paso-a-paso: instrucciones secuenciales para lograr algo\n- antes-despu\u00e9s: contraste entre una situaci\u00f3n actual y la transformaci\u00f3n\n- pregunta-respuesta: hook con pregunta, slides respondiendo, CTA final\n\nIDENTIDAD VISUAL PROPULSAR (aplicar en CADA prompt sin excepci\u00f3n):\n- Fondo oscuro profundo: #1a1a2e\n- Gradiente principal: p\u00farpura #6B46C1 \u2192 magenta #EC4899\n- Tipograf\u00eda bold sans-serif en espa\u00f1ol, legible, prominente\n- Formato cuadrado 1:1\n- Est\u00e9tica profesional tech/AI, alto contraste, ultra-detailed, 4K quality\n\nREGLAS DE TEXTO EN IMAGEN (CR\u00cdTICO):\n1. El texto_overlay es el texto EXACTO que aparecer\u00e1 visible en la imagen. M\u00e1ximo 6-8 palabras. Si necesit\u00e1s m\u00e1s, divid\u00ed en m\u00e1s slides.\n2. En el campo prompt, el texto en espa\u00f1ol va PRIMERO entre comillas dobles, usando el patr\u00f3n: Text says: \"[texto_overlay exacto]\". Despu\u00e9s describ\u00ed el visual.\n3. NUNCA incluir texto en ingl\u00e9s visible en la imagen. Todo texto visible debe ser en espa\u00f1ol castellano.\n4. Prohibido usar texto lorem ipsum, texto decorativo ilegible, o texto de relleno.\n\nREGLAS GENERALES:\n1. Cada campo prompt debe ser COMPLETAMENTE autocontenido con TODAS las restricciones de estilo. Prohibido usar \"same as above\", \"idem\" o referencias a otros slides.\n2. El array slides debe tener EXACTAMENTE el n\u00famero de slides solicitados.\n3. Los prompt van en ingl\u00e9s (excepto el texto visible que va en espa\u00f1ol entre comillas).\n4. Los texto_overlay van en espa\u00f1ol.\n5. Respond\u00e9 con JSON puro sin markdown, sin bloques ```json.\n\nFORMATO DE RESPUESTA:\n{\n \"estructura\": \"narrativo|listicle|paso-a-paso|antes-despu\u00e9s|pregunta-respuesta\",\n \"instagram_caption\": \"caption completo con hook <125 chars, 150-300 palabras, p\u00e1rrafos cortos, 8-12 hashtags, m\u00e1x 3 emojis al inicio de l\u00ednea, CTA claro\",\n \"facebook_caption\": \"caption 100-200 palabras, 3-5 hashtags, pregunta para comentarios al final, CTA\",\n \"slides\": [\n {\n \"slide_num\": 1,\n \"texto_overlay\": \"m\u00e1x 6-8 palabras en espa\u00f1ol\",\n \"prompt\": \"Text says: \\\"[texto_overlay exacto aqu\u00ed]\\\". Dark background #1a1a2e, bold white sans-serif typography centered, purple #6B46C1 to magenta #EC4899 gradient glow, professional AI/tech aesthetic, ultra-detailed, crisp text, square 1:1, no other text visible. [descripci\u00f3n visual espec\u00edfica del slide].\"\n }\n ]\n}\n\nEJEMPLO DE UN BUEN PROMPT:\nText says: \"Automatiza tu WhatsApp\". Dark background #1a1a2e, bold white sans-serif typography centered, purple #6B46C1 to magenta #EC4899 gradient glow behind text, professional AI/tech aesthetic, minimalist icons of chat bubbles and automation gears, ultra-detailed, crisp text, square 1:1, no other text visible.",
"role": "system"
},
{
"content": "=Tema: \"{{ $json.body.topic }}\"\nTipo: {{ $json.body.type }}\n{% if $json.body.angle %}\u00c1ngulo: {{ $json.body.angle }}\n{% endif %}Slides requeridos: {{ $json.body.num_images }}\n\nElige la estructura del carrusel apropiada y genera EXACTAMENTE {{ $json.body.num_images }} prompts de imagen, m\u00e1s los captions para Instagram y Facebook."
}
]
},
"options": {
"temperature": 0.5
}
},
"id": "openai-carousel",
"name": "\ud83c\udfa0 GPT-4o \u2014 Prompts Carrusel",
"type": "@n8n/n8n-nodes-langchain.openAi",
"typeVersion": 1.4,
"position": [
640,
528
],
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const raw = $input.first().json.message.content;\nconst b = $('\ud83c\udfaf Webhook Trigger').first().json.body;\n\nlet parsed;\ntry {\n parsed = JSON.parse(raw.replace(/```json\\n?/g,'').replace(/```\\n?/g,'').trim());\n} catch(e) {\n throw new Error('JSON inv\u00e1lido de GPT-4o (carrusel): ' + raw.substring(0,200));\n}\n\nconst slides = parsed.slides || [];\n\nif (slides.length !== b.num_images) {\n console.warn(`\u26a0 GPT-4o devolvi\u00f3 ${slides.length} slides, se esperaban ${b.num_images}`);\n}\n\nreturn [{\n json: {\n instagram: { caption: parsed.instagram_caption || '' },\n facebook: { caption: parsed.facebook_caption || '' },\n topic: b.topic,\n type: b.type,\n angle: b.angle || null,\n platforms: b.platforms,\n image_model: 'ideogram',\n fal_model_id: null,\n has_own_image: false,\n image_url: null,\n has_text_in_image: true,\n approval_number: b.approval_number,\n timestamp: b.timestamp,\n publish_at: b.publish_at || 'now',\n format: 'carousel',\n num_images: b.num_images,\n estructura: parsed.estructura || 'listicle',\n image_prompts: slides.map(s => ({\n slide_num: s.slide_num,\n texto_overlay: s.texto_overlay,\n prompt: s.prompt,\n })),\n }\n}];"
},
"id": "parse-carousel",
"name": "\ud83d\udd27 Parsear prompts carrusel",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
864,
528
]
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\nconst igCaption = data.instagram?.caption || data.instagram_caption || '';\nconst lines = igCaption.split('\\n');\nconst hashtagLines = lines.filter(l => l.trim().startsWith('#'));\nconst cleanLines = lines.filter(l => !l.trim().startsWith('#'));\nconst cleanCaption = cleanLines.join('\\n').trimEnd();\nconst hashtagBlock = hashtagLines.join(' ').trim();\nreturn [{\n json: {\n ...data,\n instagram: { ...(data.instagram || {}), caption: cleanCaption },\n instagram_caption: cleanCaption,\n hashtag_block: hashtagBlock,\n }\n}];"
},
"id": "extract-hashtags-carousel",
"name": "\u2702\ufe0f Extract Hashtags (Carousel)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
976,
528
]
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\nconst prompts = data.image_prompts;\n\nreturn prompts.map(slide => ({\n json: {\n slide_num: slide.slide_num,\n texto_overlay: slide.texto_overlay,\n prompt: slide.prompt,\n approval_number: data.approval_number,\n num_images: data.num_images,\n topic: data.topic,\n type: data.type,\n }\n}));"
},
"id": "explode-carousel-slides",
"name": "\ud83c\udfa0 Explode Slides",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1088,
528
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.ideogram.ai/generate",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Api-Key",
"value": "={{ $env.IDEOGRAM_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"image_request\": {\n \"prompt\": \"{{ $json.prompt.replaceAll('\"', '\\\\\"') }}\",\n \"aspect_ratio\": \"ASPECT_1_1\",\n \"model\": \"V_2_TURBO\",\n \"magic_prompt_option\": \"OFF\",\n \"style_type\": \"DESIGN\"\n }\n}",
"options": {}
},
"id": "ideogram-slide",
"name": "\ud83d\udd24 Ideogram \u2014 Slide",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1520,
528
],
"notes": "Ideogram v2 per carousel slide \u2014 called once per slide via SplitInBatches loop"
},
{
"parameters": {
"modelId": {
"__rl": true,
"value": "gpt-4o",
"mode": "list",
"cachedResultName": "GPT-4O"
},
"messages": {
"values": [
{
"content": "Eres el redactor de contenido de Propulsar.ai, agencia de automatizaciones IA para PYMEs.\n\nVoz: profesional pero accesible, directo, espa\u00f1ol latinoamericano (tuteo).\nSin buzzwords. Prohibido: revolutionario, disruptivo, sinergia, delve, unleash.\n\nDevolv\u00e9 SOLO JSON v\u00e1lido, sin markdown:\n{\n \"instagram\": {\n \"caption\": \"post completo con hashtags al final\",\n \"image_prompt\": \"prompt en ingl\u00e9s para generaci\u00f3n de imagen, dark background purple-magenta gradients, professional social media\"\n },\n \"facebook\": {\n \"caption\": \"post completo (m\u00e1s corto, 3-5 hashtags)\"\n }\n}",
"role": "system"
},
{
"content": "=Genera un post {{ $json.body.type }} sobre: \"{{ $json.body.topic }}\"\n\n{% if $json.body.angle %}\u00c1ngulo: {{ $json.body.angle }}{% endif %}\n{% if $json.body.has_text_in_image %}IMPORTANTE: El image_prompt debe incluir texto visible espec\u00edfico relevante al tema, ya que se usar\u00e1 Ideogram (excelente para texto en imagen).{% endif %}\n\nPlataformas: {{ $json.body.platforms.join(', ') }}\n\nInstagram: hook <125 chars, 150-300 palabras, p\u00e1rrafos cortos, 8-12 hashtags, m\u00e1x 3 emojis, CTA.\nFacebook: 100-200 palabras, 3-5 hashtags, pregunta final para comentarios, CTA."
}
]
},
"options": {
"temperature": 0.5
}
},
"id": "openai-text",
"name": "\ud83e\udd16 GPT-4o \u2014 Texto",
"type": "@n8n/n8n-nodes-langchain.openAi",
"typeVersion": 1.4,
"position": [
432,
368
],
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const raw = $input.first().json.message.content;\nlet parsed;\ntry {\n parsed = JSON.parse(raw.replace(/```json\\n?/g,'').replace(/```\\n?/g,'').trim());\n} catch(e) {\n throw new Error('JSON inv\u00e1lido de OpenAI: ' + raw.substring(0,200));\n}\nconst b = $('\ud83c\udfaf Webhook Trigger').first().json.body;\nreturn [{ json: {\n instagram: parsed.instagram || null,\n facebook: parsed.facebook || null,\n image_prompt: parsed.instagram?.image_prompt || '',\n platforms: b.platforms,\n topic: b.topic,\n type: b.type,\n angle: b.angle || null,\n image_model: b.image_model || 'flux',\n fal_model_id: b.fal_model_id || 'fal-ai/flux-pro/v1.1',\n has_own_image: b.has_own_image || false,\n image_url: b.image_url || null,\n has_text_in_image: b.has_text_in_image || false,\n approval_number: b.approval_number,\n timestamp: b.timestamp,\n publish_at: b.publish_at || 'now'\n}}];"
},
"id": "parse-content",
"name": "\ud83d\udd27 Parsear contenido",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
640,
368
]
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\nconst igCaption = data.instagram?.caption || data.instagram_caption || '';\nconst lines = igCaption.split('\\n');\nconst hashtagLines = lines.filter(l => l.trim().startsWith('#'));\nconst cleanLines = lines.filter(l => !l.trim().startsWith('#'));\nconst cleanCaption = cleanLines.join('\\n').trimEnd();\nconst hashtagBlock = hashtagLines.join(' ').trim();\nreturn [{\n json: {\n ...data,\n instagram: { ...(data.instagram || {}), caption: cleanCaption },\n instagram_caption: cleanCaption,\n hashtag_block: hashtagBlock,\n }\n}];"
},
"id": "extract-hashtags-single",
"name": "\u2702\ufe0f Extract Hashtags (Single)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
752,
368
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ String($json.has_own_image) }}",
"value2": "true"
}
]
}
},
"id": "check-own-image",
"name": "\ud83d\uddbc\ufe0f \u00bfImagen propia?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
864,
368
]
},
{
"parameters": {
"jsCode": "return [{ json: { ...$input.first().json, final_image_url: $input.first().json.image_url } }];"
},
"id": "use-own-image",
"name": "\ud83d\udcce Imagen propia",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1088,
208
]
},
{
"parameters": {
"method": "POST",
"url": "https://fal.run/fal-ai/flux-pro/v1.1",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Key {{ $env.FAL_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"prompt\": \"{{ $json.image_prompt }} \u2014 style: dark background #1a1a2e, purple to magenta gradient accents, professional high-quality social media graphic, ultra detailed, 4K\",\n \"image_size\": \"square_hd\",\n \"num_inference_steps\": 28,\n \"guidance_scale\": 3.5,\n \"num_images\": 1,\n \"enable_safety_checker\": true\n}",
"options": {}
},
"id": "flux-generate",
"name": "\u26a1 Flux 2 Pro (FAL.AI)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1312,
304
],
"notes": "FAL model: fal-ai/flux-pro/v1.1\nCosto: ~$0.03/img\nFortaleza: fotorrealismo premium"
},
{
"parameters": {
"method": "POST",
"url": "https://api.ideogram.ai/generate",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Api-Key",
"value": "={{ $env.IDEOGRAM_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"image_request\": {\n \"prompt\": \"{{ $json.image_prompt.replaceAll('\"', '\\\\\"') }} \u2014 professional design, dark background #1a1a2e, purple and magenta gradient elements, bold readable typography, social media post\",\n \"aspect_ratio\": \"ASPECT_1_1\",\n \"model\": \"V_2_TURBO\",\n \"magic_prompt_option\": \"OFF\",\n \"style_type\": \"DESIGN\"\n }\n}",
"options": {}
},
"id": "ideogram-generate",
"name": "\ud83d\udd24 Ideogram v3",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1312,
448
],
"notes": "Costo: ~$0.06/img\nFortaleza: texto legible en imagen (90-95% precisi\u00f3n)\nRequiere IDEOGRAM_API_KEY"
},
{
"parameters": {
"method": "POST",
"url": "https://fal.run/fal-ai/nano-banana",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Key {{ $env.FAL_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"prompt\": \"{{ $json.image_prompt }} \u2014 ultra high quality, dark background #1a1a2e, purple magenta gradient accents, professional social media graphic, 4K resolution, photorealistic\",\n \"num_images\": 1,\n \"aspect_ratio\": \"1:1\",\n \"output_format\": \"png\",\n \"safety_tolerance\": \"4\"\n}",
"options": {}
},
"id": "nano-banana-generate",
"name": "\ud83c\udf4c Nano Banana Pro (FAL.AI)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1312,
592
],
"notes": "FAL model: fal-ai/nano-banana (Gemini 3 Pro Image)\nCosto: ~$0.15/img\nFortaleza: razonamiento visual, 4K, alto impacto\nMisma FAL_API_KEY que Flux"
},
{
"parameters": {
"jsCode": "// Normalizar la URL de imagen de cualquier proveedor\nconst content = $('\ud83d\udd27 Parsear contenido').first().json;\nconst imageData = $input.first().json;\nconst model = content.image_model;\n\nlet finalUrl = null;\n\nif (model === 'flux' || model === 'nanoBanana') {\n // FAL.AI devuelve: { images: [{ url }] } o { image: { url } }\n finalUrl = imageData.images?.[0]?.url\n || imageData.image?.url\n || null;\n} else if (model === 'ideogram') {\n // Ideogram devuelve: { data: [{ url }] }\n finalUrl = imageData.data?.[0]?.url || null;\n} else if (model === 'custom') {\n finalUrl = content.image_url;\n}\n\nif (!finalUrl) {\n console.warn('\u26a0 No se pudo extraer URL de imagen. Respuesta:', JSON.stringify(imageData).substring(0, 300));\n}\n\nreturn [{ json: { ...content, final_image_url: finalUrl, image_provider_response: imageData } }];"
},
"id": "normalize-image",
"name": "\ud83d\udd17 Normalizar URL imagen",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1520,
448
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.SUPABASE_URL }}/rest/v1/content_sessions",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $env.SUPABASE_ANON_KEY }}"
},
{
"name": "Authorization",
"value": "=Bearer {{ $env.SUPABASE_ANON_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Prefer",
"value": "return=representation"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ session_id: 'propulsar_' + Date.now(), approval_number: $json.approval_number, topic: $json.topic, type: $json.type, angle: $json.angle || null, platforms: $json.platforms || [], image_model: $json.image_model, image_url: $json.image_url || null, final_image_url: $json.final_image_url, instagram_caption: ($json.instagram && $json.instagram.caption) || null, facebook_caption: ($json.facebook && $json.facebook.caption) || null, status: 'pending', publish_at: $json.publish_at || 'now' }) }}",
"options": {
"response": {
"response": {
"responseFormat": "json"
}
}
}
},
"id": "save-session-supabase",
"name": "\ud83d\udcbe Guardar sesi\u00f3n Supabase",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1632,
600
],
"notes": "Inserts pending approval session into content_sessions (flat schema \u2014 instagram_caption/facebook_caption strings, not nested). Built as HTTP Request (not Code) because n8n task runner sandbox blocks require('https'), global fetch, and $helpers.httpRequest. Response = array of the inserted row."
},
{
"parameters": {
"mode": "manual",
"duplicateItem": false,
"assignments": {
"assignments": [
{
"id": "a1",
"name": "topic",
"type": "string",
"value": "={{ $('\ud83d\udd17 Normalizar URL imagen').item.json.topic }}"
},
{
"id": "a2",
"name": "type",
"type": "string",
"value": "={{ $('\ud83d\udd17 Normalizar URL imagen').item.json.type }}"
},
{
"id": "a3",
"name": "angle",
"type": "string",
"value": "={{ $('\ud83d\udd17 Normalizar URL imagen').item.json.angle || '' }}"
},
{
"id": "a4",
"name": "platforms",
"type": "array",
"value": "={{ $('\ud83d\udd17 Normalizar URL imagen').item.json.platforms }}"
},
{
"id": "a5",
"name": "image_model",
"type": "string",
"value": "={{ $('\ud83d\udd17 Normalizar URL imagen').item.json.image_model }}"
},
{
"id": "a6",
"name": "final_image_url",
"type": "string",
"value": "={{ $('\ud83d\udd17 Normalizar URL imagen').item.json.final_image_url }}"
},
{
"id": "a7",
"name": "approval_number",
"type": "string",
"value": "={{ $('\ud83d\udd17 Normalizar URL imagen').item.json.approval_number }}"
},
{
"id": "a8",
"name": "instagram",
"type": "object",
"value": "={{ $('\ud83d\udd17 Normalizar URL imagen').item.json.instagram }}"
},
{
"id": "a9",
"name": "facebook",
"type": "object",
"value": "={{ $('\ud83d\udd17 Normalizar URL imagen').item.json.facebook }}"
},
{
"id": "a10",
"name": "session_id",
"type": "string",
"value": "={{ $json.session_id }}"
},
{
"id": "a11",
"name": "supabase_row_id",
"type": "string",
"value": "={{ $json.id }}"
}
]
},
"includeOtherFields": false,
"options": {}
},
"id": "reattach-session-data",
"name": "\ud83d\udd17 Re-attach session data",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
1744,
600
],
"notes": "Supabase HTTP Request replaced the item with the inserted row (flat schema \u2014 no nested instagram/facebook objects). Re-attach the pre-insert data via cross-ref to Normalizar URL imagen so downstream nodes (Preparar mensaje WA, etc.) keep working with the nested object shape. Adds session_id + supabase_row_id from the insert response."
},
{
"parameters": {
"jsCode": "// Handle single-post and carousel \u2014 read from correct upstream node\nlet d;\ntry {\n d = $('\ud83d\uddc2\ufe0f Collect Image URLs').first().json;\n} catch(e1) {\n try {\n d = $('\ud83d\udd17 Normalizar URL imagen').first().json;\n } catch(e2) {\n d = $input.first().json;\n }\n}\n\nconst isCarousel = d.format === 'carousel';\n\nconst typeEmoji = { educational:'\ud83d\udcda', authority:'\ud83c\udfa4', case_study:'\ud83c\udfc6' };\nconst typeLabel = { educational:'Educativo', authority:'Autoridad', case_study:'Caso de \u00e9xito' };\nconst modelInfo = {\n flux: '\u26a1 Flux 2 Pro',\n ideogram: '\ud83d\udd24 Ideogram v3',\n nanoBanana: '\ud83c\udf4c Nano Banana Pro',\n custom: '\ud83d\udcce Imagen propia',\n};\n\nconst igPreview = d.instagram?.caption?.substring(0, 280) || 'N/A';\nconst fbPreview = d.facebook?.caption?.substring(0, 180) || 'N/A';\nconst igDots = (d.instagram?.caption?.length || 0) > 280 ? '...' : '';\nconst fbDots = (d.facebook?.caption?.length || 0) > 180 ? '...' : '';\n\nconst imageStatus = isCarousel\n ? `\ud83c\udfa0 ${d.image_urls?.length || 0} im\u00e1genes generadas (carrusel)`\n : `\ud83d\uddbc\ufe0f ${d.final_image_url ? '\u2705 Imagen generada' : '\u26a0\ufe0f Sin imagen'}`;\n\nconst formatLine = isCarousel\n ? `\\n\ud83c\udfa0 *Formato:* Carrusel (${d.num_images} slides)`\n : '';\n\nconst msg = `\ud83d\ude80 *PROPULSAR CONTENT ENGINE*\n\n${typeEmoji[d.type] || '\ud83d\udcdd'} *Tipo:* ${typeLabel[d.type] || d.type}\n\ud83d\udccc *Tema:* ${d.topic}\n${d.angle ? `\ud83c\udfaf *\u00c1ngulo:* ${d.angle}\\n` : ''}\ud83d\udcf1 *Redes:* ${(d.platforms || []).join(', ')}\n\ud83d\uddbc\ufe0f *Imagen:* ${modelInfo[d.image_model] || d.image_model}${formatLine}\n\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\ud83d\udcf8 *INSTAGRAM:*\n${igPreview}${igDots}\n\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\ud83d\udc65 *FACEBOOK:*\n${fbPreview}${fbDots}\n\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n${imageStatus}\n\n*\u00bfPublicar?*\n\u2705 *SI* para publicar\n\u274c *NO* para cancelar`;\n\nreturn [{ json: { ...d, whatsapp_message: msg, session_id: `propulsar_${Date.now()}` } }];"
},
"id": "prepare-whatsapp",
"name": "\ud83d\udcf1 Preparar mensaje WA",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1744,
448
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.ycloud.com/v2/whatsapp/messages",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-API-Key",
"value": "={{ $env.YCLOUD_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ \"from\": $env.YCLOUD_WHATSAPP_NUMBER, \"to\": $json.approval_number, \"type\": \"text\", \"text\": { \"body\": $json.whatsapp_message } }) }}",
"options": {}
},
"id": "send-whatsapp",
"name": "\ud83d\udce4 Enviar WhatsApp",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1968,
448
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "propulsar-whatsapp-reply",
"responseMode": "responseNode",
"options": {}
},
"id": "webhook-reply",
"name": "\ud83d\udce8 Webhook \u2014 Reply WA",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
2192,
448
],
"notes": "Configurar esta URL en YCloud \u2192 Webhooks \u2192 Incoming Messages"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={ \"status\": \"ok\" }",
"options": {}
},
"id": "respond-ycloud",
"name": "\u2705 Responder YCloud",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
2400,
288
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.body?.whatsappInboundMessage?.text?.body?.trim().toUpperCase() }}",
"value2": "SI"
}
]
}
},
"id": "check-approval",
"name": "\u2705 \u00bfAprobado?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
2400,
464
]
},
{
"parameters": {
"method": "GET",
"url": "={{ $env.SUPABASE_URL }}/rest/v1/content_sessions",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $env.SUPABASE_ANON_KEY }}"
},
{
"name": "Authorization",
"value": "=Bearer {{ $env.SUPABASE_ANON_KEY }}"
},
{
"name": "Accept",
"value": "application/vnd.pgrst.object+json"
}
]
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "approval_number",
"value": "=eq.{{ $json.body.whatsappInboundMessage.from }}"
},
{
"name": "status",
"value": "eq.pending"
},
{
"name": "order",
"value": "created_at.desc"
},
{
"name": "limit",
"value": "1"
}
]
},
"options": {
"response": {
"response": {
"responseFormat": "json"
}
}
}
},
"id": "retrieve-session",
"name": "\ud83d\udd0d Recuperar sesi\u00f3n Supabase",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2624,
384
],
"notes": "GET pending session from content_sessions, filtered by approval_number = phone + status=pending. Uses PostgREST Accept header 'application/vnd.pgrst.object+json' so response is a SINGLE object (not array), giving downstream nodes the flat session directly. Empty result \u2192 406 \u2192 execution fails with clear error (same as the old Code node behavior). Built as HTTP Request because n8n task runner sandbox blocks every HTTP helper inside Code nodes."
},
{
"parameters": {
"jsCode": "// Normalize both single-post and carousel briefs into the sub-workflow's expected input shape.\n// Single post: $json has final_image_url (string).\n// Carousel: $json has image_urls (array of strings) and num_images.\n//\n// Output shape for Execute Workflow:\n// { image_urls: [{ index, url }], post_id, approval_number }\n\nconst data = $input.first().json;\nconst format = data.format || 'single';\n\nlet imageUrls = [];\nif (format === 'carousel' && Array.isArray(data.image_urls)) {\n imageUrls = data.image_urls.map((url, i) => ({ index: i + 1, url }));\n} else if (data.final_image_url) {\n imageUrls = [{ index: 1, url: data.final_image_url }];\n} else {\n throw new Error('Prep Re-host Input: no image URL found on approved post (neither final_image_url nor image_urls)');\n}\n\n// post_id: derive from session/timestamp. Prefer an existing session_id if present (set by Recuperar sesion Supabase).\nconst postId = data.session_id || ('propulsar_' + Date.now());\n\n// IGPUB-06: Re-extract hashtags from the stored instagram_caption (Supabase stores the original with hashtags).\n// The content-generation path runs extraction for WA preview display only; the approval path needs\n// hashtag_block derived here so it threads through Merge Rehost Output to the IG comment node.\nconst igCaption = data.instagram_caption || '';\nconst captionLines = igCaption.split('\\n');\nconst hashtagLines = captionLines.filter(l => l.trim().startsWith('#'));\nconst cleanLines = captionLines.filter(l => !l.trim().startsWith('#'));\nconst cleanCaption = cleanLines.join('\\n').trimEnd();\nconst hashtagBlock = hashtagLines.join(' ').trim();\n\nreturn [{\n json: {\n // Pass ALL upstream fields through so downstream nodes (Sheets Log, future Meta publish)\n // still have access to topic, type, angle, instagram/facebook text, etc.\n ...data,\n // Clean caption (no hashtag lines) \u2014 used by IG Create Container\n instagram_caption: cleanCaption,\n // hashtag_block threaded to Merge Rehost Output \u2192 IG comment node\n hashtag_block: hashtagBlock,\n // Plus the sub-workflow input fields:\n image_urls: imageUrls,\n post_id: postId,\n approval_number: data.approval_number\n }\n}];",
"mode": "runOnceForAllItems"
},
"id": "prep-rehost-input",
"name": "\ud83d\udd27 Prep Re-host Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3060,
380
],
"notes": "Normalizes single-post (final_image_url) into the sub-workflow input shape. Phase 7: carousel guard removed \u2014 carousel path now handled upstream by \ud83d\udd00 \u00bfFormato Carrusel?. Passes all other fields through so downstream nodes retain topic/type/angle/text."
},
{
"parameters": {
"source": "database",
"workflowId": {
"__rl": true,
"mode": "list",
"value": "BIaG266Q6AZpv4Sq",
"cachedResultName": "Re-host Images to Azure Blob"
},
"workflowInputs": {
"mappingMode": "passthrough"
},
"options": {
"waitForSubWorkflow": true
}
},
"id": "execute-rehost-subflow",
"name": "\ud83d\udd01 Re-host Images",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
3280,
380
],
"notes": "Calls the Re-host Images sub-workflow (n8n workflow ID BIaG266Q6AZpv4Sq). workflowInputs.mappingMode MUST be 'passthrough' because the sub-workflow trigger uses inputSource: passthrough \u2014 using 'defineBelow' with an empty value would send empty input and the sub-workflow's Explode node would throw. Output: { blob_urls, post_id }. NOTE: Execute Workflow replaces the item with the sub-workflow output, so downstream nodes no longer see topic/type/angle/etc. \u2014 that's why we need the '\ud83d\udd17 Merge Rehost Output' node next. If sub-workflow throws StopAndError, this node fails and the main execution aborts \u2014 Google Sheets Log and any Meta publish nodes will NOT run."
},
{
"parameters": {
"mode": "manual",
"duplicateItem": false,
"assignments": {
"assignments": [
{
"id": "merge-blob-urls",
"name": "blob_urls",
"type": "array",
"value": "={{ $json.blob_urls }}"
},
{
"id": "merge-post-id",
"name": "post_id",
"type": "string",
"value": "={{ $json.post_id }}"
},
{
"id": "merge-topic",
"name": "topic",
"type": "string",
"value": "={{ $('\ud83d\udd27 Prep Re-host Input').item.json.topic }}"
},
{
"id": "merge-type",
"name": "type",
"type": "string",
"value": "={{ $('\ud83d\udd27 Prep Re-host Input').item.json.type }}"
},
{
"id": "merge-angle",
"name": "angle",
"type": "string",
"value": "={{ $('\ud83d\udd27 Prep Re-host Input').item.json.angle }}"
},
{
"id": "merge-platforms",
"name": "platforms",
"type": "array",
"value": "={{ $('\ud83d\udd27 Prep Re-host Input').item.json.platforms }}"
},
{
"id": "merge-image-model",
"name": "image_model",
"type": "string",
"value": "={{ $('\ud83d\udd27 Prep Re-host Input').item.json.image_model }}"
},
{
"id": "merge-instagram-caption",
"name": "instagram_caption",
"type": "string",
"value": "={{ $('\ud83d\udd27 Prep Re-host Input').item.json.instagram_caption }}"
},
{
"id": "merge-facebook-caption",
"name": "facebook_caption",
"type": "string",
"value": "={{ $('\ud83d\udd27 Prep Re-host Input').item.json.facebook_caption }}"
},
{
"id": "merge-final-image",
"name": "final_image_url",
"type": "string",
"value": "={{ $('\ud83d\udd27 Prep Re-host Input').item.json.final_image_url }}"
},
{
"id": "merge-format",
"name": "format",
"type": "string",
"value": "={{ $('\ud83d\udd27 Prep Re-host Input').item.json.format }}"
},
{
"id": "merge-approval-number",
"name": "approval_number",
"type": "string",
"value": "={{ $('\ud83d\udd27 Prep Re-host Input').item.json.approval_number }}"
},
{
"id": "merge-hashtag-block",
"name": "hashtag_block",
"type": "string",
"value": "={{ $('\ud83d\udd27 Prep Re-host Input').item.json.hashtag_block || '' }}"
}
]
},
"options": {}
},
"id": "merge-rehost-output",
"name": "\ud83d\udd17 Merge Rehost Output",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
3500,
380
],
"notes": "Re-attaches original session metadata (from \ud83d\udd27 Prep Re-host Input) to the sub-workflow output (blob_urls, post_id). Without this, Execute Workflow replaces the item with only { blob_urls, post_id }, and Google Sheets Log would write blank columns. Uses a Set node (no jsCode) to avoid any env concern. If the Wizard brief ever adds new session fields, extend this node's assignments array accordingly."
},
{
"parameters": {
"jsCode": "console.log('Rechazado:', $json.body?.whatsappInboundMessage?.from, new Date().toISOString());\nreturn [{ json: { status:'rejected', ts: new Date().toISOString() } }];"
},
"id": "log-rejected",
"name": "\u274c Loguear rechazo",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2624,
560
]
},
{
"parameters": {
"jsCode": "const carousel = $('\ud83d\udd27 Parsear prompts carrusel').first().json;\nconst loopItems = $input.all();\n\n// IMG-02: Normalize and validate all image URLs\nconst imageUrls = loopItems.map(item => {\n const url = item.json.data?.[0]?.url || null;\n if (url && typeof url === 'string' && url.startsWith('https://')) {\n return url; // Keep full URL with auth params (exp, sig)\n }\n return null;\n}).filter(Boolean);\n\nif (imageUrls.length !== carousel.num_images) {\n throw new Error('Se generaron ' + imageUrls.length + ' imagenes, se esperaban ' + carousel.num_images + '. Faltan slides.');\n}\n\nreturn [{\n json: {\n instagram: carousel.instagram,\n facebook: carousel.facebook,\n topic: carousel.topic,\n type: carousel.type,\n angle: carousel.angle || null,\n platforms: carousel.platforms,\n image_model: 'ideogram',\n approval_number: carousel.approval_number,\n timestamp: carousel.timestamp,\n publish_at: carousel.publish_at || 'now',\n format: 'carousel',\n num_images: carousel.num_images,\n image_urls: imageUrls,\n final_image_url: imageUrls[0] || null,\n }\n}];"
},
"id": "collect-carousel-urls",
"name": "\ud83d\uddc2\ufe0f Collect Image URLs",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1744,
528
]
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\nconst urls = data.image_urls;\n\nreturn urls.map((url, idx) => ({\n json: {\n current_image_url: url,\n slide_index: idx + 1,\n num_images: data.num_images,\n approval_number: data.approval_number,\n _carousel_data: data,\n }\n}));"
},
"id": "split-wa-urls",
"name": "\ud83d\udce8 Split URLs WA",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1968,
528
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.ycloud.com/v2/whatsapp/messages",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-API-Key",
"value": "={{ $env.YCLOUD_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ \"from\": $env.YCLOUD_WHATSAPP_NUMBER, \"to\": $json.approval_number, \"type\": \"image\", \"image\": { \"link\": $json.current_image_url, \"caption\": \"Slide \" + $json.slide_index + \" de \" + $json.num_images } }) }}",
"options": {}
},
"id": "send-wa-image",
"name": "\ud83d\udce4 Enviar imagen WA",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
2400,
624
],
"notes": "YCloud sendDirectly \u2014 sends one carousel image per loop iteration"
},
{
"parameters": {
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $env.GOOGLE_SHEETS_ID }}"
},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "Log"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"Fecha": "={{ new Date().toISOString() }}",
"Tema": "={{ $('\ud83d\udd17 Merge Rehost Output').item.json.topic }}",
"Tipo": "={{ $('\ud83d\udd17 Merge Rehost Output').item.json.type }}",
"Angulo": "={{ $('\ud83d\udd17 Merge Rehost Output').item.json.angle || '' }}",
"Plataformas": "={{ Array.isArray($('\ud83d\udd17 Merge Rehost Output').item.json.platforms) ? $('\ud83d\udd17 Merge Rehost Output').item.json.platforms.join(', ') : '' }}",
"Modelo_Imagen": "={{ $('\ud83d\udd17 Merge Rehost Output').item.json.image_model }}",
"Imagen_URL": "={{ ($('\ud83d\udd17 Merge Rehost Output').item.json.blob_urls && $('\ud83d\udd17 Merge Rehost Output').item.json.blob_urls[0] && $('\ud83d\udd17 Merge Rehost Output').item.json.blob_urls[0].url) || $('\ud83d\udd17 Merge Rehost Output').item.json.final_image_url || '' }}",
"Estado": "Publicado",
"IG_URL": "={{ $('\ud83d\udd17 IG: Get Permalink').item.json.permalink }}",
"FB_URL": "={{ 'https://www.facebook.com/' + $('\ud83c\udf10 FB: Publish Photo').item.json.post_id }}",
"Publicado_En": "={{ new Date().toISOString() }}",
"Publish_Status": "success",
"Error_Msg": ""
},
"schema": [
{
"id": "Fecha",
"displayName": "Fecha",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Tema",
"displayName": "Tema",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Tipo",
"displayName": "Tipo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Angulo",
"displayName": "Angulo",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Plataformas",
"displayName": "Plataformas",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Modelo_Imagen",
"displayName": "Modelo_Imagen",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Imagen_URL",
"displayName": "Imagen_URL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Estado",
"displayName": "Estado",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "IG_URL",
"displayName": "IG_URL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "FB_URL",
"displayName": "FB_URL",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Publicado_En",
"displayName": "Publicado_En",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Publish_Status",
"displayName": "Publish_Status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "Error_Msg",
"displayName": "Error_Msg",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
}
]
},
"operation": "append",
"resource": "sheet"
},
"id": "log-sheets",
"name": "\ud83d\udcca Google Sheets Log",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.4,
"position": [
5480,
480
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.body.format }}",
"value2": "carousel"
}
]
}
},
"id": "carousel-check",
"name": "\ud83d\udd00 \u00bfCarrusel?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
432,
528
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.ycloud.com/v2/whatsapp/messages",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-API-Key",
"value": "={{ $env.YCLOUD_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ \"from\": $env.YCLOUD_WHATSAPP_NUMBER, \"to\": $json.approval_number, \"type\": \"image\", \"image\": { \"link\": $json.final_image_url, \"caption\": \"Preview \u2014 \" + $json.topic } }) }}",
"options": {}
},
"id": "send-single-image",
"name": "\ud83d\udce4 Enviar preview imagen",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1200,
464
],
"notes": "Single-post: sends image preview before text message"
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.image_model }}",
"value2": "ideogram"
}
]
}
},
"id": "router-if-ideogram",
"name": "\ud83c\udfa8 \u00bfIdeogram?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
1088,
448
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.image_model }}",
"value2": "nanoBanana"
}
]
}
},
"id": "router-if-nano",
"name": "\ud83c\udfa8 \u00bfNanoBanana?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
1280,
544
]
},
{
"parameters": {
"method": "POST",
"url": "=https://graph.facebook.com/v22.0/{{ $env.INSTAGRAM_ACCOUNT_ID }}/media",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ image_url: $json.blob_urls[0].url, caption: $json.instagram_caption, access_token: $env.META_PAGE_TOKEN }) }}",
"options": {}
},
"id": "ig-create-container",
"name": "\ud83d\udce4 IG: Create Container",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
3940,
480
],
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 3000,
"onError": "continueErrorOutput",
"notes": "Creates IG media container. Idempotent \u2014 retry is safe. Reads blob_urls[0].url (permanent Azure URL from Phase 4) and instagram.caption (from GPT-4o). Output: { id: <container_id> }. Common errors: 9004/2207052 (image_url unreachable), 190 (token expired)."
},
{
"parameters": {
"amount": 30,
"unit": "seconds"
},
"id": "wait-container-ready",
"name": "\u23f3 Wait 30s (container ready)",
"type": "n8n-nodes-base.wait",
"typeVersion": 1,
"position": [
4160,
480
],
"notes": "Fixed 30s wait per IGPUB-03. Container typically reaches FINISHED in 2-5s for single photos, but 30s gives a safe margin. Phase 5 Success Criterion 5 requires this gap be visible in execution trace. Polling (HARD-03) deferred to v2."
},
{
"parameters": {
"method": "POST",
"url": "=https://graph.facebook.com/v22.0/{{ $env.INSTAGRAM_ACCOUNT_ID }}/media_publish",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ creation_id: $json.id, access_token: $env.META_PAGE_TOKEN }) }}",
"options": {}
},
"id": "ig-media-publish",
"name": "\ud83d\ude80 IG: media_publish",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
4380,
480
],
"retryOnFail": false,
"maxTries": 1,
"waitBetweenTries": 0,
"onError": "continueErrorOutput",
"notes": "CRITICAL: Retry DISABLED (IGPUB-04, ERR-02). media_publish is NOT idempotent \u2014 a retry after a timeout where Meta already processed the request creates a duplicate live IG post. If this node fails, execution aborts \u2014 Felix manually verifies IG profile and n8n logs. Error 9007/2207027 means container not ready (increase Wait)."
},
{
"parameters": {
"method": "GET",
"url": "=https://graph.facebook.com/v22.0/{{ $json.id }}",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "fields",
"value": "permalink"
},
{
"name": "access_token",
"value": "={{ $env.META_PAGE_TOKEN }}"
}
]
},
"options": {}
},
"id": "ig-get-permalink",
"name": "\ud83d\udd17 IG: Get Permalink",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
4820,
480
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 1000,
"onError": "continueErrorOutput",
"notes": "GETs the published IG post's permalink (IGPUB-05). Input: { id: <media_id> } from media_publish. Output: { id, permalink }. Idempotent GET \u2014 retry is safe."
},
{
"parameters": {
"method": "POST",
"url": "=https://graph.facebook.com/v22.0/{{ $('\ud83d\ude80 IG: media_publish').item.json.id }}/comments",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ message: $('\ud83d\udd17 Merge Rehost Output').item.json.hashtag_block, access_token: $env.META_PAGE_TOKEN }) }}",
"options": {}
},
"id": "ig-post-hashtag-comment",
"name": "\ud83d\udcac IG: Post Hashtag Comment",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
4600,
480
],
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 2000,
"onError": "continueErrorOutput",
"notes": "IGPUB-06: Posts hashtags as first comment on the published IG post. Non-blocking \u2014 if comment fails, post is already live. onError=continueErrorOutput so publish chain continues. URL uses explicit cross-ref to media_publish to avoid Set v3.0 fan-out data-drop. message = hashtag_block from Merge Rehost Output."
},
{
"parameters": {
"method": "POST",
"url": "https://api.ycloud.com/v2/whatsapp/messages",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "X-API-Key",
"value": "={{ $env.YCLOUD_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ from: $env.YCLOUD_WHATSAPP_NUMBER, to: $('\ud83d\udd17 Merge Rehost Output').item.json.approval_number, type: 'text', text: { body: '\u2713 Publicado en Instagram y Facebook\\n\\nTema: ' + $('\ud83d\udd17 Merge Rehost Output').item.json.topic + '\\nInstagram: ' + $('\ud83d\udd17 IG: Get Permalink').item.json.permalink + '\\nFacebook: https://www.facebook.com/' + $('\ud83c\udf10 FB: Publish Photo').item.json.post_id + '\\nHora: ' + new Date().toISOString() } }) }}",
"options": {}
},
"id": "notify-wa-success",
"name": "\u2705 Notify WhatsApp Success",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
5260,
480
],
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 2000,
"onError": "stopWorkflow",
"notes": "Sends WhatsApp text with 'Publicado' + IG permalink + timestamp (NOTIF-01). Cross-refs Merge Rehost Output for topic + approval_number because $json at this point is { id, permalink } from the permalink GET. Uses JSON.stringify({...}) wrapper per project pattern from commit effab36 to avoid quote-escaping issues."
},
{
"parameters": {
"method": "POST",
"url": "=https://graph.facebook.com/v22.0/{{ $env.FACEBOOK_PAGE_ID }}/photos",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ url: $('\ud83d\udd17 Merge Rehost Output').item.json.blob_urls[0].url, message: $('\ud83d\udd17 Merge Rehost Output').item.json.facebook_caption || $('\ud83d\udd17 Merge Rehost Output').item.json.instagram_caption || '', access_token: $env.META_PAGE_TOKEN }) }}",
"options": {}
},
"id": "fb-publish-photo",
"name": "\ud83c\udf10 FB: Publish Photo",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": false,
"maxTries": 1,
"waitBetweenTries": 0,
"onError": "continueErrorOutput",
"position": [
5040,
480
],
"notes": "CRITICAL: Retry DISABLED (FBPUB-01, ERR-02 equivalent). POST /{PAGE_ID}/photos is NOT idempotent \u2014 retry after timeout where Meta already processed creates a duplicate live FB post. Input: blob_urls[0].url from Merge Rehost Output. Output: { id: <photo_id>, post_id: '<page_id>_<photo_id>' }. Post_id used to construct FB_URL as 'https://www.facebook.com/' + post_id."
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.format }}",
"value2": "carousel"
}
]
}
},
"id": "format-carousel-branch",
"name": "\ud83d\udd00 \u00bfFormato Carrusel?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
3720,
380
],
"notes": "Phase 7: Routes carousel briefs (format=carousel) to the carousel publish chain (TRUE output 0 \u2014 connected by Plan 02). Single-post briefs (format!=carousel) continue to the existing IG publish chain (FALSE output 1). Uses IF typeVersion 1 \u2014 v2/Switch v3 broken in n8n 2.14.2."
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.SUPABASE_URL }}/rest/v1/content_sessions",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "apikey",
"value": "={{ $env.SUPABASE_ANON_KEY }}"
},
{
"name": "Authorization",
"value": "=Bearer {{ $env.SUPABASE_ANON_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
},
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googleSheetsOAuth2ApiopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Propulsar — Content Engine v3. Uses openAi, httpRequest, googleSheets. Webhook trigger; 73 nodes.
Source: https://github.com/allendefelixGHC/Creador-Contenido-Redes/blob/383b72e8ed5cd681771df5edb9fb0cc033661df7/n8n/workflow.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Instantly map all internal URLs, perform AI-powered (ChatGPT) analysis, and deliver results in HTML via webhook, Google Sheets, or email. All from your own n8n instance!
Watch on Youtube▶️
This workflow is perfect for marketing agencies, SEO consultants, and growth specialists who need to scale personalized outreach without spending hours on manual research.
This workflow automates the creation of Journal Entries in SAP Business One (SAP B1). Depending on the source of the input data, it dynamically transforms and sends accounting records in the appropria
How it works: Send notes from Obsidian via Webhook to start the audio conversion OpenAI converts your text to natural-sounding audio and generates episode descriptions Audio files are stored in Cloudi