{
  "name": "SpicyAPI Cloud — Google Sheets Image Queue",
  "nodes": [
    {
      "id": "a31dc313-e567-411e-a1b7-1474b3c7a89b",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "parameters": {}
    },
    {
      "id": "d880447a-9737-4cab-9c9a-19b1a47b1e81",
      "name": "Settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ],
      "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": {}
      }
    },
    {
      "id": "bfd2da5c-abdd-4d83-99f7-189ebea0eacb",
      "name": "Validate Settings",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "function 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}\nfunction 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 without URL credentials.');\n  return url;\n}\n\nconst c = $input.first().json;\nif (c.enabled !== true) throw new Error('Configure Settings and credentials before setting 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();\n"
      }
    },
    {
      "id": "2c82f4ba-7a25-4b50-a431-cd04b3fc5a46",
      "name": "Read Rows",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        660,
        0
      ],
      "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
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "id": "86d215f0-1c2c-40ef-9591-4cff22a736fb",
      "name": "Select Rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c = $('Settings').first().json;\nconst rows = $input.all();\nconst ids = new Set();\nfor (const item of rows) {\n  const id = String(item.json.id || '').trim();\n  if (!/^[A-Za-z0-9_-]{1,48}$/.test(id) || ids.has(id)) throw new Error('Every row needs a unique id using 1 to 48 letters, digits, underscores or hyphens.');\n  ids.add(id);\n}\nconst eligible = rows.map((item, index) => ({item, index})).filter(({item}) => {\n  const r = item.json;\n  const status = String(r.status || '').trim().toLowerCase();\n  return status === 'todo' || (['processing', 'needs_review'].includes(status) && String(r.taskId || '').trim());\n}).slice(0, Number(c.maxItems));\nreturn eligible.map(({item, index}) => {\n  const r = item.json;\n  const id = String(r.id).trim();\n  const taskId = String(r.taskId || '').trim();\n  const base = {id, maxCostUsd: Number(c.maxCostUsd), batchMaxCostUsd: Number((eligible.length * Number(c.maxCostUsd)).toFixed(4)), taskId, acceptedTaskId: taskId, idempotencyKey: String(r.idempotencyKey || '').trim(), firstAttemptAt: String(r.firstAttemptAt || '').trim(), requestJson: String(r.requestJson || '').trim()};\n  if (taskId) {\n    if (!/^[A-Za-z0-9_-]{1,128}$/.test(taskId)) throw new Error('Invalid taskId in row ' + id + '.');\n    return {json: {...base, recoveryOnly: true}, pairedItem: {item: index}};\n  }\n  let payload;\n  if (base.idempotencyKey || base.requestJson || base.firstAttemptAt) {\n    const started = Date.parse(base.firstAttemptAt);\n    if (!/^spicy-cloud-sheet-[A-Za-z0-9_-]{1,100}$/.test(base.idempotencyKey) || !Number.isFinite(started) || started > Date.now() + 120000 || Date.now() - started >= 23 * 3600000) return {json: {...base, status: 'needs_review', error: 'Do not submit again: the saved attempt is incomplete or outside the 23-hour replay window. Find its accepted task ID in the saved execution or Console, then enter taskId to retrieve it.', skipSubmit: true}, pairedItem: {item: index}};\n    try { payload = JSON.parse(base.requestJson); } catch { throw new Error('Saved requestJson is invalid in row ' + id + '.'); }\n    if (JSON.stringify(Object.keys(payload).sort()) !== JSON.stringify(['input','model']) || payload.model !== 'alibaba/z-image-turbo/text-to-image' || typeof payload.input?.prompt !== 'string' || !payload.input.prompt.trim() || payload.input.prompt.length > 1000 || JSON.stringify(Object.keys(payload.input).sort()) !== JSON.stringify(['aspect_ratio','enable_prompt_expansion','output_format','prompt'].sort()) || payload.input.aspect_ratio !== '3:4' || payload.input.output_format !== 'png' || payload.input.enable_prompt_expansion !== false) throw new Error('Saved requestJson does not match this template in row ' + id + '.');\n  } else {\n    const prompt = String(r.prompt || '').trim();\n    if (!prompt || prompt.length > 1000) throw new Error('Each selected prompt must contain 1 to 1000 characters.');\n    const executionId = String($execution.id || '');\n    if (!/^[A-Za-z0-9_-]{1,40}$/.test(executionId)) throw new Error('A valid saved execution ID is required.');\n    payload = {model: 'alibaba/z-image-turbo/text-to-image', input: {prompt, aspect_ratio: '3:4', output_format: 'png', enable_prompt_expansion: false}};\n    base.idempotencyKey = 'spicy-cloud-sheet-' + executionId + '-' + id;\n    base.firstAttemptAt = new Date().toISOString();\n    base.requestJson = JSON.stringify(payload);\n  }\n  return {json: {...base, ...payload, status: 'processing'}, pairedItem: {item: index}};\n});"
      }
    },
    {
      "id": "24fa9d11-f1bc-467e-a354-c239be3c535c",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1100,
        0
      ],
      "parameters": {
        "batchSize": 1,
        "options": {}
      }
    },
    {
      "id": "3c484ea0-62f2-48ec-9056-4902798e194d",
      "name": "Batch Results",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1320,
        -240
      ],
      "parameters": {}
    },
    {
      "id": "c3238ebb-200e-4b53-9eb3-30163c7bfdab",
      "name": "Item Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        100
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c = $input.first().json;\nreturn [{json: {...c, accepted: !!c.acceptedTaskId, quoteRefreshes: 0, pollCount: 0, deadlineAt: Date.now() + 300000}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "6e6bbaf9-578b-4593-a6f5-21e81e3dc07f",
      "name": "Skip Submission?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1540,
        100
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "b188af94-c1ce-4411-9a95-1fa72e9fa7b2",
              "leftValue": "={{ $json.skipSubmit === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "53a5cbbb-320c-4d5f-9975-7b9058b126a8",
      "name": "Existing Task?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1760,
        180
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "37888520-d14f-44cc-b717-70d90e54bfbf",
              "leftValue": "={{ !!$json.acceptedTaskId }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "ef250494-6f03-497a-b096-753c90988e50",
      "name": "Save Request",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1980,
        360
      ],
      "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",
            "idempotencyKey": "={{ $json.idempotencyKey }}",
            "firstAttemptAt": "={{ $json.firstAttemptAt }}",
            "requestJson": "={{ $json.requestJson }}"
          },
          "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": "idempotencyKey",
              "displayName": "idempotencyKey",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "firstAttemptAt",
              "displayName": "firstAttemptAt",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "requestJson",
              "displayName": "requestJson",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "id": "8dfd4ff7-f434-48bc-aa5b-e9f5592c84d4",
      "name": "Saved Request Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2200,
        360
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const item = $('Item Context').item;\nreturn [{json: {...item.json}, ...(item.binary ? {binary: item.binary} : {}), pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "12533173-d0bb-4e0f-9650-35bb5cd03bc6",
      "name": "Quote Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2420,
        360
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{json: {...$input.first().json}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "49f2a9b3-7763-452b-9727-4a43b632dde6",
      "name": "Quote Request",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2640,
        360
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.spicyapi.ai/api/v1/jobs/quote",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "responseFormat": "json",
              "fullResponse": true,
              "neverError": true
            }
          },
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          }
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({model: $json.model, input: $json.input}) }}"
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "",
          "name": "SpicyAPI Bearer"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "b0ca4d53-77e9-4ac6-85b5-ec531f90b602",
      "name": "Check Quote",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2860,
        360
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c = $('Quote Context').item.json;\nconst response = $input.first().json;\nconst b = response.body || {};\nconst q = b.data || {};\nconst estimated = Number(q.estimatedCost);\nconst maximum = Number(q.maxCharge);\nconst amount = value => typeof value === 'string' && /^\\d+(?:\\.\\d+)?$/.test(value);\nconst valid = response.statusCode >= 200 && response.statusCode < 300 && b.code === 200 && typeof q.quoteId === 'string' && !!q.quoteId && amount(q.estimatedCost) && amount(q.maxCharge) && Number.isFinite(estimated) && Number.isFinite(maximum) && estimated >= 0 && maximum >= estimated;\nconst replayAge = Date.now() - Date.parse(c.firstAttemptAt);\nconst withinWindow = Number.isFinite(replayAge) && replayAge >= -120000 && replayAge < 23 * 3600000;\nconst withinLimit = valid && withinWindow && maximum <= c.maxCostUsd && estimated <= c.maxCostUsd;\nreturn [{json: {...c, quote: valid ? q : null, quoteAllowed: withinLimit, refreshPrice: false, status: withinLimit ? 'processing' : 'needs_review', error: withinLimit ? '' : (valid && !withinWindow ? 'The 23-hour replay window ended. Recover by the accepted task ID; no task was submitted in this attempt.' : valid ? 'Quote exceeds maxCostUsd. No task was submitted in this attempt.' : 'Could not verify the quote. No task was submitted in this attempt.'), quoteRequestId: b.request_id || ''}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "c3862fd0-f874-434e-8a27-46ce061c5c0b",
      "name": "Quote Approved?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3080,
        360
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "891ea9c2-8ef5-428d-927d-7487fbbd54f6",
              "leftValue": "={{ $json.quoteAllowed === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "e7e71582-414b-46fe-85ab-92fa4f358895",
      "name": "Submit Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3300,
        460
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{json: {...$input.first().json}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "83961c04-21c0-4ba3-a8e2-05414e1be5c2",
      "name": "Create Task",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3520,
        460
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.spicyapi.ai/api/v1/jobs/createTask",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "responseFormat": "json",
              "fullResponse": true,
              "neverError": true
            }
          },
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          }
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({model: $json.model, input: $json.input, quoteId: $json.quote.quoteId, expectedCost: $json.quote.estimatedCost}) }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Idempotency-Key",
              "value": "={{ $json.idempotencyKey }}"
            }
          ]
        }
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "",
          "name": "SpicyAPI Bearer"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "ef9a84bf-adca-4b38-bc9a-00bde62046fa",
      "name": "Check Submission",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3740,
        460
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c = $('Submit Context').item.json;\nconst response = $input.first().json;\nconst b = response.body || {};\nconst task = b.data || {};\nconst id = typeof task.taskId === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(task.taskId) ? task.taskId : '';\nconst accepted = response.statusCode >= 200 && response.statusCode < 300 && b.code === 200 && !!id;\nconst refreshPrice = !accepted && b.code === 40901 && c.quoteRefreshes === 0;\nreturn [{json: {...c, taskId: accepted ? id : (c.taskId || ''), acceptedTaskId: accepted ? id : (c.acceptedTaskId || ''), requestId: b.request_id || c.requestId || '', accepted, refreshPrice, quoteRefreshes: c.quoteRefreshes + (refreshPrice ? 1 : 0), status: accepted || refreshPrice ? 'processing' : 'needs_review', error: accepted || refreshPrice ? '' : 'Submission did not return a confirmed task ID. Keep this idempotencyKey and unchanged request; inspect the saved execution and Console before any new attempt.'}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "5789550c-8bce-4c92-8da7-14204d0a044e",
      "name": "Refresh Price Once?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3960,
        460
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "8cc3e355-852e-4780-8c67-ec3e581313c4",
              "leftValue": "={{ $json.refreshPrice === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "077309ea-94ab-4930-bfb3-806a006e2d55",
      "name": "Task Accepted?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        4180,
        560
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "5583fa72-6c35-4dd3-84cd-81ea3f0a0e57",
              "leftValue": "={{ $json.accepted === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "992b1223-9b2c-48be-833a-e3fa37c242e0",
      "name": "Save Accepted Task",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        4400,
        660
      ],
      "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 }}",
            "idempotencyKey": "={{ $json.idempotencyKey }}",
            "firstAttemptAt": "={{ $json.firstAttemptAt }}",
            "requestJson": "={{ $json.requestJson }}"
          },
          "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": "idempotencyKey",
              "displayName": "idempotencyKey",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "firstAttemptAt",
              "displayName": "firstAttemptAt",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "requestJson",
              "displayName": "requestJson",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "id": "066d35eb-8217-447a-a039-3494dc09688e",
      "name": "Accepted Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4620,
        660
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const item = $('Check Submission').item;\nreturn [{json: {...item.json}, ...(item.binary ? {binary: item.binary} : {}), pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "cda0aa01-01ba-4963-bf0f-7e581c00f016",
      "name": "Poll Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4840,
        100
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c = $input.first().json;\nconst allowed = !!c.acceptedTaskId && c.pollCount < 60 && Date.now() < c.deadlineAt;\nreturn [{json: {...c, taskId: c.acceptedTaskId || c.taskId || '', pollAllowed: allowed, pollCount: c.pollCount + (allowed ? 1 : 0), status: allowed ? 'processing' : 'needs_review', error: allowed ? '' : 'The polling deadline was reached. The task may still finish; retrieve the same task ID instead of creating another image.'}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "e6099d56-fc73-447a-bd7b-8d9a15233225",
      "name": "Within Polling Limit?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5060,
        100
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "3e87dadb-790f-4ec4-98cc-fd3f7e747b06",
              "leftValue": "={{ $json.pollAllowed === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "d579ba78-82d2-4c05-a323-6cdd9eb33748",
      "name": "Get Task",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        5280,
        200
      ],
      "parameters": {
        "method": "GET",
        "url": "https://api.spicyapi.ai/api/v1/jobs/recordInfo",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "responseFormat": "json",
              "fullResponse": true,
              "neverError": true
            }
          },
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          }
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "taskId",
              "value": "={{ $json.acceptedTaskId }}"
            }
          ]
        }
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "",
          "name": "SpicyAPI Bearer"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "31615df3-42e5-4edf-8b74-e1e28abdc22b",
      "name": "Check Task",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5500,
        200
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c = $('Poll Context').item.json;\nconst response = $input.first().json;\nconst b = response.body || {};\nconst t = b.data || {};\nconst valid = response.statusCode >= 200 && response.statusCode < 300 && b.code === 200 && t.taskId === c.acceptedTaskId;\nconst assets = Array.isArray(t.output?.assets) ? t.output.assets : [];\nconst pending = assets.some(a => a.pending === true && a.unavailable !== true);\nconst asset = assets.find(a => a.pending !== true && a.unavailable !== true && typeof a.url === 'string' && /^https:\\/\\//i.test(a.url) && String(a.mime || '').startsWith('image/'));\nconst ready = valid && t.state === 'succeeded' && !pending && !!asset;\nconst retryableLookup = !!response.error || [429,500,502,503,504].includes(Number(response.statusCode));\nconst waiting = (valid && (['queued','running'].includes(t.state) || (t.state === 'succeeded' && pending))) || (!valid && retryableLookup);\nconst keepWaiting = !ready && waiting && c.pollCount < 60 && Date.now() < c.deadlineAt;\nreturn [{json: {...c, taskId: c.acceptedTaskId, state: valid ? t.state : (c.state || ''), cost: valid ? String(t.cost ?? '') : (c.cost || ''), settled: valid ? t.settled : c.settled, resultUrl: ready ? asset.url : (c.resultUrl || ''), readyToDownload: ready, keepWaiting, status: ready || keepWaiting ? 'processing' : 'needs_review', error: ready || keepWaiting ? '' : (valid && ['failed','expired','canceled'].includes(t.state) ? String(t.errorMessage || t.errorCode || 'The accepted task ended without an image.') : 'No downloadable image was confirmed before the deadline. Retrieve this accepted task ID; do not generate a replacement automatically.'), lookupRequestId: b.request_id || ''}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "cf18cf1a-82dd-457a-af1d-6ed3adb070ef",
      "name": "Image Ready?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5720,
        200
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "23623cc0-dfd1-464f-8f83-fa61ec845b76",
              "leftValue": "={{ $json.readyToDownload === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "cc9d8da5-d9c2-4946-bb54-ea5bfc2e5b98",
      "name": "Keep Waiting?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        5940,
        360
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "156c3b5a-8126-4f81-934c-a413d9fe5f7c",
              "leftValue": "={{ $json.keepWaiting === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "5a818b12-7710-4823-9f91-ce387bdaa12d",
      "name": "Wait 5 Seconds",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        6160,
        460
      ],
      "parameters": {
        "resume": "timeInterval",
        "amount": 5,
        "unit": "seconds"
      },
      "webhookId": "652a224d-1b13-490b-907a-e88211b78f1b"
    },
    {
      "id": "17ad80c7-4ec1-43f6-b1e6-e620bad35bf8",
      "name": "Download Context",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        5940,
        40
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{json: {...$input.first().json}, pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "50208fd1-9197-4b95-b335-112185ebb429",
      "name": "Download Image",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        6160,
        40
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $json.resultUrl }}",
        "authentication": "none",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "data",
              "fullResponse": true
            }
          },
          "redirect": {
            "redirect": {
              "followRedirects": true,
              "maxRedirects": 3
            }
          }
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "67af89bf-41b0-4d55-925c-8a16cac2a016",
      "name": "Check Download",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6380,
        40
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c = $('Download Context').item.json;\nconst item = $input.first();\nconst response = item.json;\nconst mime = String(item.binary?.data?.mimeType || '').toLowerCase();\nconst ok = response.statusCode >= 200 && response.statusCode < 300 && !!item.binary?.data && mime.startsWith('image/');\nreturn [{json: {...c, status: ok ? 'done' : 'needs_review', error: ok ? '' : 'The task succeeded but its image could not be downloaded. Retrieve this same task ID to refresh the result URL; do not create a new image.'}, ...(ok ? {binary: item.binary} : {}), pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "57c9b15a-c9a3-44b2-8040-c02cbd09c334",
      "name": "Final Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        6600,
        280
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const item = $input.first();\nconst c = item.json;\nconst done = c.status === 'done' && !!item.binary?.data;\nreturn [{json: {id: c.id || '', sceneNumber: c.sceneNumber || 0, sceneName: c.sceneName || '', status: done ? 'done' : 'needs_review', taskId: c.acceptedTaskId || c.taskId || '', acceptedTaskId: c.acceptedTaskId || c.taskId || '', idempotencyKey: c.idempotencyKey || '', firstAttemptAt: c.firstAttemptAt || '', requestJson: c.requestJson || (c.model && c.input ? JSON.stringify({model: c.model, input: c.input}) : ''), resultUrl: c.resultUrl || '', cost: c.cost || '', error: done ? '' : String(c.error || 'Inspect the saved execution and Console before submitting any replacement.'), batchMaxCostUsd: c.batchMaxCostUsd || 0, requestId: c.requestId || '', quoteRequestId: c.quoteRequestId || '', lookupRequestId: c.lookupRequestId || ''}, ...(done ? {binary: item.binary} : {}), pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "26786403-0d79-4f6b-936e-43835318b4ff",
      "name": "Write Results",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        6820,
        280
      ],
      "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 }}",
            "idempotencyKey": "={{ $json.idempotencyKey }}",
            "firstAttemptAt": "={{ $json.firstAttemptAt }}",
            "requestJson": "={{ $json.requestJson }}"
          },
          "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
            },
            {
              "id": "idempotencyKey",
              "displayName": "idempotencyKey",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "firstAttemptAt",
              "displayName": "firstAttemptAt",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "requestJson",
              "displayName": "requestJson",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "id": "ea065a5a-ab72-4927-8659-65761f4b1f60",
      "name": "Preserve Image Result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        7040,
        280
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const item = $('Final Result').item;\nreturn [{json: {...item.json}, ...(item.binary ? {binary: item.binary} : {}), pairedItem: {item: 0}}];"
      }
    },
    {
      "id": "baaec668-b327-4f02-a199-c65cfcf8d1f1",
      "name": "Setup and Recovery",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -820
      ],
      "parameters": {
        "content": "## Set up before running\nImport into n8n Cloud. No community node is required. In Quote Request, Create Task and Get Task, choose Generic Credential Type → Bearer Auth and select the same credential. Select the same Google Sheets OAuth2 credential in every Sheets node. Configure Settings, then set enabled=true. The workflow is inactive and starts manually.\n\nUse a sheet with these headers: id, prompt, status, taskId, resultUrl, cost, error, idempotencyKey, firstAttemptAt, requestJson. Each id must be unique (letters, digits, underscore or hyphen; 48 characters maximum). Set new rows to todo. At most 4 rows are processed per manual run. Do not run this queue concurrently. Save Request writes the unchanged request and replay key before Create Task; Save Accepted Task writes taskId before polling. Rows with an accepted taskId are retrieved without generating again. An uncertain attempt without a taskId stays needs_review: inspect Console first. To replay the same request within 23 hours, set status=todo without clearing the three saved request fields. Older attempts must be recovered by taskId. Do not edit these saved fields.\n\n## Cost and recovery\nEvery image has its own fresh quote and maximum-charge check. A price change gets one re-quote under the same key; a higher quote stops before another submission. Poll every 5 seconds, for at most 60 lookups or 300 seconds per item (plus an in-flight HTTP request). Accepted tasks are never automatically recreated. needs_review retains taskId, replay key and request IDs. Stop waiting does not cancel a task. Download Image uses no API credential. Batch Results contains binary.data for successful images. resultUrl is temporary; save copies to your own storage. A Sheets write failure stops the run, leaving the accepted ID in the saved execution.\n\nLocal contract and simulated execution checks are documented on the download page; no real Cloud import or paid generation is claimed.",
        "height": 700,
        "width": 1000,
        "color": 5
      }
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Settings": {
      "main": [
        [
          {
            "node": "Validate Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Settings": {
      "main": [
        [
          {
            "node": "Read Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Rows": {
      "main": [
        [
          {
            "node": "Select Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select Rows": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "Batch Results",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Item Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Item Context": {
      "main": [
        [
          {
            "node": "Skip Submission?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip Submission?": {
      "main": [
        [
          {
            "node": "Final Result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Existing Task?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Existing Task?": {
      "main": [
        [
          {
            "node": "Poll Context",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Save Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Request": {
      "main": [
        [
          {
            "node": "Saved Request Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Saved Request Context": {
      "main": [
        [
          {
            "node": "Quote Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Quote Context": {
      "main": [
        [
          {
            "node": "Quote Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Quote Request": {
      "main": [
        [
          {
            "node": "Check Quote",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Quote": {
      "main": [
        [
          {
            "node": "Quote Approved?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Quote Approved?": {
      "main": [
        [
          {
            "node": "Submit Context",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Final Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Submit Context": {
      "main": [
        [
          {
            "node": "Create Task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Task": {
      "main": [
        [
          {
            "node": "Check Submission",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Submission": {
      "main": [
        [
          {
            "node": "Refresh Price Once?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Refresh Price Once?": {
      "main": [
        [
          {
            "node": "Quote Context",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Task Accepted?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Task Accepted?": {
      "main": [
        [
          {
            "node": "Save Accepted Task",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Final Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Accepted Task": {
      "main": [
        [
          {
            "node": "Accepted Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Accepted Context": {
      "main": [
        [
          {
            "node": "Poll Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll Context": {
      "main": [
        [
          {
            "node": "Within Polling Limit?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Within Polling Limit?": {
      "main": [
        [
          {
            "node": "Get Task",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Final Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Task": {
      "main": [
        [
          {
            "node": "Check Task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Task": {
      "main": [
        [
          {
            "node": "Image Ready?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Image Ready?": {
      "main": [
        [
          {
            "node": "Download Context",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Keep Waiting?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep Waiting?": {
      "main": [
        [
          {
            "node": "Wait 5 Seconds",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Final Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 5 Seconds": {
      "main": [
        [
          {
            "node": "Poll Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Context": {
      "main": [
        [
          {
            "node": "Download Image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Image": {
      "main": [
        [
          {
            "node": "Check Download",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Download": {
      "main": [
        [
          {
            "node": "Final Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Final Result": {
      "main": [
        [
          {
            "node": "Write Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Results": {
      "main": [
        [
          {
            "node": "Preserve Image Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Preserve Image Result": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all",
    "saveExecutionProgress": true,
    "executionTimeout": 4500
  },
  "pinData": {},
  "tags": []
}
