{
  "name": "Supplier PDF to CSV — Collect an existing Apify run",
  "nodes": [
    {
      "id": "collector-1",
      "name": "Collect existing run",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        200
      ],
      "parameters": {}
    },
    {
      "id": "collector-2",
      "name": "Run ID",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        200
      ],
      "parameters": {
        "jsCode": "// Copy an EXISTING run ID from Apify Console or your saved client state.\nreturn [{ json: { runId: 'PASTE_EXISTING_RUN_ID' } }];"
      }
    },
    {
      "id": "collector-3",
      "name": "Validate input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        200
      ],
      "parameters": {
        "jsCode": "return await (function initialize() {\n  const items = $input.all();\n  if (items.length !== 1) throw new Error('Collect one existing run per execution.');\n  const runId = items[0].json.runId;\n  if (typeof runId !== 'string' || !/^[A-Za-z0-9]{17}$/.test(runId)) {\n    throw new Error('Set runId to the 17-character ID of an existing Supplier Price List to CSV run.');\n  }\n  return [{ json: { runId, actorId: 'HIlb5I6IftACciGRI', maxChecks: 18 } }];\n}).call(this);"
      }
    },
    {
      "id": "collector-4",
      "name": "Read existing run",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        200
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.apify.com/v2/actor-runs/' + $('Validate input').first().json.runId }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "responseFormat": "json",
              "outputPropertyName": "body",
              "neverError": false
            }
          }
        }
      },
      "retryOnFail": false,
      "onError": "stopWorkflow",
      "notesInFlow": true,
      "notes": "Select your protected Apify Header Auth credential. Reads only; API/storage usage may apply."
    },
    {
      "id": "collector-5",
      "name": "Check run",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        200
      ],
      "parameters": {
        "jsCode": "return await (function checkRun() {\n  const ctx = $('Validate input').first().json;\n  const response = $input.first().json;\n  if (response.statusCode !== 200) throw new Error('Run lookup failed. Resume this same run ID later.');\n  const run = response.body?.data;\n  if (!run || run.id !== ctx.runId || run.actId !== ctx.actorId) {\n    throw new Error('Run or Actor identity mismatch. No output will be collected.');\n  }\n  const checks = $runIndex + 1;\n  if (run.status === 'SUCCEEDED') {\n    if (!/^[A-Za-z0-9]{17}$/.test(run.defaultKeyValueStoreId || '')) {\n      throw new Error('Missing or invalid output store ID.');\n    }\n    return [{ json: { ...ctx, checks, status: run.status, storeId: run.defaultKeyValueStoreId } }];\n  }\n  const pending = ['READY', 'RUNNING', 'TIMING-OUT', 'ABORTING'];\n  if (!pending.includes(run.status)) {\n    throw new Error('Existing run ended with ' + String(run.status) + '. This workflow never starts a replacement.');\n  }\n  if (checks >= ctx.maxChecks) {\n    throw new Error('Collection paused after 18 checks. Resume the SAME run ID later; the cloud run was not stopped.');\n  }\n  return [{ json: { ...ctx, checks, status: run.status } }];\n}).call(this);"
      }
    },
    {
      "id": "collector-6",
      "name": "Run succeeded?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1100,
        200
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "success",
              "leftValue": "={{ $json.status }}",
              "rightValue": "SUCCEEDED",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "collector-7",
      "name": "Wait 10 seconds",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1100,
        420
      ],
      "parameters": {
        "resume": "timeInterval",
        "amount": 10,
        "unit": "seconds"
      },
      "webhookId": "0b9fd67d-2312-41f3-ae7f-c21b4ca4159e"
    },
    {
      "id": "collector-8",
      "name": "Successful run",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1320,
        100
      ],
      "parameters": {}
    },
    {
      "id": "collector-9",
      "name": "Read SUMMARY",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1540,
        100
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.apify.com/v2/key-value-stores/' + $('Successful run').first().json.storeId + '/records/SUMMARY' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "responseFormat": "json",
              "outputPropertyName": "body",
              "neverError": false
            }
          }
        }
      },
      "retryOnFail": false,
      "onError": "stopWorkflow",
      "notesInFlow": true,
      "notes": "Select your protected Apify Header Auth credential. Reads only; API/storage usage may apply."
    },
    {
      "id": "collector-10",
      "name": "Read REVIEW",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1760,
        100
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.apify.com/v2/key-value-stores/' + $('Successful run').first().json.storeId + '/records/REVIEW' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "responseFormat": "json",
              "outputPropertyName": "body",
              "neverError": false
            }
          }
        }
      },
      "retryOnFail": false,
      "onError": "stopWorkflow",
      "notesInFlow": true,
      "notes": "Select your protected Apify Header Auth credential. Reads only; API/storage usage may apply."
    },
    {
      "id": "collector-11",
      "name": "Read DOCUMENTS",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1980,
        100
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.apify.com/v2/key-value-stores/' + $('Successful run').first().json.storeId + '/records/DOCUMENTS' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "responseFormat": "json",
              "outputPropertyName": "body",
              "neverError": false
            }
          }
        }
      },
      "retryOnFail": false,
      "onError": "stopWorkflow",
      "notesInFlow": true,
      "notes": "Select your protected Apify Header Auth credential. Reads only; API/storage usage may apply."
    },
    {
      "id": "collector-12",
      "name": "Read PRODUCTS.csv",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2200,
        100
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.apify.com/v2/key-value-stores/' + $('Successful run').first().json.storeId + '/records/PRODUCTS.csv' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "responseFormat": "text",
              "outputPropertyName": "body",
              "neverError": false
            }
          }
        }
      },
      "retryOnFail": false,
      "onError": "stopWorkflow",
      "notesInFlow": true,
      "notes": "Select your protected Apify Header Auth credential. Reads only; API/storage usage may apply."
    },
    {
      "id": "collector-13",
      "name": "Validated export",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2420,
        100
      ],
      "parameters": {
        "jsCode": "return await (async function exportResult() {\n  const ctx = $('Successful run').first().json;\n  function body(name) {\n    const r = $(name).first().json;\n    if (r.statusCode !== 200) throw new Error(name + ' was not available. Resume the same run ID.');\n    return r.body;\n  }\n  const summary = body('Read SUMMARY');\n  const review = body('Read REVIEW');\n  const documents = body('Read DOCUMENTS');\n  const csv = body('Read PRODUCTS.csv');\n  if (!summary || typeof summary !== 'object' || Array.isArray(summary) ||\n      !Number.isInteger(summary.delivered_rows) || summary.delivered_rows < 0 || summary.delivered_rows > 5000) {\n    throw new Error('SUMMARY must contain an integer delivered_rows from 0 to 5000.');\n  }\n  if (!Array.isArray(review) || !Array.isArray(documents)) throw new Error('Invalid diagnostics format.');\n  if (typeof csv !== 'string' || Buffer.byteLength(csv, 'utf8') > 25 * 1024 * 1024) {\n    throw new Error('Missing CSV or export exceeds this collector\\'s 25 MiB processing limit.');\n  }\n  // Strict CSV parser: quoted commas/newlines, doubled quotes, CRLF, BOM.\n  // Do not split on newlines: supplier descriptions may themselves contain them.\n  const text = csv.replace(/^\\uFEFF/, '');\n  const rows = [];\n  let row = [], cell = '', quoted = false, closed = false;\n  function field() { row.push(cell); cell = ''; closed = false; }\n  function record() { field(); rows.push(row); row = []; }\n  for (let i = 0; i < text.length; i++) {\n    const ch = text[i];\n    if (quoted) {\n      if (ch === '\"') {\n        if (text[i + 1] === '\"') { cell += '\"'; i++; }\n        else { quoted = false; closed = true; }\n      } else cell += ch;\n      continue;\n    }\n    if (ch === ',') field();\n    else if (ch === '\\r' || ch === '\\n') {\n      if (ch === '\\r' && text[i + 1] === '\\n') i++;\n      record();\n    } else if (ch === '\"' && cell === '' && !closed) quoted = true;\n    else {\n      if (closed || ch === '\"') throw new Error('Malformed CSV quoting.');\n      cell += ch;\n    }\n  }\n  if (quoted) throw new Error('Unterminated CSV quoted field.');\n  if (row.length || cell.length || closed) record();\n  const header = rows.shift();\n  if (!header || new Set(header).size !== header.length ||\n      !['sku', 'description', 'price', 'currency'].every(key => header.includes(key))) {\n    throw new Error('CSV lacks the expected unique product headers.');\n  }\n  if (rows.some(r => r.length !== header.length)) throw new Error('CSV column count mismatch.');\n  if (rows.length !== summary.delivered_rows) throw new Error('CSV record count differs from delivered_rows.');\n  const flags = [];\n  if (summary.budget_stopped) flags.push('spending_limit');\n  if (review.length) flags.push('review_records');\n  if (summary.status !== 'ok') flags.push('summary_status_' + String(summary.status || 'unknown'));\n  if (Array.isArray(summary.issues) && summary.issues.length) flags.push('summary_issues');\n  if (documents.some(d => !d || ['failed', 'error'].includes(d.status))) flags.push('document_failures');\n  if (rows.some(r => r.some(v => /^[\\t\\r\\n ]*[=+@-]/.test(v)))) flags.push('spreadsheet_formula_like_text');\n  const diagnostic = {\n    runId: ctx.runId, actorId: ctx.actorId, deliveredRows: rows.length,\n    collectionOnly: true, destinationWriteApproved: false,\n    completeness: 'Not independently established; validate destination mapping and exception policy.',\n    flags, summary, review, documents,\n  };\n  return [{\n    json: diagnostic,\n    binary: {\n      products: await this.helpers.prepareBinaryData(Buffer.from(csv, 'utf8'), 'PRODUCTS.csv', 'text/csv'),\n      diagnostics: await this.helpers.prepareBinaryData(Buffer.from(JSON.stringify(diagnostic, null, 2), 'utf8'), 'DIAGNOSTICS.json', 'application/json'),\n    },\n  }];\n}).call(this);"
      }
    },
    {
      "id": "collector-14",
      "name": "Setup and limits",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -260
      ],
      "parameters": {
        "width": 610,
        "height": 390,
        "content": "## Supplier CSV collector — offline-tested template\n1. Set the existing run ID in **Run ID**.\n2. Select your protected **Header Auth** credential on all five HTTP nodes (Authorization: Bearer YOUR_TOKEN). Never paste a token here.\n3. Execute manually, then download PRODUCTS.csv and DIAGNOSTICS.json from **Validated export**.\n\nThis workflow never starts an Actor. Repeat collection with the SAME run ID after an error; normal API/storage usage may apply. No scheduler or store/ERP writes are included. CSV count checks do not establish catalog completeness.\n\nThe template was tested with simulated responses, not imported or executed in a customer n8n instance. Full setup and limitations are in the accompanying guide."
      }
    }
  ],
  "connections": {
    "Collect existing run": {
      "main": [
        [
          {
            "node": "Run ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run ID": {
      "main": [
        [
          {
            "node": "Validate input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate input": {
      "main": [
        [
          {
            "node": "Read existing run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read existing run": {
      "main": [
        [
          {
            "node": "Check run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check run": {
      "main": [
        [
          {
            "node": "Run succeeded?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run succeeded?": {
      "main": [
        [
          {
            "node": "Successful run",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait 10 seconds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 10 seconds": {
      "main": [
        [
          {
            "node": "Read existing run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Successful run": {
      "main": [
        [
          {
            "node": "Read SUMMARY",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read SUMMARY": {
      "main": [
        [
          {
            "node": "Read REVIEW",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read REVIEW": {
      "main": [
        [
          {
            "node": "Read DOCUMENTS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read DOCUMENTS": {
      "main": [
        [
          {
            "node": "Read PRODUCTS.csv",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read PRODUCTS.csv": {
      "main": [
        [
          {
            "node": "Validated export",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 900,
    "saveManualExecutions": true
  },
  "tags": []
}
