{
  "name": "Route verified email results into ready and hold queues",
  "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\": \"email\",\n  \"actor\": \"myworkingmemory/email-verifier-bulk\",\n  \"actorId\": \"Gs5JXep3adEv7Lhpf\",\n  \"actorInput\": {\n    \"emails\": [\n      \"not-an-email\"\n    ],\n    \"verification_mode\": \"auto\",\n    \"mxFallback\": true\n  },\n  \"chargeCapUsd\": 0.05,\n  \"resultLimit\": 50,\n  \"storageKey\": \"OUTPUT\",\n  \"useProviderByok\": false\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 contact data, this manual workflow validates up to 50 addresses, starts or resumes one Apify run, and produces ready/hold queues, two CSV files and a JSON report. Ready requires safe_to_send:true, result:ok, quality:good and no test flag. Input order and duplicates remain traceable. No email is sent.\n\n### Setup\nImport into a blank inactive workflow. Select your Apify HTTP Bearer Auth credential in all four MCP nodes. Replace the default syntax diagnostic in Configure input. For BYOK, set useProviderByok:true and select HTTP Custom Auth in Start BYOK email run: its encrypted JSON must contain an Authorization header with Bearer plus your Apify token, and body.mv_api_key with your MillionVerifier key. Use the same Apify account for both credentials; never put keys in Code. With useProviderByok:false, provider checks use the limited pilot only if available. Set runEnabled:true, execute, and download files from Build useful output → Binary. The $0.05 cap covers applicable Actor events; supplier credits and n8n costs are separate.\n\nRequires MCP Client 1.1; schemas checked against n8n 2.45.5. Live validation covered invalid-email routing and downloads; BYOK and provider verification 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": "use-provider-byok",
      "name": "Use provider BYOK",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        700,
        320
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "Use provider BYOK",
              "leftValue": "={{ $json.useProviderByok === true }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "start-byok-email-run",
      "name": "Start BYOK email run",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        940,
        360
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.apify.com/v2/acts/myworkingmemory~email-verifier-bulk/runs",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpCustomAuth",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "waitForFinish",
              "value": "30"
            },
            {
              "name": "timeout",
              "value": "120"
            },
            {
              "name": "maxTotalChargeUsd",
              "value": "={{ $('Configure input').first().json.chargeCapUsd }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ $('Configure input').first().json.actorInput }}",
        "options": {
          "timeout": 60000,
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "retryOnFail": false,
      "notesInFlow": true,
      "notes": "Create an encrypted HTTP Custom Auth credential with JSON {\"headers\":{\"Authorization\":\"Bearer YOUR_APIFY_TOKEN\"},\"body\":{\"mv_api_key\":\"YOUR_MILLIONVERIFIER_KEY\"}}. Enter real values only in Credentials. The request body merges this secret at execution. Use the same Apify account as the MCP read credentials."
    },
    {
      "id": "keep-byok-run-identity",
      "name": "Keep BYOK run identity",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1160,
        360
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const run=$input.first().json.data;if(!run?.id)throw new Error('No REST run ID returned; inspect Apify Console before retrying.');return [{json:{runId:run.id,actorId:run.actId,status:run.status}}];"
      }
    },
    {
      "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\nKeep explicit ready/hold reasons and original input indexes. Download both queues as CSV plus the JSON report. Neither queue sends messages or changes contacts.",
        "color": 7,
        "width": 660,
        "height": 400
      }
    },
    {
      "id": "provider-credential-section",
      "name": "Provider credential section",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        660,
        240
      ],
      "parameters": {
        "content": "## Choose one verification provider path\nBYOK uses an encrypted HTTP Custom Auth credential for both Apify authorization and the provider key. The other branch uses the finite pilot when eligible and available; supplier credits are separate.",
        "color": 7,
        "width": 700,
        "height": 330
      }
    }
  ],
  "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": "Use provider BYOK",
            "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
          }
        ]
      ]
    },
    "Use provider BYOK": {
      "main": [
        [
          {
            "node": "Start BYOK email run",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Start one capped Actor run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start BYOK email run": {
      "main": [
        [
          {
            "node": "Keep BYOK run identity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep BYOK run identity": {
      "main": [
        [
          {
            "node": "Keep run identity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 300,
    "timezone": "America/Chicago",
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "none",
    "saveManualExecutions": false
  },
  "pinData": {},
  "tags": []
}
