{
  "nodes": [
    {
      "id": "a5ef9b3a-452c-4ccb-b6d9-203721828019",
      "name": "Daily at Noon",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -864,
        -48
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 12
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "b80adf60-3946-46a0-aaeb-a5bccb31cbf0",
      "name": "Generate Carousel Content",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        -144,
        368
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-3-flash-preview",
          "cachedResultName": "models/gemini-3-flash-preview"
        },
        "options": {
          "temperature": 0.9,
          "thinkingBudget": 0,
          "maxOutputTokens": 2048
        },
        "messages": {
          "values": [
            {
              "content": "=You are the Lead Content Strategist for [BRAND_NAME], an elite tech/AI/automation brand.\n\nGenerate an Instagram carousel about a fascinating Did You Know? style tech topic.\nPick ONE from: AI breakthroughs, automation secrets, DevOps tricks, cybersecurity facts, mind-blowing tech history, or futuristic tech predictions.\n\nThe carousel MUST be exactly 6 slides. Output ONLY valid JSON (no markdown, no code fences) Important, dont use this topic's, since they alrdy been used:{{ $json.history }}\n\n{\n  \"topic\": \"Main topic title (max 40 chars)\",\n  \"slides\": [\n    {\"num\": 1, \"type\": \"cover\", \"headline\": \"Bold hook question or shocking statement (max 60 chars)\", \"subtext\": \"Short teaser line (max 30 chars)\"},\n    {\"num\": 2, \"type\": \"fact\", \"label\": \"DID YOU KNOW?\", \"body\": \"Mind-blowing fact #1. Must be genuinely surprising. (max 120 chars)\"},\n    {\"num\": 3, \"type\": \"fact\", \"label\": \"THE REALITY\", \"body\": \"Deeper insight that builds on fact #1. (max 120 chars)\"},\n    {\"num\": 4, \"type\": \"fact\", \"label\": \"WHY IT MATTERS\", \"body\": \"Real-world impact or consequence. (max 120 chars)\"},\n    {\"num\": 5, \"type\": \"fact\", \"label\": \"PRO TIP\", \"body\": \"Actionable takeaway the reader can use today. (max 120 chars)\"},\n    {\"num\": 6, \"type\": \"cta\", \"headline\": \"Want more byte-sized tech?\", \"subtext\": \"Follow @[HANDLE]\"}\n  ],\n  \"caption\": \"Write a punchy Instagram caption: emoji hook + 2 sentences of insight + a question to drive comments + line break + 5 niche hashtags like xxxxxxx, xxxx , xxxxx\n}\n\nQuality rules:\n- Each fact MUST be genuinely surprising and technically accurate\n- Facts should build on each other telling a mini-story\n- Tone: Smart, slightly witty, insider knowledge vibe\n- Make it feel like forbidden knowledge most people dont have access to\n- Topics should be niche enough to impress engineers but accessible enough for curious beginners"
            }
          ]
        },
        "jsonOutput": true,
        "builtInTools": {
          "urlContext": false,
          "googleSearch": true,
          "codeExecution": false
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "e8044e16-cfc8-4bf1-949f-55d4e4f263ce",
      "name": "Build Slide Image Prompts",
      "type": "n8n-nodes-base.code",
      "position": [
        208,
        368
      ],
      "parameters": {
        "jsCode": "const raw = items[0].json;\nlet content;\n\ntry {\n  if (typeof raw === 'object' && raw.slides) {\n    content = raw;\n  } else if (raw.content && raw.content.parts && Array.isArray(raw.content.parts)) {\n    let jsonText = '';\n    for (const part of raw.content.parts) {\n      if (part.thought) continue;\n      if (part.text) jsonText = part.text;\n    }\n    jsonText = jsonText.replace(/```json\\s*/gi, '').replace(/```\\s*/g, '').trim();\n    content = JSON.parse(jsonText);\n  } else if (typeof raw === 'string') {\n    const cleaned = raw.replace(/```json\\s*/gi, '').replace(/```\\s*/g, '').trim();\n    content = JSON.parse(cleaned);\n  } else if (typeof raw.content === 'string') {\n    const cleaned = raw.content.replace(/```json\\s*/gi, '').replace(/```\\s*/g, '').trim();\n    content = JSON.parse(cleaned);\n  } else if (raw.text && typeof raw.text === 'string') {\n    const cleaned = raw.text.replace(/```json\\s*/gi, '').replace(/```\\s*/g, '').trim();\n    content = JSON.parse(cleaned);\n  } else {\n    const str = JSON.stringify(raw);\n    const slidesMatch = str.match(/\"slides\"\\s*:\\s*\\[/);\n    if (slidesMatch) {\n      let startIdx = str.indexOf(slidesMatch[0]);\n      for (let i = startIdx; i >= 0; i--) {\n        if (str[i] === '{') { startIdx = i; break; }\n      }\n      let depth = 0, endIdx = startIdx;\n      for (let i = startIdx; i < str.length; i++) {\n        if (str[i] === '{') depth++;\n        if (str[i] === '}') { depth--; if (depth === 0) { endIdx = i + 1; break; } }\n      }\n      content = JSON.parse(str.substring(startIdx, endIdx));\n    } else {\n      throw new Error('No slides data found in input');\n    }\n  }\n} catch (e) {\n  throw new Error('Failed to parse carousel content: ' + e.message + '. Raw keys: ' + Object.keys(raw));\n}\n\nif (!content || !content.slides || content.slides.length === 0) {\n  throw new Error('No slides found. Parsed content keys: ' + (content ? Object.keys(content) : 'null'));\n}\n\nconst avatar = 'a small 8-bit pixel art avatar character with neutral styling, dark hair, expressive eyes, slight confident smirk, wearing a dark hoodie with a simple emblem, anime pixel-art style';\n\nconst brandBase = 'Create a perfectly square 1080x1080 pixel Instagram carousel slide. ABSOLUTE RULES: Pure solid black (#000000) background filling the entire canvas. ALL text rendered in clean white (#FFFFFF) sans-serif font. Accent colors ONLY for thin decorative elements: cyber-purple (#7B2FBE) and electric blue (#00D4FF). Design must be ultra-clean, modern, minimal. NO photographs, NO 3D renders, NO gradients on background. Only flat graphic design, clean typography, and subtle geometric accents (thin neon lines, small dots, angular corner decorations). ';\n\nconst slides = content.slides;\nconst output = [];\n\nfor (const slide of slides) {\n  let prompt = brandBase;\n\n  if (slide.type === 'cover') {\n    prompt += 'Include ' + avatar + ' positioned in the bottom-right area, sized about 25% of the slide, looking excited and pointing upward. ';\n    prompt += 'LAYOUT: Large bold white headline text ' + (slide.headline || '') + ' centered in the upper 60% of the slide. ';\n    prompt += 'Smaller text below in electric blue: ' + (slide.subtext || '') + '. ';\n    prompt += 'A thin glowing purple (#7B2FBE) accent line under the headline. Very thin purple border around entire slide. ';\n    prompt += 'Tiny @[HANDLE] watermark at bottom-left in dark gray. Subtle dot grid pattern barely visible in corners.';\n  } else if (slide.type === 'cta') {\n    prompt += 'Include ' + avatar + ' centered in the middle of the slide, larger (about 35% of slide), waving with a friendly expression. ';\n    prompt += 'LAYOUT: Bold text ' + (slide.headline || '') + ' at the top in white. ';\n    prompt += 'Below the avatar: ' + (slide.subtext || '') + ' in electric blue (#00D4FF). ';\n    prompt += 'A subtle right-pointing arrow icon (swipe indicator). Glowing thin purple border. Subtle futuristic grid pattern in background.';\n  } else {\n    prompt += 'Include ' + avatar + ' very small (about 10% of slide) in the top-right corner. ';\n    prompt += 'LAYOUT: Label text ' + (slide.label || 'DID YOU KNOW?') + ' at the top in electric blue (#00D4FF), smaller font. ';\n    prompt += 'A thin purple horizontal line separator below the label. ';\n    prompt += 'Main body text: ' + (slide.body || '') + ' in large bold white text, centered vertically in the middle 60% of the slide. ';\n    prompt += 'Slide number ' + (slide.num || '') + '/6 very small at the bottom-center in dark gray. Subtle angular decorations in bottom corners.';\n  }\n\n  output.push({\n    json: {\n      slideNum: slide.num,\n      slideType: slide.type,\n      imagePrompt: prompt,\n      caption: content.caption || '',\n      topic: content.topic || ''\n    }\n  });\n}\n\nreturn output;"
      },
      "typeVersion": 2
    },
    {
      "id": "1adbef92-6c5f-4f68-88b5-75b71912330c",
      "name": "Generate Slide Images",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        480,
        -64
      ],
      "parameters": {
        "prompt": "={{ $json.imagePrompt }}",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-3-pro-image-preview",
          "cachedResultName": "models/gemini-3-pro-image-preview"
        },
        "options": {},
        "resource": "image"
      },
      "typeVersion": 1.1
    },
    {
      "id": "5e7196fe-fdff-4de6-b02b-bdf317dc636f",
      "name": "Prepare Slack Package",
      "type": "n8n-nodes-base.code",
      "position": [
        704,
        -64
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const slideNum = $json.slideNum || ($itemIndex + 1);\nconst caption = $json.caption || '';\nconst topic = $json.topic || '';\n\nconst comment = slideNum === 1\n  ? '\\u{1F4F1} *NEW CAROUSEL READY* \\u2014 ' + topic + '\\n\\n' + caption + '\\n\\n_Slide ' + slideNum + '/6_'\n  : '_Slide ' + slideNum + '/6_';\n\nreturn {\n  json: {\n    ...$json,\n    slackComment: comment,\n    fileName: 'carousel_slide_' + String(slideNum).padStart(2, '0') + '.png'\n  },\n  binary: $binary\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "ce0d2eb2-64f3-460c-b873-395eb837e625",
      "name": "Upload Slides to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        928,
        -64
      ],
      "parameters": {
        "options": {
          "fileName": "={{ $json.fileName }}",
          "channelId": "[SLACK_CHANNEL_ID]",
          "initialComment": "={{ $json.slackComment }}"
        },
        "resource": "file",
        "authentication": "oAuth2"
      },
      "typeVersion": 2.4
    },
    {
      "id": "f6a5d0c7-8db2-420a-8c49-e0c7ae15be2a",
      "name": "Get row(s) in sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -640,
        -48
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "[GOOGLE_SHEET_TAB_URL]",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "[GOOGLE_SHEET_ID]",
          "cachedResultUrl": "[GOOGLE_SHEET_URL]",
          "cachedResultName": "[SHEET_NAME]"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "dc4bc875-93e0-44e8-bbb9-4b0401ecd871",
      "name": "Code in JavaScript",
      "type": "n8n-nodes-base.code",
      "position": [
        -416,
        -48
      ],
      "parameters": {
        "jsCode": "// Map the input items into a single string\n// Note: \"Topic\" must match your Google Sheet column name exactly!\nconst history = items.map(i => i.json.Topic).filter(t => t).join(', ');\n\nreturn { \n  history: history || \"None yet\" \n};"
      },
      "typeVersion": 2
    },
    {
      "id": "9d0cb24d-8a9f-429c-a215-e4bdb6df9c5d",
      "name": "Append row in sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1152,
        -64
      ],
      "parameters": {
        "columns": {
          "value": {
            "Topic": "={{ $('Build Slide Image Prompts').item.json.topic }}"
          },
          "schema": [
            {
              "id": "Topic",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Topic",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "row_number"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "[GOOGLE_SHEET_TAB_URL]",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "[GOOGLE_SHEET_ID]",
          "cachedResultUrl": "[GOOGLE_SHEET_URL]",
          "cachedResultName": "[SHEET_NAME]"
        }
      },
      "executeOnce": true,
      "typeVersion": 4.7
    },
    {
      "id": "7b70adb3-bc64-4b25-ac6c-782cbc999d54",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        464,
        -256
      ],
      "parameters": {
        "color": 4,
        "width": 880,
        "height": 400,
        "content": "### \ud83d\ude80 03: RENDERING & DELIVERY\n**Goal:** Final media output and logging.\n* **Rendering:** Gemini Pro Image creates the final 1080x1080 PNGs.\n* **Slack:** Pushes the carousel as a thread/package for review.\n* **Loop Closure:** Updates the Google Sheet so the AI knows not to repeat this topic."
      },
      "typeVersion": 1
    },
    {
      "id": "5ffddb14-ca81-4b8f-9618-18636f37f7cf",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -752,
        -208
      ],
      "parameters": {
        "color": 6,
        "width": 496,
        "height": 304,
        "content": "### \ud83d\udd0d 01: DATA & CONTEXT\n**Goal:** Ensure content is unique and niche-relevant.\n* **Deduplication:** The workflow reads the Sheet to see what has already been \"done.\"\n* **Prompting:** The history is injected into the AI prompt as \"Negative Context.\""
      },
      "typeVersion": 1
    },
    {
      "id": "3cf87288-e465-44ac-9726-43c2b2c666aa",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -160,
        160
      ],
      "parameters": {
        "width": 528,
        "height": 352,
        "content": "### \u270d\ufe0f 02: THE CREATIVE ENGINE\n**Goal:** Content and Design logic.\n* **Copywriting:** Generates slides with a specific narrative arc (Hook \u2192 Fact \u2192 Reality \u2192 CTA).\n* **Image Logic:** A custom JS bridge translates slide text into detailed visual prompts for the Image model."
      },
      "typeVersion": 1
    },
    {
      "id": "2c2c70a7-6edd-4f35-8dc5-9f0593708f8a",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1248,
        -352
      ],
      "parameters": {
        "width": 352,
        "height": 832,
        "content": "# \ud83d\ude80 AI Carousel Generator\n**Automated Instagram Content Engine**\n\n### \ud83d\udee0\ufe0f HOW IT WORKS\n1. **Trigger:** Fires daily at noon.\n2. **Memory:** Checks a Google Sheet for previously used topics to avoid duplicates.\n3. **Ideation:** Gemini 3 Flash generates a 6-slide carousel structure in JSON format.\n4. **Visuals:** A custom JavaScript engine builds branding-specific image prompts.\n5. **Creation:** Gemini 3 Pro Image generates 6 unique high-fidelity 1080x1080 slides.\n6. **Delivery:** Packages slides and the caption, then sends them to Slack.\n7. **Logging:** Saves the new topic back to Google Sheets.\n\n### \u2699\ufe0f HOW TO SET UP\n* **Google Sheets:** Create a sheet with a column header named `Topic`.\n* **Credentials:** Connect your Google Service Account, Gemini API, and Slack OAuth.\n* **Slack:** Replace `[SLACK_CHANNEL_ID]` with your target channel ID.\n* **Variables:** Update the `[BRAND_NAME]` and `[HANDLE]` in the Gemini prompt.\n\n### \ud83c\udfa8 CUSTOMIZATION\n* **Visual Style:** Edit the `brandBase` constant in the **Build Slide Image Prompts** node to change colors or themes.\n* **Avatar:** Change the `avatar` constant in the code node to match your brand persona.\n* **Schedule:** Change the **Daily at Noon** trigger to your preferred posting frequency."
      },
      "typeVersion": 1
    },
    {
      "id": "9d8cfd69-146a-47b0-96ee-659a6b65e636",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        336,
        384
      ],
      "parameters": {
        "color": 3,
        "width": 256,
        "height": 208,
        "content": "### \u26a0\ufe0f IMPORTANT\n**JSON Parsing:** Gemini occasionally adds markdown code fences (```json). \nThis node contains a robust regex cleaner to prevent the workflow from crashing if the AI returns non-standard formatting."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Daily at Noon": {
      "main": [
        [
          {
            "node": "Get row(s) in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "Generate Carousel Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get row(s) in sheet": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Slide Images": {
      "main": [
        [
          {
            "node": "Prepare Slack Package",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Slack Package": {
      "main": [
        [
          {
            "node": "Upload Slides to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload Slides to Slack": {
      "main": [
        [
          {
            "node": "Append row in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Slide Image Prompts": {
      "main": [
        [
          {
            "node": "Generate Slide Images",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Carousel Content": {
      "main": [
        [
          {
            "node": "Build Slide Image Prompts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}