Zie619/n8n-workflows · error · Error

monthly_searches data is missing or not an array from Loop O

Error message

monthly_searches data is missing or not an array from Loop Over YT Keywords node.

What it means

Same guard as the Google variant, but in the 'Format YT Data' Code node: it reads $node["Loop Over YT Keywords"].json and requires monthly_searches to be a non-empty-capable array before cross-joining with 'Add Second Tier YT Keyword Data'. Thrown when that loop node's current item lacks a valid monthly_searches array. Note the code's comments still say 'Google' — it is a copy of the G node with names swapped.

Source

Thrown at workflows/Splitout/1642_Splitout_Code_Automation_Webhook.json:638

        400
      ],
      "parameters": {
        "jsCode": "// Get the monthly search data from the \"Loop Over Google Keywords\" node\nconst loopData = $node[\"Loop Over Google Keywords\"].json;\nif (!loopData || !loopData.monthly_searches || !Array.isArray(loopData.monthly_searches)) {\n  throw new Error(\"monthly_searches data is missing or not an array from Loop Over Google Keywords node.\");\n}\nconst monthlySearches = loopData.monthly_searches;\n\n// Get all items from the \"Add Second Tier G Keyword Data\" node\nconst secondTierItems = $items(\"Add Second Tier G Keyword Data\");\n\nif (!secondTierItems || secondTierItems.length === 0) {\n  throw new Error(\"No data found in Add Second Tier G Keyword Data node.\");\n}\n\nconst results = [];\n\n// Loop through each second-tier item\nsecondTierItems.forEach(itemWrapper => {\n  const item = itemWrapper.json;\n  // Validate that the required properties exist on the second-tier item.\n  if (!item.keyword || item.Id === undefined) {\n    throw new Error(\"A second tier item is missing 'keyword' or 'Id'.\");\n  }\n  \n  // For each monthly search record, combine with the second-tier data\n  monthlySearches.forEach(record => {\n    // Validate that each monthly record has the required properties.\n    if (record.year === undefined || record.month === undefined || record.search_volume === undefined) {\n      throw new Error(\"A monthly search record is missing 'year', 'month', or 'search_volume'.\");\n    }\n    \n    results.push({\n      json: {\n        keyword: item.keyword,\n        google_keyword_id: item.Id,\n        year: record.year,\n        month: record.month,\n        search_volume: record.search_volume,\n        unique_id: `${record.year}-${record.month}-${item.keyword}`\n      }\n    });\n  });\n});\n\n// Chunk the results into batches of 1000 items each\nconst batchSize = 1000;\nconst batchedResults = [];\n\nfor (let i = 0; i < results.length; i += batchSize) {\n  // Create a batch containing up to batchSize items\n  const batchItems = results.slice(i, i + batchSize).map(item => item.json);\n  batchedResults.push({\n    json: {\n      batch: batchItems\n    }\n  });\n}\n\nreturn batchedResults;\n"
      },
      "typeVersion": 2,
      "alwaysOutputData": false,
      "notes": "This code node performs automated tasks as part of the workflow."
    },
    {
      "id": "7d654cf7-1223-4f10-8026-997f5418402e",
      "name": "Format YT Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -220,
        980
      ],
      "parameters": {
        "jsCode": "// Get the monthly search data from the \"Loop Over Google Keywords\" node\nconst loopData = $node[\"Loop Over YT Keywords\"].json;\nif (!loopData || !loopData.monthly_searches || !Array.isArray(loopData.monthly_searches)) {\n  throw new Error(\"monthly_searches data is missing or not an array from Loop Over YT Keywords node.\");\n}\nconst monthlySearches = loopData.monthly_searches;\n\n// Get all items from the \"Add Second Tier G Keyword Data\" node\nconst secondTierItems = $items(\"Add Second Tier YT Keyword Data\");\n\nif (!secondTierItems || secondTierItems.length === 0) {\n  throw new Error(\"No data found in Add Second Tier YT Keyword Data node.\");\n}\n\nconst results = [];\n\n// Loop through each second-tier item\nsecondTierItems.forEach(itemWrapper => {\n  const item = itemWrapper.json;\n  // Validate that the required properties exist on the second-tier item.\n  if (!item.keyword || item.Id === undefined) {\n    throw new Error(\"A second tier item is missing 'keyword' or 'Id'.\");\n  }\n  \n  // For each monthly search record, combine with the second-tier data\n  monthlySearches.forEach(record => {\n    // Validate that each monthly record has the required properties.\n    if (record.year === undefined || record.month === undefined || record.search_volume === undefined) {\n      throw new Error(\"A monthly search record is missing 'year', 'month', or 'search_volume'.\");\n    }\n    \n    results.push({\n      json: {\n        keyword: item.keyword,\n        google_keyword_id: item.Id,\n        year: record.year,\n        month: record.month,\n        search_volume: record.search_volume,\n        unique_id: `${record.year}-${record.month}-${item.keyword}`\n      }\n    });\n  });\n});\n\n// Chunk the results into batches of 1000 items each\nconst batchSize = 1000;\nconst batchedResults = [];\n\nfor (let i = 0; i < results.length; i += batchSize) {\n  // Create a batch containing up to batchSize items\n  const batchItems = results.slice(i, i + batchSize).map(item => item.json);\n  batchedResults.push({\n    json: {\n      batch: batchItems\n    }\n  });\n}\n\nreturn batchedResults;\n"
      },
      "typeVersion": 2,
      "notes": "This code node performs automated tasks as part of the workflow."
    },
    {
      "id": "67848762-a140-4c63-b8ca-e20331135741",
      "name": "Bulk Import G Monthly Search Volume",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        0,
        400
      ],
      "parameters": {
        "url": "{{ $env.API_BASE_URL }}",
        "method": "POST",
        "options": {
          "batching": {
            "batch": {

View on GitHub (pinned to 94007c1445)

Solutions

  1. Verify the 'Loop Over YT Keywords' node name matches the $node reference exactly and that its pinned output has monthly_searches as an array.
  2. Default missing monthly_searches to [] and return [] for zero-volume keywords instead of failing the whole run.
  3. Set alwaysOutputData: true on the loop node or add an IF guard so Format YT Data only runs when the loop emitted data.
  4. Clean up the stale Google comments/naming (google_keyword_id is written for YT rows too) to prevent future mix-ups.

Example fix

// before
const loopData = $node["Loop Over YT Keywords"].json;
if (!loopData || !loopData.monthly_searches || !Array.isArray(loopData.monthly_searches)) {
  throw new Error("monthly_searches data is missing or not an array from Loop Over YT Keywords node.");
}

// after
const loopData = $('Loop Over YT Keywords').first().json ?? {};
const monthlySearches = Array.isArray(loopData.monthly_searches) ? loopData.monthly_searches : [];
if (monthlySearches.length === 0) {
  return []; // no volume data for this keyword: nothing to bulk import
}
Defensive patterns

Strategy: validation

Validate before calling

const loopData = $('Loop Over YT Keywords').first().json ?? {};
if (!Array.isArray(loopData.monthly_searches)) {
  console.warn(`monthly_searches missing/not array. Keys: ${Object.keys(loopData).join(', ')}`);
  return [];
}

Type guard

const isMonthlyArray = (d) => Array.isArray(d?.monthly_searches);

Prevention

When it happens

Trigger: YouTube-keyword rows returned by the data provider without monthly_searches, the 'Loop Over YT Keywords' node emitting no item (its output empty), or the field arriving as an object/string. Also triggered if the node name in the workflow was renamed so $node["Loop Over YT Keywords"] resolves to stale/undefined data.

Common situations: Copy-pasted node where only some names were updated (comments still reference Google), zero-volume YouTube keywords, renamed loop nodes breaking $node references, or API schema drift on the search-volume endpoint.

Related errors


AI-assisted analysis of Zie619/n8n-workflows@94007c1445 (2026-08-15). Data as JSON: /api/errors/80206d84d54a73c4. Report an issue: GitHub.