{
  "nodes": [
    {
      "parameters": {},
      "id": "64ba2f83-adc4-4f1e-b18d-ee635966db83",
      "name": "Run Workflow",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -2320,
        416
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "options": {}
      },
      "id": "ad844669-e4f0-4353-a420-d9ee1d86f05e",
      "name": "Read Topics from Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        -2096,
        416
      ],
      "notes": "Connect your Google account, select your spreadsheet, and make sure you have a column called 'topic' with your descriptions."
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "827bcc8b-0490-4673-9e23-0682ef67148d",
      "name": "Loop Over Topics",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        -1872,
        416
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googlePalmApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({\n  contents: [{ parts: [{ text: `You are an expert AI image prompt engineer. Given this topic: \"${$json.topic}\"\n\nGenerate exactly 5 diverse, detailed image prompts optimized for AI image generation.\n\nEach prompt MUST vary significantly in visual style, framing/composition, mood/lighting, and subject treatment.\n\nEach prompt should be 2-3 sentences. Do not use double quotation marks inside the prompts; use single quotes for any sign text.` }] }],\n  generationConfig: {\n    temperature: 1.0,\n    maxOutputTokens: 8192,\n    responseMimeType: 'application/json',\n    responseSchema: {\n      type: 'ARRAY',\n      items: { type: 'STRING' },\n      minItems: 5,\n      maxItems: 5\n    }\n  }\n}) }}",
        "options": {
          "timeout": 60000
        }
      },
      "id": "f5697177-fd6d-459c-915e-248e7d1d9f6e",
      "name": "Generate 5 Prompt Variations",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -1648,
        272
      ],
      "notes": "Writes 5 varied image prompts per topic. Add your Google Gemini API credential."
    },
    {
      "parameters": {
        "jsCode": "const res = $input.first().json;\nconst parts = res.candidates?.[0]?.content?.parts || [];\nconst responseText = parts.filter(p => p.text && !p.thought).map(p => p.text).join('\\n');\nif (!responseText) throw new Error('No text from Gemini: ' + JSON.stringify(res).slice(0, 300));\n\nconst originalTopic = $('Loop Over Topics').first().json.topic;\n\nlet cleaned = responseText\n  .replace(/```json\\s*/gi, '')\n  .replace(/```/g, '')\n  .replace(/[\\u0000-\\u001F]+/g, ' ')\n  .trim();\nlet prompts;\ntry {\n  prompts = JSON.parse(cleaned);\n} catch (e) {\n  const match = cleaned.match(/\\[[\\s\\S]*\\]/);\n  if (!match) throw new Error('Could not parse prompts: ' + cleaned.slice(0, 300));\n  prompts = JSON.parse(match[0]);\n}\nif (!Array.isArray(prompts) || !prompts.length) throw new Error('Not an array of prompts');\n\nconst safeTopic = String(originalTopic).replace(/[^a-zA-Z0-9\\s]/g, '').replace(/\\s+/g, '_').substring(0, 40).toLowerCase();\n\nreturn prompts.slice(0, 5).map((p, i) => ({\n  json: {\n    topic: originalTopic,\n    promptIndex: i + 1,\n    imagePrompt: typeof p === 'string' ? p : (p.prompt || JSON.stringify(p)),\n    fileName: `${safeTopic}_v${i + 1}.png`,\n    folderName: safeTopic\n  }\n}));"
      },
      "id": "25e8d466-b755-436a-9783-61bc7ffbc384",
      "name": "Parse Into 5 Prompts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -1424,
        272
      ]
    },
    {
      "parameters": {
        "options": {
          "reset": "={{ $prevNode.name === 'Parse Into 5 Prompts' }}"
        }
      },
      "id": "314a91d3-d65a-4e9f-8629-c2a28f171156",
      "name": "Loop Over Prompts",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        -1232,
        272
      ]
    },
    {
      "parameters": {
        "amount": 3
      },
      "id": "2ef66436-a96e-4d3b-9e02-595d95b0a72b",
      "name": "Rate Limit Delay",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        -976,
        224
      ],
      "webhookId": "rate-limit-wait"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-lite-image:generateContent",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googlePalmApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ contents: [{ parts: [{ text: 'Generate an image based on this description. Do not include any text in the image unless specifically described. Create only the image: ' + $json.imagePrompt }] }], generationConfig: { responseModalities: ['IMAGE'] } }) }}",
        "options": {
          "timeout": 60000
        }
      },
      "id": "1c8c7557-2d88-4312-a66f-916f68eb7965",
      "name": "Generate Image with Gemini",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -752,
        224
      ],
      "onError": "continueRegularOutput",
      "notes": "Generates each image. Add your Gemini credential. If Google retires this model, swap the name between models/ and :generateContent."
    },
    {
      "parameters": {
        "jsCode": "// Extract image data from Gemini response\nconst item = $input.first().json;\nconst meta = $('Loop Over Prompts').first().json;\n\n// Check for errors\nif (item.error) {\n  return [{\n    json: {\n      success: false,\n      error: item.error.message || 'Image generation failed',\n      topic: meta.topic,\n      promptIndex: meta.promptIndex,\n      imagePrompt: meta.imagePrompt\n    }\n  }];\n}\n\nconst candidates = item.candidates;\nif (!candidates || !candidates[0]) {\n  return [{\n    json: {\n      success: false,\n      error: 'No candidates in response',\n      topic: meta.topic,\n      promptIndex: meta.promptIndex\n    }\n  }];\n}\n\n// Find the image part in the response\nconst parts = candidates[0].content.parts;\nlet imageData = null;\nlet textResponse = '';\n\nfor (const part of parts) {\n  if (part.inlineData && part.inlineData.data) {\n    imageData = part.inlineData;\n  }\n  if (part.text) {\n    textResponse = part.text;\n  }\n}\n\nif (!imageData) {\n  return [{\n    json: {\n      success: false,\n      error: 'No image data in response. Text response: ' + textResponse,\n      topic: meta.topic,\n      promptIndex: meta.promptIndex,\n      imagePrompt: meta.imagePrompt\n    }\n  }];\n}\n\n// Determine file extension from mime type\nconst mimeType = imageData.mimeType || 'image/png';\nconst ext = mimeType.includes('jpeg') ? '.jpg' : '.png';\nconst fileName = meta.fileName || `image_${meta.promptIndex}${ext}`;\n\nreturn [{\n  json: {\n    success: true,\n    topic: meta.topic,\n    promptIndex: meta.promptIndex,\n    imagePrompt: meta.imagePrompt,\n    fileName: fileName,\n    folderName: meta.folderName,\n    mimeType: mimeType\n  },\n  binary: {\n    image: {\n      data: imageData.data,\n      mimeType: mimeType,\n      fileName: fileName\n    }\n  }\n}];"
      },
      "id": "d41a3e53-70fc-4511-a7cd-ab085ec03d36",
      "name": "Extract Image Data",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -528,
        224
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "condition-success",
              "leftValue": "={{ $json.success }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "626fb651-732f-4b4a-ad43-62c2abef7aad",
      "name": "Image Generated?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -304,
        224
      ]
    },
    {
      "parameters": {
        "inputDataFieldName": "image",
        "name": "={{ $json.fileName }}",
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive"
        },
        "folderId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "options": {
          "simplifyOutput": true
        }
      },
      "id": "7a246c89-29b7-49f5-9efb-72b9ab776cde",
      "name": "Upload to Google Drive",
      "type": "n8n-nodes-base.googleDrive",
      "typeVersion": 3,
      "position": [
        -48,
        304
      ],
      "notes": "Add your Google Drive credential and pick a destination folder."
    },
    {
      "parameters": {
        "jsCode": "// Log failed image generation for review\nconst item = $input.first().json;\nconsole.log(`FAILED: Topic \"${item.topic}\" - Prompt #${item.promptIndex}`);\nconsole.log(`Error: ${item.error}`);\nconsole.log(`Prompt was: ${item.imagePrompt}`);\n\nreturn [$input.first()];"
      },
      "id": "3053d987-fc52-4ef4-aa76-db2026d3df63",
      "name": "Log Error",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -48,
        592
      ]
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\nconst ok = items.filter(i => i.json.success !== false && !i.json.error).length;\nreturn [{ json: {\n  topic: $('Parse Into 5 Prompts').first().json.topic,\n  status: ok > 0 ? 'done' : 'failed',\n  images_generated: ok,\n  last_run: new Date().toISOString()\n}}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -976,
        -16
      ],
      "id": "d39d8aa8-4093-4451-867e-66d256530498",
      "name": "Summarize Topics"
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [
            "topic"
          ],
          "schema": [
            {
              "id": "topic",
              "displayName": "topic",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "images_generated",
              "displayName": "images_generated",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "last_run",
              "displayName": "last_run",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "displayName": "row_number",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "canBeUsedToMatch": true,
              "readOnly": true,
              "removed": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -752,
        -16
      ],
      "id": "545ded71-8b3d-4a92-8293-3327decc5670",
      "name": "Update row in sheet",
      "retryOnFail": true,
      "waitBetweenTries": 5000,
      "notes": "Pick the same sheet. Columns: topic, status, images_generated, last_run. Match on topic."
    }
  ],
  "connections": {
    "Run Workflow": {
      "main": [
        [
          {
            "node": "Read Topics from Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Topics from Sheet": {
      "main": [
        [
          {
            "node": "Loop Over Topics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Topics": {
      "main": [
        [],
        [
          {
            "node": "Generate 5 Prompt Variations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate 5 Prompt Variations": {
      "main": [
        [
          {
            "node": "Parse Into 5 Prompts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Into 5 Prompts": {
      "main": [
        [
          {
            "node": "Loop Over Prompts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Prompts": {
      "main": [
        [
          {
            "node": "Summarize Topics",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Rate Limit Delay",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rate Limit Delay": {
      "main": [
        [
          {
            "node": "Generate Image with Gemini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Image with Gemini": {
      "main": [
        [
          {
            "node": "Extract Image Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Image Data": {
      "main": [
        [
          {
            "node": "Image Generated?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Image Generated?": {
      "main": [
        [
          {
            "node": "Upload to Google Drive",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to Google Drive": {
      "main": [
        [
          {
            "node": "Loop Over Prompts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Error": {
      "main": [
        [
          {
            "node": "Loop Over Prompts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Summarize Topics": {
      "main": [
        [
          {
            "node": "Update row in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update row in sheet": {
      "main": [
        [
          {
            "node": "Loop Over Topics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "meta": {},
  "name": "SearchFront - Bulk AI Image Generator"
}
