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 Google Keywords node.

What it means

Thrown by the 'Format G Data' Code node when $('Loop Over Google Keywords').json (i.e. $node["Loop Over Google Keywords"].json) is absent, or its monthly_searches property is missing or not an array. The node cross-joins the loop node's monthly_searches records with rows from 'Add Second Tier G Keyword Data' and batches the result for a bulk NocoDB import, so it hard-requires an array of {year, month, search_volume} records.

Source

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

          "id": "LAbGsn1RMARiq5Gy",
          "name": "NocoDB Token account"
        }
      },
      "executeOnce": false,
      "retryOnFail": true,
      "typeVersion": 3,
      "notes": "This nocoDb node performs automated tasks as part of the workflow."
    },
    {
      "id": "1fdaf0fc-5c11-406f-93fb-b4a7fd3b6eed",
      "name": "Format G Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -240,
        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."

View on GitHub (pinned to 94007c1445)

Solutions

  1. Pin the output of 'Loop Over Google Keywords' and confirm the active item actually has monthly_searches as an array; log Object.keys(loopData || {}).
  2. If zero-volume keywords legitimately lack the field, default it instead of throwing: const monthlySearches = Array.isArray(loopData?.monthly_searches) ? loopData.monthly_searches : []; and skip empty items.
  3. If the loop can emit nothing, set alwaysOutputData: true on it or gate this node behind an IF that checks the loop produced data.
  4. Align the field name with the live API response (rename in the loop node's Set step if the API changed).

Example fix

// before
const loopData = $node["Loop Over Google 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 Google Keywords node.");
}

// after - tolerate zero-volume keywords, fail only on structural breakage
const loopData = $('Loop Over Google Keywords').first().json ?? {};
const monthlySearches = Array.isArray(loopData.monthly_searches) ? loopData.monthly_searches : [];
if (monthlySearches.length === 0 && Object.keys(loopData).length > 0) {
  // keyword exists but has no volume data: nothing to import for it
  return [];
}
if (Object.keys(loopData).length === 0) {
  throw new Error('Loop Over Google Keywords produced no item at all.');
}
Defensive patterns

Strategy: validation

Validate before calling

const loopData = $('Loop Over Google Keywords').first().json ?? {};
const monthlySearches = Array.isArray(loopData.monthly_searches) ? loopData.monthly_searches : [];
if (monthlySearches.length === 0) {
  // zero-volume keyword or empty loop output: nothing to import, don't fail the batch
  return [];
}

Type guard

const hasMonthlySearches = (d) =>
  d != null && Array.isArray(d.monthly_searches) &&
  d.monthly_searches.every(r => 'year' in r && 'month' in r && 'search_volume' in r);

Try / catch

try {
  return buildBatches(monthlySearches, secondTierItems);
} catch (e) {
  throw new Error(`Format G Data failed: ${e.message}; loopData keys: ${Object.keys(loopData).join(',')}`);
}

Prevention

When it happens

Trigger: The keyword API returned a keyword without monthly_searches (no volume data), the loop node emitted zero items (alwaysOutputData: false means empty output yields nothing, so $node[...] is undefined), the field is named differently (monthly_search_volume, searches), or monthly_searches came back as an object keyed by month instead of an array.

Common situations: DataforSEO/SEMrush-style keyword endpoints that omit monthly_searches for zero-volume keywords; loop-over-items emitting no item on empty batches; API schema version bump renaming fields; test runs where the loop node was skipped.

Related errors


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