{
  "name": "CISA KEV watchlist → analyst review rows",
  "active": false,
  "nodes": [
    {
      "id": "manual",
      "name": "Start manually",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        300
      ],
      "parameters": {}
    },
    {
      "id": "settings",
      "name": "Edit small input",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        240,
        300
      ],
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"existingRunId\": \"\",\n  \"actorInput\": {\n    \"vendors\": [],\n    \"products\": [],\n    \"cveIds\": [\n      \"CVE-2021-44228\",\n      \"CVE-2023-27350\"\n    ],\n    \"includeEpss\": false,\n    \"maxResults\": 5,\n    \"comparePrevious\": true,\n    \"onlyChanges\": false,\n    \"snapshotName\": \"n8n-kev-manual-v1\",\n    \"maxRunSeconds\": 120\n  }\n}",
        "options": {}
      }
    },
    {
      "id": "validate",
      "name": "Validate settings",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const validateConfig = function validateConfig(value, actorId, kind) {\n  const object = v => v && typeof v === 'object' && !Array.isArray(v);\n  if (!object(value) || !object(value.actorInput)) throw Error('Configure one Actor input object.');\n  const input = value.actorInput;\n  const limit = kind === 'permits' ? input.maxResultsPerCity * input.cities?.length : input.maxResults;\n  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 25) throw Error('This starter template supports 1–25 total rows. Keep a small explicit result cap.');\n  if (!Number.isSafeInteger(input.maxRunSeconds) || input.maxRunSeconds < 30 || input.maxRunSeconds > 120) throw Error('Keep maxRunSeconds between 30 and 120; this template launches with a 180-second platform timeout.');\n  if (kind === 'permits' && (!Array.isArray(input.cities) || !input.cities.length || new Set(input.cities).size !== input.cities.length || input.cities.some(x => !['chicago','san_francisco','new_york','austin','seattle','los_angeles','baton_rouge','cincinnati','mesa','marin_county'].includes(x)))) throw Error('Select explicit supported permit cities with no duplicates.');\n  const existingRunId = value.existingRunId ?? '';\n  if (typeof existingRunId !== 'string' || existingRunId && !/^[A-Za-z0-9]{17}$/.test(existingRunId)) throw Error('existingRunId must be blank or an exact 17-character Apify run ID.');\n  return { actorId, kind, actorInput: input, rowLimit: limit, existingRunId, startedAtMs: Date.now() };\n};\nreturn [{json:validateConfig($input.first().json,\"maBtzREXN4v3DFSGW\",\"cisa\")}];"
      }
    },
    {
      "id": "new",
      "name": "Start a new run?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        720,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "new-condition",
              "leftValue": "={{ !$json.existingRunId }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "launch",
      "name": "Launch once - costs apply",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        960,
        200
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.apify.com/v2/acts/maBtzREXN4v3DFSGW/runs?timeout=180&memory=256&maxTotalChargeUsd=0.15",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "raw",
        "rawContentType": "application/json",
        "body": "={{ JSON.stringify($('Validate settings').first().json.actorInput) }}",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "autodetect",
              "outputPropertyName": "body"
            }
          },
          "timeout": 15000
        }
      },
      "retryOnFail": false,
      "onError": "continueRegularOutput"
    },
    {
      "id": "launch-receipt",
      "name": "Keep launch run ID",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        200
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const readResponse = function readResponse(response, statuses = [200]) {\n  if (!response || !statuses.includes(response.statusCode)) throw Error('HTTP request failed or returned an unexpected status. Inspect the node; do not automatically repeat a launch.');\n  const contentType = Object.entries(response.headers ?? {}).find(([k]) => k.toLowerCase() === 'content-type')?.[1];\n  if (typeof contentType !== 'string' || !/^application\\/json(?:\\s*;|$)/i.test(contentType)) throw Error('Expected JSON; redirects and HTML responses are not accepted.');\n  const body = typeof response.body === 'string' ? JSON.parse(response.body) : response.body;\n  if (body == null || typeof body !== 'object') throw Error('Response has no JSON object or array.');\n  return body;\n};\nconst launchContext = function launchContext(response, config, read) {\n  try {\n    const run = read(response, [201]).data;\n    if (!run || run.actId !== config.actorId || !/^[A-Za-z0-9]{17}$/.test(run.id)) throw Error('Unrecognized launch response.');\n    return { runId: run.id, startedAtMs: config.startedAtMs };\n  } catch {\n    throw Error('No verified launch receipt. The Actor MAY have started. Check Apify Console before retrying; paste its run ID into existingRunId to resume. This template never retries POST automatically.');\n  }\n};\nreturn [{json:launchContext($input.first().json,$('Validate settings').first().json,readResponse)}];"
      }
    },
    {
      "id": "resume",
      "name": "Use existing run ID",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        440
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const c=$('Validate settings').first().json; return [{json:{runId:c.existingRunId,startedAtMs:c.startedAtMs}}];"
      }
    },
    {
      "id": "context",
      "name": "Run context - retain this ID",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1440,
        300
      ],
      "parameters": {}
    },
    {
      "id": "status",
      "name": "Read run status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        1680,
        300
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.apify.com/v2/actor-runs/' + $('Run context - retain this ID').first().json.runId }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "autodetect",
              "outputPropertyName": "body"
            }
          },
          "timeout": 15000
        }
      },
      "retryOnFail": false,
      "onError": "continueRegularOutput"
    },
    {
      "id": "inspect-status",
      "name": "Inspect run status",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1920,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const readResponse = function readResponse(response, statuses = [200]) {\n  if (!response || !statuses.includes(response.statusCode)) throw Error('HTTP request failed or returned an unexpected status. Inspect the node; do not automatically repeat a launch.');\n  const contentType = Object.entries(response.headers ?? {}).find(([k]) => k.toLowerCase() === 'content-type')?.[1];\n  if (typeof contentType !== 'string' || !/^application\\/json(?:\\s*;|$)/i.test(contentType)) throw Error('Expected JSON; redirects and HTML responses are not accepted.');\n  const body = typeof response.body === 'string' ? JSON.parse(response.body) : response.body;\n  if (body == null || typeof body !== 'object') throw Error('Response has no JSON object or array.');\n  return body;\n};\nconst inspectRun = function inspectRun(response, config, context, iteration, read, now = Date.now()) {\n  const fail = reason => ({ route: 'failure', runId: context.runId, reason });\n  let run;\n  try { run = read(response).data; } catch { return fail('Could not read run status. Resume this same run ID after resolving the API issue.'); }\n  if (!run || run.id !== context.runId || run.actId !== config.actorId) return fail('Run identity does not match this template.');\n  if (run.status === 'SUCCEEDED') {\n    if (![run.defaultDatasetId, run.defaultKeyValueStoreId].every(v => typeof v === 'string' && /^[A-Za-z0-9]{17}$/.test(v))) return fail('Completed run is missing its own dataset or key-value store.');\n    return { route: 'success', runId: run.id, datasetId: run.defaultDatasetId, storeId: run.defaultKeyValueStoreId, status: run.status };\n  }\n  if (!['READY','RUNNING','TIMING-OUT','ABORTING'].includes(run.status)) return fail('Actor ended with status ' + String(run.status) + '. Partial data is not sent onward.');\n  if (iteration >= 47 || now - context.startedAtMs >= 270000) return fail('Polling limit reached. No new run was started. Check Apify Console and resume this same run ID; its platform timeout remains in force.');\n  return { route: 'poll', runId: run.id, pollNumber: iteration + 1 };\n};\nreturn [{json:inspectRun($input.first().json,$('Validate settings').first().json,$('Run context - retain this ID').first().json,$runIndex,readResponse)}];"
      }
    },
    {
      "id": "success",
      "name": "Run succeeded?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2160,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "success-condition",
              "leftValue": "={{ $json.route === 'success' }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "pending",
      "name": "Poll again?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2160,
        560
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "pending-condition",
              "leftValue": "={{ $json.route === 'poll' }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "wait",
      "name": "Wait 5 seconds",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1880,
        560
      ],
      "parameters": {
        "resume": "timeInterval",
        "amount": 5,
        "unit": "seconds"
      }
    },
    {
      "id": "failure",
      "name": "Stop and inspect run",
      "type": "n8n-nodes-base.stopAndError",
      "typeVersion": 1,
      "position": [
        2440,
        560
      ],
      "parameters": {
        "errorType": "errorMessage",
        "errorMessage": "={{ $json.reason + ' Run ID: ' + $json.runId }}"
      }
    },
    {
      "id": "summary",
      "name": "Fetch run SUMMARY",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        2400,
        300
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.apify.com/v2/key-value-stores/' + $json.storeId + '/records/SUMMARY' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "autodetect",
              "outputPropertyName": "body"
            }
          },
          "timeout": 15000
        }
      },
      "retryOnFail": false,
      "onError": "continueRegularOutput"
    },
    {
      "id": "summary-check",
      "name": "Require clean SUMMARY",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2640,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const readResponse = function readResponse(response, statuses = [200]) {\n  if (!response || !statuses.includes(response.statusCode)) throw Error('HTTP request failed or returned an unexpected status. Inspect the node; do not automatically repeat a launch.');\n  const contentType = Object.entries(response.headers ?? {}).find(([k]) => k.toLowerCase() === 'content-type')?.[1];\n  if (typeof contentType !== 'string' || !/^application\\/json(?:\\s*;|$)/i.test(contentType)) throw Error('Expected JSON; redirects and HTML responses are not accepted.');\n  const body = typeof response.body === 'string' ? JSON.parse(response.body) : response.body;\n  if (body == null || typeof body !== 'object') throw Error('Response has no JSON object or array.');\n  return body;\n};\nconst inspectSummary = function inspectSummary(response, config, run, read) {\n  const s = read(response);\n  const object = v => v && typeof v === 'object' && !Array.isArray(v);\n  if (!object(s) || !object(s.failedSources) || Object.keys(s.failedSources).length) throw Error('SUMMARY is missing or reports a failed source. Inspect the run before delivering rows.');\n  for (const key of ['stoppedOnChargeLimit','stoppedOnTime','stoppedEarlyOnTimeLimit','baselineSkippedOnDeadline','historyCommitSkipped']) if (s[key]) throw Error('SUMMARY reports an operational stop or history failure: ' + key + '. Do not present partial data as a clean check.');\n  let count;\n  if (config.kind === 'permits') {\n    const labels = {chicago:'Chicago, IL',san_francisco:'San Francisco, CA',new_york:'New York, NY',austin:'Austin, TX',seattle:'Seattle, WA',los_angeles:'Los Angeles, CA',baton_rouge:'Baton Rouge, LA',cincinnati:'Cincinnati, OH',mesa:'Mesa, AZ',marin_county:'Marin County, CA'};\n    count = s.total;\n    if (s.stoppedEarlyOnTimeLimit !== false || s.stoppedOnChargeLimit !== false || s.datasetItemsSaved !== count || !object(s.perCity) || Object.values(s.perCity).some(v => !Number.isSafeInteger(v) || v < 0) || Object.values(s.perCity).reduce((a,b)=>a+b,0) !== count || config.actorInput.cities.some(city => !(labels[city] in s.perCity))) throw Error('Permit SUMMARY counts or selected-source coverage are inconsistent.');\n  } else {\n    count = s.rowsSaved;\n    if (s.status !== 'ok' || s.incomplete !== false || s.stoppedOnChargeLimit !== false || s.stoppedOnTime !== false) throw Error('Actor SUMMARY does not confirm a clean bounded observation.');\n  }\n  if (!Number.isSafeInteger(count) || count < 0 || count > config.rowLimit) throw Error('Saved count is invalid or exceeds this template’s bounded row cap.');\n  const boundedSample = config.kind === 'permits' ? Object.values(s.perCity).some(v => v >= config.actorInput.maxResultsPerCity) : s.stoppedOnMaxResults === true || (s.sourcesNotReached?.length ?? 0) > 0 || (s.sourceCoverage ?? []).some(v => v.hasMore || v.publicVariantLimitReached);\n  return { ...run, summary: s, expectedRows: count, boundedSample, coverageNote: 'Only the inspected source window is represented. Never infer removals, no exposure, or complete coverage from missing rows.' };\n};\nreturn [{json:inspectSummary($input.first().json,$('Validate settings').first().json,$('Run succeeded?').first().json,readResponse)}];"
      }
    },
    {
      "id": "dataset-meta",
      "name": "Read dataset count",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        2880,
        300
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.apify.com/v2/datasets/' + $json.datasetId }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "autodetect",
              "outputPropertyName": "body"
            }
          },
          "timeout": 15000
        }
      },
      "retryOnFail": false,
      "onError": "continueRegularOutput"
    },
    {
      "id": "count-check",
      "name": "Require exact saved count",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3120,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const readResponse = function readResponse(response, statuses = [200]) {\n  if (!response || !statuses.includes(response.statusCode)) throw Error('HTTP request failed or returned an unexpected status. Inspect the node; do not automatically repeat a launch.');\n  const contentType = Object.entries(response.headers ?? {}).find(([k]) => k.toLowerCase() === 'content-type')?.[1];\n  if (typeof contentType !== 'string' || !/^application\\/json(?:\\s*;|$)/i.test(contentType)) throw Error('Expected JSON; redirects and HTML responses are not accepted.');\n  const body = typeof response.body === 'string' ? JSON.parse(response.body) : response.body;\n  if (body == null || typeof body !== 'object') throw Error('Response has no JSON object or array.');\n  return body;\n};\nconst inspectDataset = function inspectDataset(response, accepted, read) {\n  const dataset = read(response).data;\n  if (!dataset || dataset.id !== accepted.datasetId || !Number.isSafeInteger(dataset.itemCount) || dataset.itemCount !== accepted.expectedRows) throw Error('Dataset count does not match SUMMARY. Stop and inspect; do not silently truncate or rerun the Actor.');\n  return accepted;\n};\nreturn [{json:inspectDataset($input.first().json,$('Require clean SUMMARY').first().json,readResponse)}];"
      }
    },
    {
      "id": "rows",
      "name": "Fetch bounded dataset",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.5,
      "position": [
        3360,
        300
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.apify.com/v2/datasets/' + $json.datasetId + '/items?format=json&clean=false&offset=0&limit=26' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "redirect": {
            "redirect": {
              "followRedirects": false
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "autodetect",
              "outputPropertyName": "body"
            }
          },
          "timeout": 15000
        }
      },
      "retryOnFail": false,
      "onError": "continueRegularOutput"
    },
    {
      "id": "row-check",
      "name": "Validate complete saved rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3600,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const readResponse = function readResponse(response, statuses = [200]) {\n  if (!response || !statuses.includes(response.statusCode)) throw Error('HTTP request failed or returned an unexpected status. Inspect the node; do not automatically repeat a launch.');\n  const contentType = Object.entries(response.headers ?? {}).find(([k]) => k.toLowerCase() === 'content-type')?.[1];\n  if (typeof contentType !== 'string' || !/^application\\/json(?:\\s*;|$)/i.test(contentType)) throw Error('Expected JSON; redirects and HTML responses are not accepted.');\n  const body = typeof response.body === 'string' ? JSON.parse(response.body) : response.body;\n  if (body == null || typeof body !== 'object') throw Error('Response has no JSON object or array.');\n  return body;\n};\nconst validateRows = function validateRows(response, accepted, config, read) {\n  const rows = read(response);\n  if (!Array.isArray(rows) || rows.length !== accepted.expectedRows || rows.length > config.rowLimit) throw Error('Dataset response is incomplete or does not match the saved count.');\n  const seen = new Set();\n  const scalar = v => (typeof v === 'string' && v.trim() && !/^(?:null|undefined|n\\/a)$/i.test(v.trim())) || (typeof v === 'number' && Number.isSafeInteger(v));\n  // n8n’s isolated Code runner does not expose the URL global. These are output-only links, never fetched.\n  const url = v => typeof v === 'string' && v.length <= 4000 && /^https?:\\/\\/(?![^/?#]*@)[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::[0-9]{1,5})?(?:[/?#][^\\s]*)?$/i.test(v);\n  for (const row of rows) {\n    if (!row || typeof row !== 'object' || Array.isArray(row)) throw Error('Dataset contains a non-record value.');\n    let key;\n    if (config.kind === 'shopify') {\n      if (!url(row.storeUrl) || !url(row.productUrl) || !scalar(row.variantId) || !scalar(row.productId) || typeof row.title !== 'string' || !row.title.trim() || typeof row.price !== 'string' || !/^\\d+(?:\\.\\d+)?$/.test(row.price) || typeof row.available !== 'boolean') throw Error('Shopify row has no usable variant identity, price or availability.');\n      key = JSON.stringify([row.storeUrl, String(row.variantId)]);\n    } else if (config.kind === 'permits') {\n      if (!scalar(row.sourceCity) || !scalar(row.permitId) || (row.sourceUrl != null && row.sourceUrl !== '' && !url(row.sourceUrl))) throw Error('Permit row has no usable source/permit identity or has an invalid source URL.');\n      key = JSON.stringify([row.sourceCity, String(row.permitId)]);\n    } else {\n      if (typeof row.cveId !== 'string' || !/^CVE-\\d{4}-\\d{4,}$/.test(row.cveId) || !url(row.url) || !row.title || !row.requiredAction) throw Error('CISA row has no usable CVE identity or required source fields.');\n      key = row.cveId;\n    }\n    if (seen.has(key)) throw Error('Duplicate record identity: inspect before destination updates.');\n    seen.add(key);\n  }\n  return { ...accepted, rows, outcome: rows.length ? 'rows_ready' : 'valid_empty' };\n};\nreturn [{json:validateRows($input.first().json,$('Require exact saved count').first().json,$('Validate settings').first().json,readResponse)}];"
      }
    },
    {
      "id": "has-rows",
      "name": "Has business rows?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3840,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "has-rows-condition",
              "leftValue": "={{ $json.outcome === 'rows_ready' }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "flat",
      "name": "Sheets-ready rows",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4080,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const flattenRows = function flattenRows(accepted, kind) {\n  const safe = v => {\n    if (v == null) return '';\n    if (typeof v === 'object') v = JSON.stringify(v);\n    return typeof v === 'string' && /^\\s*[=+@-]/.test(v) ? \"'\" + v : v;\n  };\n  const fields = kind === 'shopify' ? ['storeUrl','variantId','productId','title','variantTitle','productUrl','price','compareAtPrice','currency','available','comparisonStatus','changeTypes','changedFields','previous','observedAt']\n    : kind === 'permits' ? ['sourceCity','permitId','permitType','permitClass','status','workDescription','address','city','state','zip','valuation','contractorName','issueDate','appliedDate','sourceUrl','scrapedAt']\n    : ['cveId','title','vendor','product','dateAdded','publishedDueDate','requiredAction','knownRansomwareCampaignUse','matchedWatchlist','comparisonStatus','changedFields','previous','url','observedAt'];\n  return accepted.rows.map(row => {\n    const recordKey = kind === 'shopify' ? JSON.stringify([row.storeUrl,String(row.variantId)]) : kind === 'permits' ? JSON.stringify([row.sourceCity,String(row.permitId)]) : row.cveId;\n    return {json:{record_key:recordKey,replay_key:JSON.stringify([accepted.runId,recordKey]),apify_run_id:accepted.runId,bounded_sample:accepted.boundedSample,...Object.fromEntries(fields.map(k=>[k,safe(row[k])]))}};\n  });\n};\nreturn flattenRows($input.first().json,\"cisa\");"
      }
    },
    {
      "id": "empty",
      "name": "Valid empty - keep prior records",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4080,
        560
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const a=$input.first().json;return [{json:{outcome:'valid_empty',apify_run_id:a.runId,rows:0,summary:a.summary,note:'No business rows to deliver. Preserve prior destination records; empty does not prove removal, safety or exhaustive coverage.'}}];"
      }
    },
    {
      "id": "setup-note",
      "name": "Setup before running",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        220,
        -120
      ],
      "parameters": {
        "width": 700,
        "height": 280,
        "content": "## Manual starter · free template, paid Actor usage\n1. Edit the small input. Keep the result cap at 1–25 total rows.\n2. Create Header Auth: name `Authorization`, value `Bearer <your Apify token>`. Select it on all 5 HTTP Request nodes. No token or credential is included.\n3. Review current Actor pricing. Each NEW run requests a $0.15 charge ceiling, 256 MiB and 180-second timeout. n8n hosting may also cost money.\n4. Run once manually. Do not execute Launch in isolation or retry a failed whole execution blindly.\nhttps://apify.com/maydit/cisa-kev-watchlist-monitor"
      }
    },
    {
      "id": "replay-note",
      "name": "Replay and history",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1550,
        -120
      ],
      "parameters": {
        "width": 760,
        "height": 240,
        "content": "## Retain the run ID before delivery\nPaste an existing run ID into settings to skip launch and reread that same run. After an uncertain POST, inspect Console before any retry.\nShopify/CISA: choose your own snapshotName and use it sequentially; no history reset is performed. History may advance before destination delivery.\nAt most 48 status reads, 5 seconds between reads, 270-second polling deadline; workflow timeout 360 seconds. Network/API errors stop, never relaunch."
      }
    },
    {
      "id": "scope-note",
      "name": "Understand these rows",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        3370,
        -120
      ],
      "parameters": {
        "width": 740,
        "height": 280,
        "content": "## Scope and destination\nMonitor two exact CVEs in CISA’s catalog. This does not scan assets or establish your exposure. publishedDueDate is CISA’s published context, not your organization’s deadline. Never close tickets because a row is missing.\nOnly clean SUMMARY + exact dataset count + usable unique identities reach Sheets-ready rows. Result caps are labeled bounded_sample; operational partials stop.\nConnect your own Sheets Append or Update Row using record_key, writing values literally (RAW). Keep replay_key for delivery deduplication. No Google Sheets connection or schedule is included. Review retention of execution data."
      }
    }
  ],
  "connections": {
    "Start manually": {
      "main": [
        [
          {
            "node": "Edit small input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit small input": {
      "main": [
        [
          {
            "node": "Validate settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate settings": {
      "main": [
        [
          {
            "node": "Start a new run?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start a new run?": {
      "main": [
        [
          {
            "node": "Launch once - costs apply",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Use existing run ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Launch once - costs apply": {
      "main": [
        [
          {
            "node": "Keep launch run ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep launch run ID": {
      "main": [
        [
          {
            "node": "Run context - retain this ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Use existing run ID": {
      "main": [
        [
          {
            "node": "Run context - retain this ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run context - retain this ID": {
      "main": [
        [
          {
            "node": "Read run status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read run status": {
      "main": [
        [
          {
            "node": "Inspect run status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inspect run status": {
      "main": [
        [
          {
            "node": "Run succeeded?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run succeeded?": {
      "main": [
        [
          {
            "node": "Fetch run SUMMARY",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Poll again?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll again?": {
      "main": [
        [
          {
            "node": "Wait 5 seconds",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Stop and inspect run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 5 seconds": {
      "main": [
        [
          {
            "node": "Read run status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch run SUMMARY": {
      "main": [
        [
          {
            "node": "Require clean SUMMARY",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Require clean SUMMARY": {
      "main": [
        [
          {
            "node": "Read dataset count",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read dataset count": {
      "main": [
        [
          {
            "node": "Require exact saved count",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Require exact saved count": {
      "main": [
        [
          {
            "node": "Fetch bounded dataset",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch bounded dataset": {
      "main": [
        [
          {
            "node": "Validate complete saved rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate complete saved rows": {
      "main": [
        [
          {
            "node": "Has business rows?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has business rows?": {
      "main": [
        [
          {
            "node": "Sheets-ready rows",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Valid empty - keep prior records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 360,
    "saveManualExecutions": true
  },
  "pinData": {},
  "tags": []
}
