{
  "name": "SpicyAPI \u00b7 Google Sheets image queue",
  "nodes": [
    {
      "id": "c88edfb5-7032-4516-b2e4-a5ae4d1e7f58",
      "name": "Setup and limits",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "parameters": {
        "content": "## Google Sheets \u2192 images \u2192 row status\n\nSelf-hosted n8n 2.42 + SpicyAPI node 0.1.1.\n\nCreate headers: id, prompt, status, taskId, resultUrl, cost, error. Each id must be unique. Add prompts and status=todo. In Settings add documentId/sheetName, choose 1\u20134 maxItems, review maxCostUsd, then enabled=true. Select one Google Sheets credential in all three Sheets nodes and a SpicyAPI credential on Generate Image.\n\nRead Todo Rows returns all matches; Select Todo Rows bounds the paid batch to 4. Mark Processing writes before generation. Run manually with one execution at a time; it is not a multi-worker queue lock.\n\nDefault maximum: 4 \u00d7 $0.10 = $0.40. Each image is quoted before creation; wait is bounded at 300 seconds. done means the image was downloaded into Image Results binary.data. resultUrl is temporary. error/needs_review/processing rows are never picked up as todo automatically: recover accepted task IDs before manually resetting a row. If row writeback fails, inspect the saved execution before rerunning. No Drive archiving is included.\n\nSelect the same SpicyAPI credential in Get Image Result. Generate submits once and stores its task ID; Get Image Result retrieves that task and waits/downloads without generating again. Any result that is not ready becomes needs_review. Accepted task IDs remain in the Generate node and result output even if fetching fails. If no ID was returned, inspect the saved execution and your console before creating any new task.",
        "width": 780,
        "height": 520
      },
      "position": [
        0,
        -420
      ]
    },
    {
      "id": "01feee39-0385-4285-8e77-ec6b9b51c55c",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "parameters": {},
      "position": [
        0,
        0
      ]
    },
    {
      "id": "ce798372-2633-4bd1-a074-f6f6664a75c0",
      "name": "Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "0f5031de-cc92-4a76-8176-350a1b8c4fcd",
              "name": "enabled",
              "value": false,
              "type": "boolean"
            },
            {
              "id": "76a681df-1c36-4f75-ab95-862db0cd31a9",
              "name": "documentId",
              "value": "",
              "type": "string"
            },
            {
              "id": "18ec732e-77fb-4ef9-9e4a-39bebb98f7d5",
              "name": "sheetName",
              "value": "Images",
              "type": "string"
            },
            {
              "id": "3ab9b210-6b8c-4b8c-986f-5b8252848953",
              "name": "maxItems",
              "value": 4,
              "type": "number"
            },
            {
              "id": "524aa618-817e-44bb-9281-78b9f0cab6b2",
              "name": "maxCostUsd",
              "value": 0.1,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "position": [
        220,
        0
      ]
    },
    {
      "id": "d7e93d75-d284-4eb1-a5a5-275fb0026a2b",
      "name": "Validate Settings",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "function publicImage(value) {\n  const url = String(value || '').trim();\n  const match = url.match(/^https:\\/\\/([^/?#]+)(?:[/?#]|$)/i);\n  const host = match ? match[1].toLowerCase() : '';\n  const hostname = host.split(':')[0];\n  if (!host || host.includes('@') || host.startsWith('[') || !/^[a-z0-9.-]+(?::443)?$/.test(host) || /^(localhost|127\\.|0\\.|10\\.|192\\.168\\.|169\\.254\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.)/i.test(hostname)) throw new Error('Set a public HTTPS referenceImageUrl in Settings, without URL credentials.');\n  return url;\n}\nfunction costLimit(value) {\n  const limit = Number(value);\n  if (!Number.isFinite(limit) || limit <= 0 || limit > 0.10) throw new Error('maxCostUsd must be above 0 and at most 0.10 per image.');\n  return limit;\n}\n\nconst c = $input.first().json;\nif (c.enabled !== true) throw new Error('Configure Settings and credentials, then set enabled=true.');\nif (!String(c.documentId || '').trim() || !String(c.sheetName || '').trim()) throw new Error('Set documentId and sheetName in Settings.');\ncostLimit(c.maxCostUsd);\nif (!Number.isInteger(Number(c.maxItems)) || Number(c.maxItems) < 1 || Number(c.maxItems) > 4) throw new Error('maxItems must be an integer from 1 to 4.');\nreturn $input.all();"
      },
      "position": [
        450,
        0
      ]
    },
    {
      "id": "b8ff38b9-4bd4-4aa2-9529-5a138c41f3d0",
      "name": "Read Todo Rows",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "parameters": {
        "resource": "sheet",
        "operation": "read",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Settings').first().json.documentId }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Settings').first().json.sheetName }}"
        },
        "options": {
          "returnFirstMatch": false
        },
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "status",
              "lookupValue": "todo"
            }
          ]
        },
        "combineFilters": "AND"
      },
      "position": [
        690,
        0
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "",
          "name": "Select your Google Sheets credential"
        }
      }
    },
    {
      "id": "aba64cba-34f3-4081-a9c3-d8d746383b75",
      "name": "Select Todo Rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c = $('Settings').first().json;\nconst rows = $input.all();\nconst todo = rows.map((item, index) => ({item, index})).filter(({item}) => String(item.json.status || '').trim().toLowerCase() === 'todo');\nconst ids = new Set();\nfor (const {item} of todo) {\n  const id = String(item.json.id || '').trim();\n  if (!id || ids.has(id)) throw new Error('Every todo row needs a unique, non-empty id.');\n  ids.add(id);\n}\nreturn todo.slice(0, Number(c.maxItems)).map(({item, index}) => {\n  const prompt = String(item.json.prompt || '').trim();\n  if (!prompt || prompt.length > 1000) throw new Error('Each selected prompt must contain 1 to 1000 characters.');\n  return {json: {id: String(item.json.id).trim(), prompt, status: 'processing', maxCostUsd: Number(c.maxCostUsd), batchMaxCostUsd: Number((Math.min(todo.length, Number(c.maxItems)) * Number(c.maxCostUsd)).toFixed(4))}, pairedItem: {item: index}};\n});"
      },
      "position": [
        920,
        0
      ]
    },
    {
      "id": "a9fe021d-50a8-4224-a01e-0f4c9e318422",
      "name": "Mark Processing",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "parameters": {
        "resource": "sheet",
        "operation": "update",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Settings').first().json.documentId }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Settings').first().json.sheetName }}"
        },
        "options": {
          "cellFormat": "RAW"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "id": "={{ $json.id }}",
            "status": "processing"
          },
          "matchingColumns": [
            "id"
          ],
          "schema": [
            {
              "id": "id",
              "displayName": "id",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "position": [
        1160,
        0
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "",
          "name": "Select your Google Sheets credential"
        }
      }
    },
    {
      "id": "7bdcff21-54ae-4df4-9d93-0bdd4519738c",
      "name": "Generate Image",
      "type": "@spicyapi/n8n-nodes-spicyapi.spicyapiAi",
      "typeVersion": 1,
      "parameters": {
        "resource": "image",
        "operation": "generate",
        "model": {
          "__rl": true,
          "mode": "id",
          "value": "alibaba/z-image-turbo/text-to-image"
        },
        "prompt": "={{ $('Select Todo Rows').all()[$itemIndex].json.prompt }}",
        "mediaInputs": {},
        "modelParameters": {
          "mappingMode": "defineBelow",
          "value": {
            "aspect_ratio": "3:4",
            "output_format": "png",
            "enable_prompt_expansion": false
          },
          "schema": []
        },
        "additionalParameters": "{}",
        "maxCost": "={{ $('Settings').first().json.maxCostUsd }}",
        "generateOptions": {
          "waitForCompletion": false,
          "downloadOutput": false
        }
      },
      "position": [
        1400,
        0
      ],
      "credentials": {
        "spicyapiAiApi": {
          "id": "",
          "name": "Select your SpicyAPI credential"
        }
      },
      "continueOnFail": true
    },
    {
      "id": "17e6d752-a763-4141-8e1a-3d29e3981d72",
      "name": "Get Image Result",
      "type": "@spicyapi/n8n-nodes-spicyapi.spicyapiAi",
      "typeVersion": 1,
      "parameters": {
        "resource": "task",
        "operation": "get",
        "taskId": "={{ $json.taskId || '' }}",
        "taskOptions": {
          "waitForCompletion": true,
          "maxWaitTime": 300,
          "downloadOutput": true,
          "binaryPropertyName": "data"
        }
      },
      "position": [
        1630,
        0
      ],
      "credentials": {
        "spicyapiAiApi": {
          "id": "",
          "name": "Select your SpicyAPI credential"
        }
      },
      "continueOnFail": true
    },
    {
      "id": "bbafaabb-9f20-467d-9258-4a150526f35f",
      "name": "Image Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const inputs = $('Select Todo Rows').all();\nreturn $input.all().map((item, index) => {\n  const accepted = $('Generate Image').all()[index]?.json || {};\n  const t = item.json;\n  const taskId = t.taskId || accepted.taskId || '';\n  const failure = String(t.error || t.errorMessage || accepted.error || 'Retrieve this task before creating another image.');\n  const ready = t.state === 'succeeded' && !!item.binary?.data;\n  return {json: {id: inputs[index].json.id, status: ready ? 'done' : 'needs_review', taskId, resultUrl: t.output?.assets?.find(a => a.url && String(a.mime || '').startsWith('image/'))?.url || '', cost: t.cost || '', error: ready ? '' : failure + (taskId ? '' : ' Inspect the saved execution and console before creating another task.'), batchMaxCostUsd: inputs[index].json.batchMaxCostUsd}, ...(item.binary ? {binary: item.binary} : {}), pairedItem: {item: index}};\n});"
      },
      "position": [
        1880,
        0
      ]
    },
    {
      "id": "8221add0-f95e-43e2-b781-dd1d5359d1dd",
      "name": "Write Results",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "parameters": {
        "resource": "sheet",
        "operation": "update",
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Settings').first().json.documentId }}"
        },
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Settings').first().json.sheetName }}"
        },
        "options": {
          "cellFormat": "RAW"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "id": "={{ $json.id }}",
            "status": "={{ $json.status }}",
            "taskId": "={{ $json.taskId }}",
            "resultUrl": "={{ $json.resultUrl }}",
            "cost": "={{ $json.cost }}",
            "error": "={{ $json.error }}"
          },
          "matchingColumns": [
            "id"
          ],
          "schema": [
            {
              "id": "id",
              "displayName": "id",
              "required": false,
              "defaultMatch": true,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "displayName": "status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "taskId",
              "displayName": "taskId",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "resultUrl",
              "displayName": "resultUrl",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "cost",
              "displayName": "cost",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "error",
              "displayName": "error",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "position": [
        2130,
        0
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "",
          "name": "Select your Google Sheets credential"
        }
      }
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "Validate Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Settings": {
      "main": [
        [
          {
            "node": "Read Todo Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Todo Rows": {
      "main": [
        [
          {
            "node": "Select Todo Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select Todo Rows": {
      "main": [
        [
          {
            "node": "Mark Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mark Processing": {
      "main": [
        [
          {
            "node": "Generate Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Image": {
      "main": [
        [
          {
            "node": "Get Image Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Image Results": {
      "main": [
        [
          {
            "node": "Write Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Image Result": {
      "main": [
        [
          {
            "node": "Image Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 1800
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}
