{
  "nodes": [
    {
      "id": "9d2458b4-fea4-4a3d-a1a9-c9a6d2849560",
      "name": "Call Files API",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -752,
        -160
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/files",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "multipart-form-data",
        "authentication": "predefinedCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "purpose",
              "value": "batch"
            },
            {
              "name": "file",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "data"
            }
          ]
        },
        "nodeCredentialType": "openAiApi"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2,
      "alwaysOutputData": true
    },
    {
      "id": "974a2186-4480-4cc2-bd82-391a9a0ceca7",
      "name": "Call Batch API",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -528,
        -160
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/batches",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"input_file_id\": \"{{ $json.id }}\",\n  \"endpoint\": \"/v1/responses\",\n  \"completion_window\": \"24h\"\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2,
      "alwaysOutputData": true
    },
    {
      "id": "208f7358-6098-4137-b067-07238efc9d08",
      "name": "Start (mock data)",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -1200,
        -160
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "a93d70c7-923d-4219-9df4-00fde597a542",
      "name": "If status = completed",
      "type": "n8n-nodes-base.if",
      "position": [
        -528,
        160
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "e9c51ad5-8ae7-46fc-b072-102fb37d6c14",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.status }}",
              "rightValue": "completed"
            },
            {
              "id": "916818b2-446e-4e61-9524-5f09fc094c40",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.output_file_id }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "09f854d5-43bd-47b0-825b-220eff998eaa",
      "name": "Convert to batch requests in .jsonl",
      "type": "n8n-nodes-base.code",
      "position": [
        -976,
        -160
      ],
      "parameters": {
        "jsCode": "const inputs = $input.first().json.inputs\nconst systemPrompt = $input.first().json.systemPrompt;\n\nfunction createBatchRequestLine(input, index) {\n  return {\n    custom_id: `${index}_${Date.now()}`,\n    method: \"POST\",\n    url: \"/v1/responses\",\n    body: {\n      model: \"gpt-5-mini\",\n      instructions: systemPrompt,\n      input,\n    }\n  };\n}\n\nconst jsonlContent = inputs\n  .map((input, index) => JSON.stringify(createBatchRequestLine(input, index)))\n  .join('\\n');\n\n// Convert to base64 for binary\nconst base64Content = Buffer.from(jsonlContent, 'utf-8').toString('base64');\n\n// Return with binary data - ready for HTTP Request!\nreturn [{\n  binary: {\n    data: {\n      data: base64Content,\n      mimeType: 'application/x-ndjson',\n      fileName: 'batch_input.jsonl'\n    }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "fe5c9d43-a1ee-4f1b-bd4b-988fe0194d50",
      "name": ".jsonl to base64",
      "type": "n8n-nodes-base.extractFromFile",
      "position": [
        -80,
        64
      ],
      "parameters": {
        "options": {},
        "operation": "binaryToPropery"
      },
      "typeVersion": 1.1
    },
    {
      "id": "6e372e85-930f-4a71-96e0-5f3a5668f513",
      "name": "Decode base64",
      "type": "n8n-nodes-base.code",
      "position": [
        144,
        64
      ],
      "parameters": {
        "jsCode": "const base64Content = $input.first().json.data;\n\nconst jsonlContent = Buffer.from(base64Content, 'base64').toString('utf-8');\n\nconst lines = jsonlContent\n  .split('\\n')\n  .filter(line => line.trim())\n  .map(line => {\n    try {\n      return JSON.parse(line);\n    } catch (e) {\n      return null;\n    }\n  })\n  .filter(line => line !== null);\n\n// Extract answers from each line\nconst results = [];\nlet parsed = 0;\nlet errors = 0;\n\nfor (const line of lines) {\n  try {\n    if (line.error || line.response?.status_code !== 200) {\n      errors++;\n      continue;\n    }\n\n    const output = line.response?.body?.output || [];\n    const messageOutput = output.find(o => o.type === 'message');\n    const text = messageOutput?.content?.[0]?.text;\n\n    if (!text) {\n      errors++;\n      continue;\n    }\n\n    results.push(text);\n    parsed++;\n  } catch (e) {\n    errors++;\n  }\n}\n\n// Get batch metadata from the earlier node\nconst batch = $('If status = completed').first().json;\n\nreturn [{\n  json: {\n    id: batch.id,\n    status: batch.status,\n    output_file_id: batch.output_file_id,\n    result: results,\n    parsed,\n    errors\n  }\n}];"
      },
      "typeVersion": 2,
      "alwaysOutputData": false
    },
    {
      "id": "de97fb6a-2795-41f2-a2da-67aac90bd87e",
      "name": "Call Batch API to retrieve batch object",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -752,
        160
      ],
      "parameters": {
        "url": "=https://api.openai.com/v1/batches/{{ $json.id }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "6ac10207-84af-4386-96ee-af15e74e71fe",
      "name": "Download .jsonl result",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -304,
        64
      ],
      "parameters": {
        "url": "=https://api.openai.com/v1/files/{{ $json.output_file_id }}/content",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        },
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.1
    },
    {
      "id": "9df69ab5-13c5-4a4f-8969-ed6bd4a640e0",
      "name": "Cron Job (5 mins)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1200,
        160
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes"
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "de0aebc0-d494-406c-8d2e-d3eb95e14930",
      "name": "Create a row in batch table",
      "type": "n8n-nodes-base.supabase",
      "position": [
        -304,
        -160
      ],
      "parameters": {
        "tableId": "openai_batches",
        "fieldsUi": {
          "fieldValues": [
            {
              "fieldId": "=id",
              "fieldValue": "={{ $json.id }}"
            },
            {
              "fieldId": "batch_status",
              "fieldValue": "={{ $json.status }}"
            }
          ]
        }
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "6b78a91e-aeec-4777-800a-5b9751d88365",
      "name": "Get the earliest uncompleted batch",
      "type": "n8n-nodes-base.postgres",
      "position": [
        -976,
        160
      ],
      "parameters": {
        "sort": {
          "values": [
            {
              "column": "created_at"
            }
          ]
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "openai_batches",
          "cachedResultName": "openai_batches"
        },
        "where": {
          "values": [
            {
              "value": "failed",
              "column": "batch_status",
              "condition": "!="
            },
            {
              "value": "expired",
              "column": "batch_status",
              "condition": "!="
            },
            {
              "value": "cancelled",
              "column": "batch_status",
              "condition": "!="
            },
            {
              "value": "completed",
              "column": "batch_status",
              "condition": "!="
            }
          ]
        },
        "schema": {
          "__rl": true,
          "mode": "list",
          "value": "public"
        },
        "options": {},
        "operation": "select",
        "returnAll": true
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.6
    },
    {
      "id": "7477a084-cf40-4922-bf1a-edacd3e2f87c",
      "name": "Update status",
      "type": "n8n-nodes-base.supabase",
      "position": [
        -304,
        256
      ],
      "parameters": {
        "filters": {
          "conditions": [
            {
              "keyName": "id",
              "keyValue": "={{ $json.id }}",
              "condition": "eq"
            }
          ]
        },
        "tableId": "openai_batches",
        "fieldsUi": {
          "fieldValues": [
            {
              "fieldId": "updated_at",
              "fieldValue": "={{ $now }}"
            },
            {
              "fieldId": "batch_status",
              "fieldValue": "={{ $json.status }}"
            }
          ]
        },
        "matchType": "allFilters",
        "operation": "update"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "433a549e-b052-451b-9eba-309ceaea800c",
      "name": "Update status and result",
      "type": "n8n-nodes-base.supabase",
      "position": [
        368,
        64
      ],
      "parameters": {
        "filters": {
          "conditions": [
            {
              "keyName": "id",
              "keyValue": "={{ $json.id }}",
              "condition": "eq"
            }
          ]
        },
        "tableId": "openai_batches",
        "fieldsUi": {
          "fieldValues": [
            {
              "fieldId": "updated_at",
              "fieldValue": "={{ $now }}"
            },
            {
              "fieldId": "batch_status",
              "fieldValue": "={{ $json.status }}"
            },
            {
              "fieldId": "output_file_id",
              "fieldValue": "={{ $json.output_file_id }}"
            },
            {
              "fieldId": "result",
              "fieldValue": "={{ $json.result }}"
            }
          ]
        },
        "matchType": "allFilters",
        "operation": "update"
      },
      "credentials": {
        "supabaseApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "602d3950-5d6d-4707-b51d-3e5f2550be8e",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1792,
        -960
      ],
      "parameters": {
        "width": 640,
        "height": 688,
        "content": "# OpenAI Batch API (FIFO with Supabase / Postgres)\n\n### How it works\nThis workflow uses OpenAI's **Batch API** to process multiple prompts at reduced cost, with Supabase as a persistent queue for tracking batch jobs.\n\n**Phase 1 \u2014 Submit:** Takes an array of text inputs and a system prompt, converts them into `.jsonl` batch format, uploads to OpenAI's Files API, submits a batch job, and records it in the `openai_batches` table.\n\n**Phase 2 \u2014 Poll & Retrieve (every 5 mins):** A cron job queries Supabase for the earliest uncompleted batch, checks its status via OpenAI, and either updates the status (if still processing) or downloads, decodes, and stores the completed results back into `openai_batches`.\n\n### Setup steps\n1. **Create the Supabase table** `openai_batches`:\n   - `id` (text, PK) \u2014 OpenAI batch ID\n   - `batch_status` (text, not null)\n   - `output_file_id` (text, nullable)\n   - `result` (text[], nullable) \u2014 array of LLM answers\n   - `created_at` (timestamptz, default now())\n   - `updated_at` (timestamptz, nullable)\n2. Add your **OpenAI API credentials** to all HTTP Request nodes.\n3. Add your **Supabase** and **Postgres** credentials.\n4. Replace mock data in **Start** with your actual `systemPrompt` and `inputs` array.\n\n### Customization\n- Change the model in **Convert to batch requests** (currently `gpt-5-mini`).\n- Adjust cron interval (currently every 5 mins).\n- For strict FIFO, set the Postgres select to `LIMIT 1`."
      },
      "typeVersion": 1
    },
    {
      "id": "16cd7a56-a24b-4e68-81cb-947eb446f75b",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1792,
        -160
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 112,
        "content": "## \ud83d\udce4 Phase 1: Submit Batch\nConverts inputs to `.jsonl`, uploads to OpenAI, creates a batch job, and records it in `openai_batches`."
      },
      "typeVersion": 1
    },
    {
      "id": "46ddb6e7-1571-42a0-b9c8-bac07c947224",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1792,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 112,
        "content": "## \ud83d\udce5 Phase 2: Poll & Retrieve (cron)\nQueries `openai_batches` for uncompleted batches, checks OpenAI status, downloads + decodes results and stores them back if completed."
      },
      "typeVersion": 1
    },
    {
      "id": "b2374aa6-7301-4c46-9465-bd596b95fe8f",
      "name": "Submission done",
      "type": "n8n-nodes-base.noOp",
      "position": [
        -80,
        -160
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "e7977e36-2dbb-47d5-afe5-eaa2165d9f34",
      "name": "Retrieval done",
      "type": "n8n-nodes-base.noOp",
      "position": [
        592,
        64
      ],
      "parameters": {},
      "typeVersion": 1
    }
  ],
  "connections": {
    "Decode base64": {
      "main": [
        [
          {
            "node": "Update status and result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Batch API": {
      "main": [
        [
          {
            "node": "Create a row in batch table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Files API": {
      "main": [
        [
          {
            "node": "Call Batch API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    ".jsonl to base64": {
      "main": [
        [
          {
            "node": "Decode base64",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cron Job (5 mins)": {
      "main": [
        [
          {
            "node": "Get the earliest uncompleted batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start (mock data)": {
      "main": [
        [
          {
            "node": "Convert to batch requests in .jsonl",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If status = completed": {
      "main": [
        [
          {
            "node": "Download .jsonl result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Update status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download .jsonl result": {
      "main": [
        [
          {
            "node": ".jsonl to base64",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update status and result": {
      "main": [
        [
          {
            "node": "Retrieval done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create a row in batch table": {
      "main": [
        [
          {
            "node": "Submission done",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get the earliest uncompleted batch": {
      "main": [
        [
          {
            "node": "Call Batch API to retrieve batch object",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert to batch requests in .jsonl": {
      "main": [
        [
          {
            "node": "Call Files API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Batch API to retrieve batch object": {
      "main": [
        [
          {
            "node": "If status = completed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}