{
  "name": "Inspect document metadata and create an explicit cleaning review",
  "active": false,
  "nodes": [
    {
      "id": "run-manually",
      "name": "Run manually",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "parameters": {}
    },
    {
      "id": "configure-input",
      "name": "Configure input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Edit these values; keep secrets in n8n credentials, not this code.\nconst config = {\n  \"runEnabled\": false,\n  \"resumeRunId\": \"\",\n  \"key\": \"metadata\",\n  \"actor\": \"myworkingmemory/document-metadata-cleaner\",\n  \"actorId\": \"egULX8186JN65aPhU\",\n  \"actorInput\": {\n    \"operation\": \"inspect\",\n    \"documentUrl\": \"https://example.com/replace-with-your-document.pdf\",\n    \"format\": \"auto\"\n  },\n  \"chargeCapUsd\": 0.05,\n  \"resultLimit\": 1,\n  \"storageKey\": \"OUTPUT\"\n};\nfunction validateConfig(config) {\n  if (config.runEnabled !== true) throw new Error('Setup: select your Apify Bearer credential, edit Actor input, then set runEnabled:true in Configure input. No Actor was started.');\n  if (!Number.isFinite(config.chargeCapUsd) || config.chargeCapUsd <= 0 || config.chargeCapUsd > 10) throw new Error('Set chargeCapUsd to a finite value greater than zero and no more than 10 USD for this starter.');\n  if (!Number.isInteger(config.resultLimit) || config.resultLimit < 1 || config.resultLimit > 50) throw new Error('Set resultLimit to 1–50 rows; automatic paging is disabled.');\n  if (typeof config.resumeRunId !== 'string' || (config.resumeRunId && !/^[a-zA-Z0-9]+$/.test(config.resumeRunId))) throw new Error('resumeRunId must be an Apify run ID or an empty string.');\n  if (config.resumeRunId) return config;\n  const a = config.actorInput;\n  if (!a || typeof a !== 'object' || Array.isArray(a)) throw new Error('actorInput must be an object.');\n  if (config.key === 'software' && (!Number.isInteger(a.limit) || a.limit < 1 || a.limit > 10 || !Number.isInteger(a.offset) || a.offset < 0)) throw new Error('Set software limit to 1–10 and offset to a nonnegative integer.');\n  if (config.key === 'software' && a.preview === true && (a.query || a.category || a.platform || a.offset !== 0)) throw new Error('Fixed preview requires blank query/category/platform and offset:0. Set preview:false for a paid search.');\n  if (config.key === 'shopify') {\n    if (!Array.isArray(a.startUrls) || !a.startUrls.length || a.startUrls.length > 2) throw new Error('Use one or two Shopify store URLs.');\n    for (const key of ['maxProducts', 'maxProductsPerStore', 'maxPagesPerStore', 'maxRequests', 'maxRunSeconds']) if (!Number.isFinite(a[key]) || a[key] <= 0) throw new Error(`${key} must be positive; zero can mean unlimited.`);\n    if (a.maxProducts > 25 || a.maxProductsPerStore > 25 || a.maxPagesPerStore > 2 || a.maxRequests > 5 || a.maxRunSeconds > 90) throw new Error('Starter bounds: 25 products, 2 pages per store, 5 requests, 90 seconds.');\n  }\n  if (config.key === 'youtube') {\n    const urls = a.videoUrls || (a.videoUrl ? [a.videoUrl] : []);\n    if (!Array.isArray(urls) || urls.length < 1 || urls.length > 5) throw new Error('Supply videoUrl or videoUrls with 1–5 YouTube videos.');\n  }\n  if (config.key === 'email') {\n    if (a.mv_api_key) throw new Error('Keep mv_api_key in the HTTP Custom Auth credential and set useProviderByok:true; remove the secret from actorInput.');\n    if (!!a.emails === !!a.dataset_id) throw new Error('Supply exactly one of emails or dataset_id.');\n    if (a.emails && (!Array.isArray(a.emails) || a.emails.length < 1 || a.emails.length > 50)) throw new Error('Use 1–50 email input rows in this starter.');\n    if (a.dataset_id) throw new Error('This bounded starter accepts emails directly; the Actor supports dataset_id for a separately sized production run.');\n  }\n  if (config.key === 'metadata') {\n    if (!!a.documentUrl === !!a.documentBase64) throw new Error('Provide exactly one documentUrl or documentBase64.');\n    if (a.documentUrl && (!/^https:\\/\\//i.test(a.documentUrl) || a.documentUrl.includes('example.com/'))) throw new Error('Replace the example URL with your accessible HTTPS PDF/DOCX URL.');\n    if (!['inspect', 'clean'].includes(a.operation)) throw new Error('operation must be inspect or clean.');\n    if (a.operation === 'clean' && !/^[a-f0-9]{64}$/i.test(a.inspectedSha256 || '')) throw new Error('Inspect this document first; set inspectedSha256 from its report before requesting clean.');\n  }\n  return config;\n}\nreturn [{json:validateConfig(config)}];"
      }
    },
    {
      "id": "optional-daily-schedule-disabled",
      "name": "Optional daily schedule disabled",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        0,
        160
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 9,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "disabled": true
    },
    {
      "id": "start-here",
      "name": "Start here",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -760,
        -940
      ],
      "parameters": {
        "content": "### How it works\nFor teams reviewing documents before sharing, this manual workflow starts or resumes one Apify inspection or explicitly requested clean operation, then preserves findings, warnings and verification in a JSON report. An authenticated download retrieves a returned document copy. Inspection does not clean the source; a report is not a guarantee that every identifier is gone.\n\n### Setup\nImport into a blank inactive workflow. Select your Apify HTTP Bearer Auth credential in all four MCP nodes and Download returned document. In Configure input, replace documentUrl with an accessible HTTPS DOCX/PDF URL, or use documentBase64 instead. Keep operation:inspect, set runEnabled:true and execute. Review findings first. For a separate clean request, set operation:clean and inspectedSha256 from the inspection; content-changing cleanup also requires confirmContentChanges:true. Apify pricing differs from RapidAPI: cleaning is $4 on Free or $0.02 on eligible paid plans, checked October 28, 2026. The default $0.05 cap does not cover Free-plan cleaning. Retrieve reports and returned copies from the final node’s Binary tab; n8n costs are separate.\n\nRequires MCP Client 1.1; schemas checked against n8n 2.45.5. Live validation covered synthetic-DOCX inspection; cleaning and returned-document download were not exercised.\n\n### Customization\nIf pending, set resumeRunId to retrieve the same run. No automatic paging or retries. Before enabling the disabled schedule, set your workflow timezone explicitly, choose repeated inputs and clear resumeRunId; file import does not apply workflow settings.",
        "color": 1,
        "height": 860,
        "width": 660
      }
    },
    {
      "id": "resume-existing-run",
      "name": "Resume existing run",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        480,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "Resume existing run",
              "leftValue": "={{ $json.resumeRunId.length > 0 }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "use-existing-run",
      "name": "Use existing run",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        700,
        -120
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{json:{runId:$input.first().json.resumeRunId}}];"
      }
    },
    {
      "id": "start-one-capped-actor-run",
      "name": "Start one capped Actor run",
      "type": "@n8n/n8n-nodes-langchain.mcpClient",
      "typeVersion": 1.1,
      "position": [
        700,
        100
      ],
      "parameters": {
        "serverTransport": "httpStreamable",
        "endpointUrl": "https://mcp.apify.com/?tools=call-actor,get-actor-run,get-dataset-items,get-key-value-store-record",
        "authentication": "bearerAuth",
        "tool": {
          "__rl": true,
          "mode": "id",
          "value": "call-actor"
        },
        "inputMode": "json",
        "jsonInput": "={{ { actor: $('Configure input').first().json.actor, input: $('Configure input').first().json.actorInput, waitSecs: 30, callOptions: { timeout: 120, maxTotalChargeUsd: $('Configure input').first().json.chargeCapUsd } } }}",
        "options": {
          "timeout": 60000,
          "convertToBinary": false
        }
      },
      "retryOnFail": false,
      "notesInFlow": true,
      "notes": "Select your own Apify HTTP Bearer Auth credential. Never paste a token into the URL or workflow JSON."
    },
    {
      "id": "keep-run-identity",
      "name": "Keep run identity",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        940,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "function unwrapMcp(value) {\n  if (value?.error || value?.isError) throw new Error(`MCP error: ${JSON.stringify(value.error || value.content)}`);\n  if (value?.structuredContent && typeof value.structuredContent === 'object') return value.structuredContent;\n  for (const part of value?.content || []) {\n    if (part.type !== 'text') continue;\n    if (part.text && typeof part.text === 'object') return part.text;\n    if (typeof part.text === 'string') { try { const parsed = JSON.parse(part.text); if (parsed && typeof parsed === 'object') return parsed; } catch {} }\n  }\n  return value;\n}\nconst run=unwrapMcp($input.first().json);if(!run.runId)throw new Error('No runId returned. Inspect the last execution and Apify Console before starting again.');return [{json:run}];"
      }
    },
    {
      "id": "wait-for-the-same-run",
      "name": "Wait for the same run",
      "type": "@n8n/n8n-nodes-langchain.mcpClient",
      "typeVersion": 1.1,
      "position": [
        1160,
        0
      ],
      "parameters": {
        "serverTransport": "httpStreamable",
        "endpointUrl": "https://mcp.apify.com/?tools=call-actor,get-actor-run,get-dataset-items,get-key-value-store-record",
        "authentication": "bearerAuth",
        "tool": {
          "__rl": true,
          "mode": "id",
          "value": "get-actor-run"
        },
        "inputMode": "json",
        "jsonInput": "={{ { runId: $json.runId, waitSecs: 45 } }}",
        "options": {
          "timeout": 60000,
          "convertToBinary": false
        }
      },
      "retryOnFail": false,
      "notesInFlow": true,
      "notes": "Select your own Apify HTTP Bearer Auth credential. Never paste a token into the URL or workflow JSON."
    },
    {
      "id": "check-run-and-storage",
      "name": "Check run and storage",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1380,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "function unwrapMcp(value) {\n  if (value?.error || value?.isError) throw new Error(`MCP error: ${JSON.stringify(value.error || value.content)}`);\n  if (value?.structuredContent && typeof value.structuredContent === 'object') return value.structuredContent;\n  for (const part of value?.content || []) {\n    if (part.type !== 'text') continue;\n    if (part.text && typeof part.text === 'object') return part.text;\n    if (typeof part.text === 'string') { try { const parsed = JSON.parse(part.text); if (parsed && typeof parsed === 'object') return parsed; } catch {} }\n  }\n  return value;\n}\nconst run=unwrapMcp($input.first().json);const config=$('Configure input').first().json;if((run.actorId && run.actorId!==config.actorId)||(run.actorName && run.actorName!==config.actor)||(!run.actorId&&!run.actorName))throw new Error('This run belongs to a different Actor or its identity is missing.');if(run.status!=='SUCCEEDED')throw new Error('Actor run '+run.runId+' status '+run.status+'. Do not restart blindly. If still running, set resumeRunId to this ID and execute again to fetch the same run.');const datasetId=run.storages?.datasets?.default?.id||run.storages?.defaultDatasetId||run.defaultDatasetId;const keyValueStoreId=run.storages?.keyValueStores?.default?.id||run.storages?.defaultKeyValueStoreId||run.defaultKeyValueStoreId;if(!datasetId||!keyValueStoreId)throw new Error('Run succeeded but storage IDs are missing; inspect '+run.runId);return [{json:{...run,datasetId,keyValueStoreId}}];"
      }
    },
    {
      "id": "read-bounded-result-rows",
      "name": "Read bounded result rows",
      "type": "@n8n/n8n-nodes-langchain.mcpClient",
      "typeVersion": 1.1,
      "position": [
        1600,
        0
      ],
      "parameters": {
        "serverTransport": "httpStreamable",
        "endpointUrl": "https://mcp.apify.com/?tools=call-actor,get-actor-run,get-dataset-items,get-key-value-store-record",
        "authentication": "bearerAuth",
        "tool": {
          "__rl": true,
          "mode": "id",
          "value": "get-dataset-items"
        },
        "inputMode": "json",
        "jsonInput": "={{ { datasetId: $json.datasetId, limit: $('Configure input').first().json.resultLimit, offset: 0, clean: true } }}",
        "options": {
          "timeout": 60000,
          "convertToBinary": false
        }
      },
      "retryOnFail": false,
      "notesInFlow": true,
      "notes": "Select your own Apify HTTP Bearer Auth credential. Never paste a token into the URL or workflow JSON."
    },
    {
      "id": "read-coverage-report",
      "name": "Read coverage report",
      "type": "@n8n/n8n-nodes-langchain.mcpClient",
      "typeVersion": 1.1,
      "position": [
        1820,
        0
      ],
      "parameters": {
        "serverTransport": "httpStreamable",
        "endpointUrl": "https://mcp.apify.com/?tools=call-actor,get-actor-run,get-dataset-items,get-key-value-store-record",
        "authentication": "bearerAuth",
        "tool": {
          "__rl": true,
          "mode": "id",
          "value": "get-key-value-store-record"
        },
        "inputMode": "json",
        "jsonInput": "={{ { keyValueStoreId: $('Check run and storage').first().json.keyValueStoreId, recordKey: $('Configure input').first().json.storageKey } }}",
        "options": {
          "timeout": 60000,
          "convertToBinary": false
        }
      },
      "retryOnFail": false,
      "notesInFlow": true,
      "notes": "Select your own Apify HTTP Bearer Auth credential. Never paste a token into the URL or workflow JSON."
    },
    {
      "id": "build-useful-output",
      "name": "Build useful output",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2040,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "function unwrapMcp(value) {\n  if (value?.error || value?.isError) throw new Error(`MCP error: ${JSON.stringify(value.error || value.content)}`);\n  if (value?.structuredContent && typeof value.structuredContent === 'object') return value.structuredContent;\n  for (const part of value?.content || []) {\n    if (part.type !== 'text') continue;\n    if (part.text && typeof part.text === 'object') return part.text;\n    if (typeof part.text === 'string') { try { const parsed = JSON.parse(part.text); if (parsed && typeof parsed === 'object') return parsed; } catch {} }\n  }\n  return value;\n}\n\nfunction csv(rows, columns) {\n  const cell = value => {\n    let text = value == null ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value);\n    // Untrusted source fields must not become spreadsheet formulas when opened.\n    if (/^[\\s]*[=+@\\-]|^[\\t\\r\\n]/.test(text)) text = \"'\" + text;\n    return '\"' + text.replaceAll('\"', '\"\"') + '\"';\n  };\n  return [columns.map(cell).join(','), ...rows.map(row => columns.map(key => cell(row[key])).join(','))].join('\\r\\n');\n}\n\nfunction validateConfig(config) {\n  if (config.runEnabled !== true) throw new Error('Setup: select your Apify Bearer credential, edit Actor input, then set runEnabled:true in Configure input. No Actor was started.');\n  if (!Number.isFinite(config.chargeCapUsd) || config.chargeCapUsd <= 0 || config.chargeCapUsd > 10) throw new Error('Set chargeCapUsd to a finite value greater than zero and no more than 10 USD for this starter.');\n  if (!Number.isInteger(config.resultLimit) || config.resultLimit < 1 || config.resultLimit > 50) throw new Error('Set resultLimit to 1–50 rows; automatic paging is disabled.');\n  if (typeof config.resumeRunId !== 'string' || (config.resumeRunId && !/^[a-zA-Z0-9]+$/.test(config.resumeRunId))) throw new Error('resumeRunId must be an Apify run ID or an empty string.');\n  if (config.resumeRunId) return config;\n  const a = config.actorInput;\n  if (!a || typeof a !== 'object' || Array.isArray(a)) throw new Error('actorInput must be an object.');\n  if (config.key === 'software' && (!Number.isInteger(a.limit) || a.limit < 1 || a.limit > 10 || !Number.isInteger(a.offset) || a.offset < 0)) throw new Error('Set software limit to 1–10 and offset to a nonnegative integer.');\n  if (config.key === 'software' && a.preview === true && (a.query || a.category || a.platform || a.offset !== 0)) throw new Error('Fixed preview requires blank query/category/platform and offset:0. Set preview:false for a paid search.');\n  if (config.key === 'shopify') {\n    if (!Array.isArray(a.startUrls) || !a.startUrls.length || a.startUrls.length > 2) throw new Error('Use one or two Shopify store URLs.');\n    for (const key of ['maxProducts', 'maxProductsPerStore', 'maxPagesPerStore', 'maxRequests', 'maxRunSeconds']) if (!Number.isFinite(a[key]) || a[key] <= 0) throw new Error(`${key} must be positive; zero can mean unlimited.`);\n    if (a.maxProducts > 25 || a.maxProductsPerStore > 25 || a.maxPagesPerStore > 2 || a.maxRequests > 5 || a.maxRunSeconds > 90) throw new Error('Starter bounds: 25 products, 2 pages per store, 5 requests, 90 seconds.');\n  }\n  if (config.key === 'youtube') {\n    const urls = a.videoUrls || (a.videoUrl ? [a.videoUrl] : []);\n    if (!Array.isArray(urls) || urls.length < 1 || urls.length > 5) throw new Error('Supply videoUrl or videoUrls with 1–5 YouTube videos.');\n  }\n  if (config.key === 'email') {\n    if (a.mv_api_key) throw new Error('Keep mv_api_key in the HTTP Custom Auth credential and set useProviderByok:true; remove the secret from actorInput.');\n    if (!!a.emails === !!a.dataset_id) throw new Error('Supply exactly one of emails or dataset_id.');\n    if (a.emails && (!Array.isArray(a.emails) || a.emails.length < 1 || a.emails.length > 50)) throw new Error('Use 1–50 email input rows in this starter.');\n    if (a.dataset_id) throw new Error('This bounded starter accepts emails directly; the Actor supports dataset_id for a separately sized production run.');\n  }\n  if (config.key === 'metadata') {\n    if (!!a.documentUrl === !!a.documentBase64) throw new Error('Provide exactly one documentUrl or documentBase64.');\n    if (a.documentUrl && (!/^https:\\/\\//i.test(a.documentUrl) || a.documentUrl.includes('example.com/'))) throw new Error('Replace the example URL with your accessible HTTPS PDF/DOCX URL.');\n    if (!['inspect', 'clean'].includes(a.operation)) throw new Error('operation must be inspect or clean.');\n    if (a.operation === 'clean' && !/^[a-f0-9]{64}$/i.test(a.inspectedSha256 || '')) throw new Error('Inspect this document first; set inspectedSha256 from its report before requesting clean.');\n  }\n  return config;\n}\n\nfunction normalizeResult(key, dataset, output, config, run) {\n  const rawRows = Array.isArray(dataset?.items) ? dataset.items : [];\n  const base = { actor: config.actor, runId: run.runId, runStatus: run.status, capturedAt: new Date().toISOString(), retrievedRows: rawRows.length, totalDatasetRows: dataset.totalItemCount ?? rawRows.length, truncated: Number(dataset.totalItemCount || 0) > rawRows.length, actorReport: output, nextOffset: null };\n  if (key === 'software') {\n    const envelope = output && Array.isArray(output.records) ? output : rawRows.find(r => Array.isArray(r.records));\n    const rows = envelope?.records || rawRows;\n    const shortlist = rows.map(r => ({ id: r.id ?? '', name: r.name ?? '', purpose: r.purpose ?? '', category: r.category ?? '', type: r.type ?? '', platforms: r.platforms ?? [], sourceURL: r.sourceURL ?? '', getSoftwareURL: r.getSoftwareURL ?? '', licenseDeclaration: r.licenseDeclaration ?? '', licenseURL: r.licenseURL ?? '', requirements: r.requirements ?? '', reviewedAt: r.reviewedAt ?? '', evidenceLimits: r.evidenceLimits ?? [] }));\n    return { ...base, mode: config.actorInput.preview ? 'fixed_sample_preview' : 'search_page', query: config.actorInput.query || '', totalMatches: envelope?.total ?? output?.total ?? shortlist.length, nextOffset: envelope?.nextOffset ?? output?.nextOffset ?? null, shortlist, csv: csv(shortlist, ['name','purpose','category','type','platforms','sourceURL','getSoftwareURL','licenseDeclaration','licenseURL','requirements','reviewedAt','evidenceLimits']), nextAction: 'Review linked sources, requirements and license terms before choosing software; no install or next-page request is automatic.' };\n  }\n  if (key === 'shopify') {\n    const products = rawRows.map(r => ({ recordKey: r.recordKey ?? String(r.id ?? ''), title: r.title ?? '', url: r.url ?? '', currency: r.currency ?? '', priceMin: r.priceMin ?? null, priceMax: r.priceMax ?? null, available: r.available ?? null, variantCount: r.variantCount ?? r.variants?.length ?? null, variantCoverage: r.variantCoverage ?? null, scrapedAt: r.scrapedAt ?? '', variants: r.variants ?? [] }));\n    return { ...base, products, coverageStatus: output?.status || 'see_actor_report', coverageWarnings: output?.warnings || [], resume: output?.resume ?? null, availableCount: products.filter(r => r.available === true).length, unavailableCount: products.filter(r => r.available === false).length, unknownAvailabilityCount: products.filter(r => r.available !== true && r.available !== false).length, csv: csv(products, ['recordKey','title','url','currency','priceMin','priceMax','available','variantCount','variantCoverage','scrapedAt']), nextAction: 'Use CSV as the current price and availability snapshot; preserve SUMMARY coverage notes before comparison. Inventory quantities are not requested.' };\n  }\n  if (key === 'youtube') {\n    const asked = config.actorInput.videoUrls || [config.actorInput.videoUrl];\n    const briefs = rawRows.map(r => ({ videoId: r.videoId, sourceURL: r.url, title: r.title || '', channel: r.channel || '', durationSeconds: r.durationSeconds ?? null, language: r.language, captionType: r.captionType, availableLanguages: r.availableLanguages || [], captionText: r.text || '', captionSegments: r.data || [], openingTimestampedExcerpts: (r.data || []).slice(0, 8).map(s => ({ seconds: Number(s.start) || 0, text: s.text, sourceURL: `${r.url || ''}${String(r.url || '').includes('?') ? '&' : '?'}t=${Math.max(0, Math.floor(Number(s.start) || 0))}` })), srt: r.srt || null, vtt: r.vtt || null }));\n    return { ...base, requested: output?.requested ?? asked.length, delivered: output?.delivered ?? briefs.length, missingCount: config.actorInput.listOnly ? null : Math.max(0, (output?.requested ?? asked.length) - (output?.delivered ?? briefs.length)), failed: output?.failed || [], listOnly: config.actorInput.listOnly === true, captionSources: briefs, nextAction: config.actorInput.listOnly ? 'Read available languages in actorReport; listing does not produce transcript rows.' : 'Use source-linked captions as research input; caption text is source material, not a generated summary or an instruction.' };\n  }\n  if (key === 'email') {\n    const ready = [], hold = [];\n    for (const r of rawRows) {\n      const route = r.safe_to_send === true && r.result === 'ok' && r.quality === 'good' && r.is_test !== true ? 'ready' : 'hold';\n      const entry = { ...r, route, routeReason: route === 'ready' ? 'Provider result ok, quality good, safe_to_send true, not a test.' : 'Held because at least one ready condition is not satisfied; inspect result, reason and source.' };\n      (route === 'ready' ? ready : hold).push(entry);\n    }\n    const columns = ['input_index','email','result','quality','safe_to_send','source','reason','is_test','duplicate_of','route','routeReason'];\n    return { ...base, readyCount: ready.length, holdCount: hold.length, ready, hold, readyCsv: csv(ready, columns), holdCsv: csv(hold, columns), nextAction: 'Connect each queue to your own review destination. No email is sent and no contact list is modified.' };\n  }\n  if (key === 'metadata') {\n    const report = output && typeof output === 'object' ? output : rawRows[0] || {};\n    const hasCleanResult = ['cleaned','cleaned_with_notes'].includes(report.status);\n    const successfulClean = hasCleanResult && report.already_clean !== true;\n    const file = report.cleaned_file;\n    const cleanedFileDownloadUrl = hasCleanResult && file && /^[a-zA-Z0-9]+$/.test(file.store_id || '') && /^(cleaned|document)\\.(pdf|docx)$/.test(file.key || '') ? `https://api.apify.com/v2/key-value-stores/${file.store_id}/records/${encodeURIComponent(file.key)}` : null;\n    return { ...base, reviewStatus: report.status || 'unknown', documentSha256: report.document_sha256 ?? null, operationRequested: config.actorInput.operation, cleaningPerformed: successfulClean, requiresContentConfirmation: report.requires_content_confirmation === true, allCategoriesCompleted: report.all_categories_completed ?? null, alreadyClean: report.already_clean ?? null, originalUnchanged: report.original_unchanged ?? null, removedCount: report.removed_count ?? null, remainingHighRisk: report.high_risk_remaining || [], verification: report.verification ?? null, findings: report.findings || report.found_before || [], removed: report.removed || [], warnings: report.warnings || [], notes: report.notes || [], cleanedFile: file ?? null, cleanedFileDownloadUrl, nextAction: report.already_clean === true ? 'No cleaning changes were needed; any returned document is an unchanged copy. Review the inspection findings.' : successfulClean ? 'Retrieve the separate cleaned_file with your Apify credential and review verification, warnings and remaining findings.' : report.requires_content_confirmation ? 'Review the content changes and inspect hash; a separate explicit clean request can set confirmContentChanges:true with the matching inspectedSha256.' : 'Review findings and the inspect hash before explicitly choosing clean; no automatic cleaning occurs.' };\n  }\n  throw new Error(`Unknown workflow result kind: ${key}`);\n}\n\nfunction resultItem(result) {\n  const binary = {};\n  const attach = (key, text, fileName, mimeType) => { binary[key] = { data: Buffer.from(text,'utf8').toString('base64'), fileName, mimeType }; };\n  attach('report', JSON.stringify(result,null,2), 'result-report.json', 'application/json');\n  if (typeof result.csv === 'string') attach('csv',result.csv,'results.csv','text/csv');\n  if (typeof result.readyCsv === 'string') attach('ready',result.readyCsv,'email-ready.csv','text/csv');\n  if (typeof result.holdCsv === 'string') attach('hold',result.holdCsv,'email-hold.csv','text/csv');\n  for (const [index, source] of (result.captionSources || []).entries()) if (source.srt) attach(`captions_${index+1}`,source.srt,`captions-${index+1}.srt`,'application/x-subrip');\n  return {json:result,binary};\n}\nconst config=$('Configure input').first().json;const dataset=unwrapMcp($('Read bounded result rows').first().json);const record=unwrapMcp($input.first().json);const output=record.value??record;const run=$('Check run and storage').first().json;return [resultItem(normalizeResult(config.key,dataset,output,config,run))];"
      }
    },
    {
      "id": "document-copy-available",
      "name": "Document copy available",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2260,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "Document copy available",
              "leftValue": "={{ Boolean($json.cleanedFileDownloadUrl) }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "download-returned-document",
      "name": "Download returned document",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2480,
        -100
      ],
      "parameters": {
        "url": "={{ $json.cleanedFileDownloadUrl }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth",
        "options": {
          "timeout": 60000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "documentCopy"
            }
          }
        }
      },
      "retryOnFail": false,
      "notes": "Select your Apify Bearer credential. The URL is constructed from validated store ID/key on api.apify.com; redirects are disabled."
    },
    {
      "id": "report-and-document-copy",
      "name": "Report and document copy",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2700,
        -100
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const report=$('Build useful output').first();const download=$input.first();return [{json:report.json,binary:{...report.binary,...download.binary}}];"
      }
    },
    {
      "id": "return-inspection-report",
      "name": "Return inspection report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2480,
        120
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return $input.all();"
      }
    },
    {
      "id": "input-section",
      "name": "Input section",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -40,
        -130
      ],
      "parameters": {
        "content": "## Configure and validate\nChoose the intended inputs and limits before enabling execution. Keep credentials in n8n. The optional schedule stays disabled during setup.",
        "color": 7,
        "width": 460,
        "height": 410
      }
    },
    {
      "id": "execution-section",
      "name": "Execution section",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        440,
        -280
      ],
      "parameters": {
        "content": "## Start once or retrieve the same run\nStart one capped Actor or resume its existing ID. Check its identity and status before reading results; a pending run needs manual resumption, not an automatic new paid start.",
        "color": 7,
        "width": 1100,
        "height": 500
      }
    },
    {
      "id": "results-section",
      "name": "Results section",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1560,
        -180
      ],
      "parameters": {
        "content": "## Read coverage and prepare the result\nPreserve inspection status, document hash, findings, warnings and verification. Do not treat confirmation_required, partial inspection or already-clean output as a successful removal claim.",
        "color": 7,
        "width": 660,
        "height": 400
      }
    },
    {
      "id": "document-delivery-section",
      "name": "Document delivery section",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2220,
        -240
      ],
      "parameters": {
        "content": "## Return the report and any document copy\nOnly a returned, validated file reference triggers an authenticated download. Inspection or refused cleaning returns the report alone. Review warnings and remaining findings before using a copy.",
        "color": 7,
        "width": 780,
        "height": 500
      }
    }
  ],
  "connections": {
    "Run manually": {
      "main": [
        [
          {
            "node": "Configure input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Optional daily schedule disabled": {
      "main": [
        [
          {
            "node": "Configure input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure input": {
      "main": [
        [
          {
            "node": "Resume existing run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resume existing run": {
      "main": [
        [
          {
            "node": "Use existing run",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Start one capped Actor run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Use existing run": {
      "main": [
        [
          {
            "node": "Keep run identity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start one capped Actor run": {
      "main": [
        [
          {
            "node": "Keep run identity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep run identity": {
      "main": [
        [
          {
            "node": "Wait for the same run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for the same run": {
      "main": [
        [
          {
            "node": "Check run and storage",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check run and storage": {
      "main": [
        [
          {
            "node": "Read bounded result rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read bounded result rows": {
      "main": [
        [
          {
            "node": "Read coverage report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read coverage report": {
      "main": [
        [
          {
            "node": "Build useful output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build useful output": {
      "main": [
        [
          {
            "node": "Document copy available",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Document copy available": {
      "main": [
        [
          {
            "node": "Download returned document",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Return inspection report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download returned document": {
      "main": [
        [
          {
            "node": "Report and document copy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 300,
    "timezone": "America/Chicago",
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "none",
    "saveManualExecutions": false
  },
  "pinData": {},
  "tags": []
}
