AutomationFlowsAI & RAG › Analyze Google Ads Search Terms and Get Negative Keyword Suggestions with…

Analyze Google Ads Search Terms and Get Negative Keyword Suggestions with…

Original n8n title: Analyze Google Ads Search Terms and Get Negative Keyword Suggestions with Openai Gpt-5.4-mini

ByZeljislav Petrovic @zepix on n8n.io

This workflow is built for PPC managers, digital marketing agencies, and in-house marketing teams who run Google Ads search campaigns and want to stop wasting budget on irrelevant queries. Instead of manually reviewing hundreds or thousands of search terms in spreadsheets, this…

Event trigger★★★★☆ complexityAI-powered19 nodesOpenAI ChatGoogle SheetsHTTP RequestChain Llm
AI & RAG Trigger: Event Nodes: 19 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #15385 — we link there as the canonical source.

This workflow follows the Chainllm → Google Sheets recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "fTjiqx2C2bq1IkyU",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Google Ads Search Terms Analyzer",
  "tags": [],
  "nodes": [
    {
      "id": "46986d67-9501-4952-b150-666f0bfa848b",
      "name": "GPT 5.4-mini",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1088,
        208
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5.4-mini",
          "cachedResultName": "gpt-5.4-mini"
        },
        "options": {
          "timeout": 180000
        },
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "fac0c7f3-1bc5-4c32-85ec-9033317def45",
      "name": "Set Google Ads IDs & business context",
      "type": "n8n-nodes-base.set",
      "position": [
        416,
        0
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "43be7152-48ea-45fd-b9a9-afd409a96dc4",
              "name": "google_ads_mcc",
              "type": "string",
              "value": "YOUR_GOOGLE_ADS_MANAGER__ID"
            },
            {
              "id": "ab431508-dc08-42ff-82bf-79f46276f16a",
              "name": "google_ads_customer",
              "type": "string",
              "value": "YOUR_GOOGLE_ADS_CUSTOMER_ID"
            },
            {
              "id": "2e7f7861-686a-421b-bef4-ed7c3b55b3bb",
              "name": "google_ads_developer_token",
              "type": "string",
              "value": "YOUR_GOOGLE_ADS_DEVELOPER_TOKEN"
            },
            {
              "id": "f0a78572-7200-4abd-ac5c-54f339fc0edb",
              "name": "company_name",
              "type": "string",
              "value": "ENTER YOUR BUSINESS DETAILS HERE"
            },
            {
              "id": "42045519-c1d0-4dec-aa61-84fbd362c37c",
              "name": "company_website",
              "type": "string",
              "value": "ENTER YOUR BUSINESS DETAILS HERE"
            },
            {
              "id": "cb96e289-ff42-4475-aba8-6a4a69e26199",
              "name": "industry",
              "type": "string",
              "value": "ENTER YOUR BUSINESS DETAILS HERE"
            },
            {
              "id": "9a3f384d-25f9-43c3-b84c-c94391d8acfd",
              "name": "business_description",
              "type": "string",
              "value": "ENTER YOUR BUSINESS DETAILS HERE"
            },
            {
              "id": "3a43a9a9-6cae-4069-a89c-d464b19a3a69",
              "name": "target_audience",
              "type": "string",
              "value": "ENTER YOUR BUSINESS DETAILS HERE"
            },
            {
              "id": "9c7427ff-b825-4bd4-9a96-46d80d9a9f85",
              "name": "campaign_goals",
              "type": "string",
              "value": "ENTER YOUR BUSINESS DETAILS HERE"
            },
            {
              "id": "d9429b19-63cf-4eff-8308-16c9571ca9bc",
              "name": "key_offerings",
              "type": "string",
              "value": "ENTER YOUR BUSINESS DETAILS HERE"
            },
            {
              "id": "17759ad0-ceea-46bf-9e0c-11d2205dafb7",
              "name": "known_irrelevant_topics",
              "type": "string",
              "value": "ENTER YOUR BUSINESS DETAILS HERE"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "87465cf8-9485-4ef3-8870-fcecae5137e0",
      "name": "Save global exclusion suggestions",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2304,
        0
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "group",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "group",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "negative_keyword",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "negative_keyword",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "match_type",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "match_type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rationale",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "rationale",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Create spreadsheet for storing analyzed search terms').first().json.sheets[1].properties.sheetId }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Create spreadsheet for storing analyzed search terms').first().json.spreadsheetId }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "a43f5dda-780d-4114-8ef9-1865bcc02a43",
      "name": "Extract & format search terms analysis table",
      "type": "n8n-nodes-base.code",
      "position": [
        1696,
        0
      ],
      "parameters": {
        "jsCode": "const text = $('Analyze search terms & suggest keyword exclusions/inclusions').first().json.text;\n\nconst tableMatch = text.match(/\\| search_term \\|.*?\\n([\\s\\S]*?)(?=## PART 3|## RECOMMENDED GLOBAL EXCLUSIONS|$)/);\nif (!tableMatch) throw new Error(\"Table not found in AI output\");\n\nconst rows = tableMatch[1]\n  .split('\\n')\n  .filter(line => line.startsWith('|') && !line.match(/^\\|[-| ]+\\|$/))\n  .map(line => {\n    const cols = line.split('|').slice(1, -1).map(c => c.trim());\n    return {\n      search_term:            cols[0],\n      status:                 cols[1],\n      campaign:               cols[2],\n      ad_group:               cols[3],\n      matched_keyword:        cols[4],\n      match_type:             cols[5],\n      impressions:            cols[6],\n      clicks:                 cols[7],\n      ctr:                    cols[8],\n      cost:                   cols[9],\n      avg_cpc:                cols[10],\n      avg_cpm:                cols[11],\n      conversion_rate:        cols[12],\n      cost_per_conversion:    cols[13],\n      intent_signal:          cols[14],\n      relevancy:              cols[15],\n      relevancy_reasoning:    cols[16],\n      exclusion_suggestion:   cols[17],\n      exclusion_level:        cols[18],\n      priority_for_exclusion: cols[19],\n      inclusion_suggestion:   cols[20],\n      inclusion_type:         cols[21],\n      inclusion_level:        cols[22],\n    };\n  })\n  .filter(row => row.search_term && row.search_term !== '---');\n\nreturn rows.map(json => ({ json }));"
      },
      "typeVersion": 2
    },
    {
      "id": "ef110f5d-8e16-466c-85c9-5222f3ee09d1",
      "name": "Fetch search terms data from Google Ads",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        624,
        0
      ],
      "parameters": {
        "url": "=https://googleads.googleapis.com/v23/customers/{{ $json.google_ads_customer }}/googleAds:searchStream",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"query\": \"SELECT campaign.name, campaign.advertising_channel_type, ad_group.name, search_term_view.search_term, search_term_view.status, segments.keyword.info.text, segments.keyword.info.match_type, metrics.impressions, metrics.clicks, metrics.ctr, metrics.cost_micros, metrics.average_cpc, metrics.average_cpm, metrics.conversions, metrics.conversions_from_interactions_rate, metrics.cost_per_conversion FROM search_term_view WHERE segments.date DURING LAST_30_DAYS AND metrics.impressions >= 1 ORDER BY metrics.impressions DESC\"\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "developer-token",
              "value": "={{ $json.google_ads_developer_token }}"
            },
            {
              "name": "login-customer-id",
              "value": "={{ $json.google_ads_mcc }}"
            }
          ]
        },
        "nodeCredentialType": "googleAdsOAuth2Api"
      },
      "credentials": {
        "googleAdsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "d304acae-5193-4309-922e-e19a12c8fe43",
      "name": "Clean & format fetched data for AI",
      "type": "n8n-nodes-base.code",
      "position": [
        832,
        0
      ],
      "parameters": {
        "jsCode": "// Get results array from first input item\nconst rawData = $input.first().json;\n\n// Support both formats: direct array or object with results property\nconst resultsArray = Array.isArray(rawData)\n  ? rawData[0].results\n  : rawData.results;\n\n// Formatting helper functions\nfunction formatPercent(value) {\n  const parsed = parseFloat(value);\n  // Return 0% if value is null, undefined, or NaN\n  if (isNaN(parsed)) return '0%';\n  const pct = parsed * 100;\n  return (pct % 1 === 0 ? pct.toFixed(0) : pct.toFixed(2)) + '%';\n}\n\nfunction formatDollar(micros) {\n  const dollars = parseFloat(micros) / 1_000_000;\n  return '$' + (dollars % 1 === 0 ? dollars.toFixed(0) : dollars.toFixed(2));\n}\n\nfunction formatDollarDirect(value) {\n  const parsed = parseFloat(value);\n  // Return $0 if value is null, undefined, or NaN (e.g. avg_cpc when clicks = 0)\n  if (isNaN(parsed)) return '$0';\n  const dollars = parsed / 1_000_000;\n  return '$' + (dollars % 1 === 0 ? dollars.toFixed(0) : dollars.toFixed(2));\n}\n\nfunction formatConversions(value) {\n  return Math.round(parseFloat(value)).toString();\n}\n\n// Map and clean each search term entry\nconst cleanedResults = resultsArray.map(item => {\n  const m = item.metrics;\n  const stv = item.searchTermView;\n  const seg = item.segments?.keyword?.info || {};\n\n  // cost_per_conversion can be undefined/null/0\n  let costPerConv = 'N/A';\n  if (m.costPerConversion !== undefined && m.costPerConversion !== null) {\n    const val = parseFloat(m.costPerConversion);\n    costPerConv = val > 0 ? formatDollarDirect(m.costPerConversion) : '$0';\n  }\n\n  // Get campaign type from campaign.advertisingChannelType\n  const campaignType = item.campaign?.advertisingChannelType ?? 'UNKNOWN';\n\n  return {\n    search_term: stv.searchTerm,\n    status: stv.status,\n    campaign: item.campaign.name,\n    campaign_type: campaignType,\n    ad_group: item.adGroup.name,\n    matched_keyword: seg.text || 'N/A',\n    match_type: seg.matchType || 'N/A',\n    impressions: parseInt(m.impressions),\n    clicks: parseInt(m.clicks),\n    ctr: formatPercent(m.ctr),\n    cost: formatDollar(m.costMicros),\n    avg_cpc: formatDollarDirect(m.averageCpc),\n    avg_cpm: formatDollarDirect(m.averageCpm),\n    conversions: formatConversions(m.conversions),\n    conversion_rate: formatPercent(m.conversionsFromInteractionsRate),\n    cost_per_conversion: costPerConv\n  };\n})\n// Filter: keep only rows with impressions >= 2 OR clicks >= 1\n.filter(row => row.impressions >= 5 || row.clicks >= 1);\n\n// Sort by impressions descending (highest first)\ncleanedResults.sort((a, b) => b.impressions - a.impressions);\n\n// Return as a single item with the cleaned search terms array\nreturn [{\n  json: {\n    total_search_terms: cleanedResults.length,\n    search_terms: cleanedResults\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "6387033b-a69c-468d-8e32-4662afd36448",
      "name": "Execute workflow manually",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        144,
        192
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "cf6d6fbe-f0af-456b-b88c-0610cf4e3460",
      "name": "Execute workflow every 30 days",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        144,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "daysInterval": 30,
              "triggerAtHour": 2
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "88b031ab-36e4-4278-af96-20fca63061f9",
      "name": "Analyze search terms & suggest keyword exclusions/inclusions",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        1088,
        0
      ],
      "parameters": {
        "text": "=Here is the search terms dataset and business context for analysis.\n\nSearch terms ({{ $json.total_search_terms }} terms total):\n{{ JSON.stringify($json.search_terms, null, 2) }}\n\nBusiness context:\n- company_name: {{ $('Set Google Ads IDs & business context').first().json.company_name }}\n- company_website: {{ $('Set Google Ads IDs & business context').first().json.company_website }}\n- industry: {{ $('Set Google Ads IDs & business context').first().json.industry }}\n- business_description: {{ $('Set Google Ads IDs & business context').first().json.business_description }}\n- target_audience: {{ $('Set Google Ads IDs & business context').first().json.target_audience }}\n- campaign_goals: {{ $('Set Google Ads IDs & business context').first().json.campaign_goals }}\n- key_offerings: {{ $('Set Google Ads IDs & business context').first().json.key_offerings }}\n- known_irrelevant_topics: {{ $('Set Google Ads IDs & business context').first().json.known_irrelevant_topics }}",
        "batching": {},
        "messages": {
          "messageValues": [
            {
              "message": "=You are an expert Google Ads analyst and PPC strategist with deep knowledge of\nsearch intent, keyword relevancy, negative keyword strategy, and campaign\noptimization. Your task is to perform a comprehensive, intelligent analysis of a\nGoogle Ads Search Terms Report and produce a structured, actionable output that\nhelps the advertiser eliminate wasted spend and improve targeting precision.\n\nYou think like a senior PPC specialist. You do not draw conclusions from isolated\nmetrics alone. You combine performance data, search intent signals, business\ncontext, campaign and ad group structure, matched keyword, match type, and expert\nintuition to form well-reasoned judgments.\n\n-- SECTION 1: INPUT DATA --\n\nYou will receive two inputs in the user message:\n\n1. BUSINESS CONTEXT \u2014 a structured description of the advertiser's business,\n   provided as key-value fields:\n\n   - company_name          : Name of the company\n   - company_website       : Website URL\n   - industry              : Industry or business category\n   - business_description  : What the company does, its core offerings, services\n                             or products, value proposition, and important nuances\n                             about what they DO and DO NOT offer or serve\n   - target_audience       : Ideal customer profile \u2014 who they are, their role,\n                             demographics, needs, pain points, and what they search\n                             for when they are a good fit for this business\n   - campaign_goals        : Primary goals of the campaigns (e.g. lead generation,\n                             purchases) and the type of search intent being targeted\n                             (e.g. transactional, commercial, local service queries)\n   - key_offerings         : Main products, services, or categories being promoted\n                             that search terms should ideally relate to\n   - known_irrelevant_topics : (Optional) Topics, industries, use cases, or audiences\n                             that are clearly out of scope and should always be\n                             treated as irrelevant\n\n2. SEARCH TERMS DATASET \u2014 a tabular dataset where each row represents one search\n   term that triggered an ad, with the following columns:\n\n   - search_term         : The actual query the user typed into Google\n   - status              : \"Added\" (already a keyword), \"Excluded\" (already a\n                           negative keyword), or \"None\" (no action taken yet)\n   - campaign            : The campaign this term triggered\n   - ad_group            : The ad group this term triggered\n   - matched_keyword     : The keyword in the account that matched this term\n   - match_type          : Broad / Phrase / Exact\n   - impressions         : Number of times the ad was shown\n   - clicks              : Number of clicks received\n   - ctr                 : Click-through rate (%)\n   - cost                : Total spend on this term\n   - avg_cpc             : Average cost per click\n   - avg_cpm             : Average cost per thousand impressions\n   - conversion_rate     : Conversion rate (%)\n   - cost_per_conversion : Cost per conversion\n\nINFERENCE NOTE: If any business context fields are missing or incomplete, infer\nwhat you can from the campaign names, ad group names, and matched keywords in the\ndataset. State your inferences explicitly at the start of your output.\n\n-- SECTION 2: ANALYSIS INSTRUCTIONS --\n\nFor each search term, perform a holistic assessment before assigning any values.\nConsider all of the following signals together:\n\na) BUSINESS FIT: Does this term reflect a need the business can actually serve,\n   based on the business context provided?\n\nb) INTENT SIGNAL: What is the user's likely intent behind this query?\n   - Transactional     (ready to buy / hire / sign up)\n   - Commercial        (researching options, comparing, evaluating)\n   - Informational     (learning, general curiosity \u2014 rarely converts)\n   - Navigational      (looking for a specific brand or site)\n\nc) CAMPAIGN & AD GROUP FIT: Does this term belong in the campaign and ad group it\n   triggered, or is it a thematic mismatch even if slightly relevant?\n\nd) KEYWORD MATCH QUALITY: How well does the matched_keyword describe this search\n   term? A poor match (e.g. broad match pulling in unrelated queries) is itself a\n   signal of a targeting problem.\n\ne) PERFORMANCE SIGNALS: Use metrics as supporting evidence, not as the sole basis\n   for decisions. High cost with zero conversions is a warning. But do not\n   automatically label a low-CTR term as irrelevant \u2014 context matters.\n\nf) STATUS AWARENESS: Factor in the status column:\n   - \"Added\"    \u2192 already a keyword; flag if it should not be\n   - \"Excluded\" \u2192 already a negative; confirm whether the exclusion is correct\n   - \"None\"     \u2192 no action taken; this is your primary analysis target\n\ng) EXPERT INTUITION: Apply real-world PPC knowledge. Some terms look harmless but\n   consistently attract the wrong audience. Some look unusual but are high-value\n   niche queries. Think beyond the raw data.\n\nDO NOT:\n- Base relevancy solely on impressions, clicks, or cost\n- Mark a term \"Directly Relevant\" just because it has conversions if it is\n  semantically misaligned with the business\n- Mark a term \"Directly Irrelevant\" just because it has no clicks if it is\n  clearly on-topic\n- Suggest excluding a \"Directly Relevant\" term just because performance is low\n- Suggest broad-match exclusions for specific, high-value terms\n\n-- SECTION 3: RELEVANCY SCALE --\n\nAssign exactly one of the following five labels to each search term:\n\nDirectly Relevant\n  The term precisely matches the business's offering, target audience, and\n  campaign intent. High confidence this user is an ideal customer.\n\nIndirectly Relevant\n  The term is thematically related and may attract a partially qualified audience.\n  The user could be a potential customer but the signal is weak or imprecise.\n  Monitor closely.\n\nBorderline\n  The term is ambiguous. Part of the audience searching this could be relevant,\n  part could not. Intent is unclear or mixed. Requires manual judgment or further\n  data before deciding.\n\nIndirectly Irrelevant\n  The term is topically adjacent but the user's intent, context, or profile makes\n  them a poor fit. Examples: wrong funnel stage, wrong geography, wrong use case,\n  DIY intent vs. professional service.\n\nDirectly Irrelevant\n  The term has no meaningful connection to the business, its offerings, or its\n  target audience. The ad should never have shown for this query.\n\n-- SECTION 4: EXCLUSION SUGGESTION RULES --\n\nWHEN TO SUGGEST AN EXCLUSION:\nProvide an exclusion suggestion for every term where there is an existing or\npotential relevancy problem \u2014 whether full or partial. This includes:\n- All \"Directly Irrelevant\" terms\n- All \"Indirectly Irrelevant\" terms\n- All \"Borderline\" terms where a specific problematic modifier can be isolated\n- \"Indirectly Relevant\" terms where a problematic word within the term is the\n  source of the problem (partial exclusion)\n\nDo NOT suggest exclusions for \"Directly Relevant\" or \"Indirectly Relevant\" terms\nwhere no specific problematic component can be isolated.\n\nFULL vs. PARTIAL TERM EXCLUSION:\n- Full term exclusion: use when the entire search term is irrelevant and no part\n  of it has value\n- Partial term exclusion: use when only a specific word or modifier causes\n  irrelevance; isolate and exclude only that component\n\nMATCH TYPE SYNTAX:\n- Broad match negative  \u2192  no symbols       e.g.:  free\n- Phrase match negative \u2192  double quotes    e.g.:  \"how to\"\n- Exact match negative  \u2192  square brackets  e.g.:  [free accounting software]\n\nMATCH TYPE SELECTION:\n- Broad: short, universally problematic terms (e.g. free, jobs, salary, tutorial)\n- Phrase: multi-word problems where word order matters (e.g. \"how to\", \"what is\")\n- Exact: only when the term is problematic in this exact form but subsets of it\n  could be valuable\n\nEXCLUSION LEVEL:\n- Account Level  : universally irrelevant to the business; add to global exclusions list\n- Campaign Level : irrelevant to this campaign but potentially valid in another\n- Ad Group Level : relevant to the campaign but not this specific ad group theme\n\n-- SECTION 5: INCLUSION SUGGESTION RULES --\n\nWHEN TO SUGGEST AN INCLUSION:\nProvide an inclusion suggestion for search terms that represent a genuine\ntargeting opportunity \u2014 either as a new keyword to add, or as inspiration for\na new ad group or campaign theme. This applies to:\n- Terms with \"Directly Relevant\" or \"Indirectly Relevant\" relevancy that are\n  NOT already in \"Added\" status and show meaningful intent signal\n- Terms that reveal an unaddressed audience segment or service angle that\n  recurs across multiple queries (pattern-based opportunity)\n- Terms that are \"Directly Relevant\" but currently matched by a looser keyword\n  via Broad or Phrase match, suggesting they deserve their own Exact or Phrase\n  keyword entry for tighter control\n\nDo NOT suggest inclusions for:\n- Terms already in \"Added\" status (they are already keywords)\n- Terms with \"Directly Irrelevant\" or \"Indirectly Irrelevant\" relevancy\n- Terms with clearly informational or navigational intent and no commercial signal\n- Terms where the search volume is too low or the semantic match too vague to\n  justify adding as a standalone keyword\n\nINCLUSION TYPE:\nSpecify what type of addition is recommended:\n- New keyword  : add this term (or a close variant) as a new keyword to an\n                 existing ad group\n- New ad group : this term reveals a thematic gap; create a new ad group\n                 around this topic within an existing campaign\n- New campaign : this term signals a distinct enough audience or service theme\n                 to warrant a separate campaign structure\n- Bid increase : the term already exists but is likely underserved by current\n                 bid; flag for bid review rather than new addition\n\nINCLUSION LEVEL:\nSpecify where the addition should happen:\n- Account Level  : applicable as a shared theme across multiple campaigns\n- Campaign Level : new keyword or group within this specific campaign\n- Ad Group Level : add directly to this specific existing ad group\n\nMATCH TYPE RECOMMENDATION:\nSuggest the appropriate match type for the new keyword:\n- Exact   [keyword] : for high-value, precise terms where tight control is needed\n- Phrase  \"keyword\" : for terms with clear intent but natural query variation\n- Broad    keyword  : only if the term is a strong seed for discovery in a\n                      new theme with controlled bidding\n\nFORMAT:\nThe inclusion recommendation is split across three dedicated output columns:\n\n  inclusion_suggestion : The exact keyword to add, with match type symbols.\n                         - Exact match  \u2192 [keyword]\n                         - Phrase match \u2192 \"keyword\"\n                         - Broad match  \u2192 keyword\n                         Leave blank if no inclusion is warranted.\n\n  inclusion_type       : New keyword / New ad group / New campaign /\n                         Bid increase / N/A\n\n  inclusion_level      : Account Level / Campaign Level / Ad Group Level / N/A\n\n-- SECTION 6: OUTPUT FORMAT --\n\nPART 1 \u2014 INFERENCE SUMMARY\n\nState briefly what you inferred about the business from the dataset structure,\nand note that these inferences informed all decisions below.\n\nOutput this section with the heading EXACTLY as: ## PART 1 \u2014 INFERENCE SUMMARY\nDo not paraphrase, shorten, or alter this heading in any way.\n\nPART 2 \u2014 SEARCH TERMS ANALYSIS TABLE\n\nOutput a complete table containing every row from the input dataset. Do not skip,\nsummarize, or omit any row. If the dataset exceeds 50 rows, output in clearly\nlabeled batches of 50.\n\nOutput this section with the heading EXACTLY as: ## PART 2 \u2014 SEARCH TERMS ANALYSIS TABLE\nDo not paraphrase, shorten, or alter this heading in any way.\n\nColumn order \u2014 reproduce all original columns first, then append the new ones:\n\n1.  search_term\n2.  status\n3.  campaign\n4.  ad_group\n5.  matched_keyword\n6.  match_type\n7.  impressions\n8.  clicks\n9.  ctr\n10. cost\n11. avg_cpc\n12. avg_cpm\n13. conversion_rate\n14. cost_per_conversion\n15. intent_signal\n    Transactional / Commercial / Informational / Navigational\n16. relevancy\n    Directly Relevant / Indirectly Relevant / Borderline /\n    Indirectly Irrelevant / Directly Irrelevant\n17. relevancy_reasoning\n    1\u20133 sentences explaining why this label was assigned. Be specific \u2014 reference\n    the business context, intent signal, campaign fit, and any performance signals.\n    Generic explanations like \"not relevant to the business\" are not acceptable.\n18. exclusion_suggestion\n    The exact negative keyword to add, with correct match type symbols.\n    Leave blank if no exclusion is needed.\n19. exclusion_level\n    Account Level / Campaign Level / Ad Group Level / N/A\n20. priority_for_exclusion\n    High   = significant spend waste or brand risk; act immediately\n    Medium = moderate risk; address in next optimization cycle\n    Low    = minor issue; low volume or spend; address when convenient\n    N/A    = no exclusion needed\n21. inclusion_suggestion\n    The exact target keyword to add, with correct match type symbols. Leave blank if no inclusion is needed.\n22. inclusion_type\n    New keyword / New ad group / New campaign / Bid increase / N/A\n23. inclusion_level\n    Account Level / Campaign Level / Ad Group Level / N/A\n\nPART 3 \u2014 RECOMMEDNED GLOBAL EXCLUSIONS\n\nOutput this section with the heading EXACTLY as: ## PART 3 \u2014 RECOMMENDED GLOBAL EXCLUSIONS\nDo not paraphrase, shorten, or alter this heading in any way.\n\nThis list contains broad and phrase match terms that are universally or\nnear-universally irrelevant to this business across all or most campaigns.\nTypical candidates:\n- Short, high-frequency terms that trigger irrelevant queries at scale\n  (e.g. free, cheap, diy)\n- Informational modifiers that signal zero purchase intent\n  (e.g. \"what is\", \"how to\", definition, tutorial)\n- Job seeker terms (e.g. jobs, salary, career) if not applicable\n- Wrong audience or wrong industry terms\n- Competitor signals where appropriate\n\nFormat:\n\n| Negative Keyword | Match Type | Rationale |\n\nOrganize into groups:\n  Group 1: Informational / Zero-Intent Modifiers\n  Group 2: Price / Deal Seeking Terms\n  Group 3: DIY / Self-Service Terms (if applicable)\n  Group 4: Job Seeker / Employment Terms (if applicable)\n  Group 5: Wrong Audience / Wrong Industry Terms\n  Group 6: Brand / Competitor Terms (if applicable)\n  Group 7: Other Globally Problematic Terms\n\nOnly include terms that appear as a genuine problem pattern in the dataset or are\nclearly applicable global exclusions for this business type. Do not pad the list\nwith generic suggestions that do not apply.\n\n-- SECTION 7: QUALITY STANDARDS --\n\n1. COMPLETENESS: Process every single row. The output table must have exactly as\n   many data rows as the input.\n\n2. CONSISTENCY: Apply the relevancy scale and exclusion rules consistently.\n   Similar terms in similar contexts should receive similar labels unless there is\n   a documented reason for the difference.\n\n3. SPECIFICITY: Relevancy reasoning must reference the specific search term,\n   business context, intent, and campaign fit. Generic explanations are not\n   acceptable.\n\n4. PROPORTIONALITY: Match exclusion aggressiveness to actual risk. Do not exclude\n   broad terms that would also block relevant traffic. Do not under-exclude when\n   there is clear evidence of waste.\n\n5. DATA INTEGRITY: Reproduce all original column values exactly as provided.\n   Do not modify, round, or reformat any metric values.\n\n6. AMBIGUITY: When genuinely uncertain, default to \"Borderline\" rather than\n   guessing. Explain the ambiguity clearly in the reasoning column.\n\n7. LANGUAGE: Analyze and respond in the same language as the search terms.\n   If multiple languages are present in the dataset, note this and process each\n   term in its own language context.\n\n8. NO HALLUCINATION: Do not invent performance data, campaign names, or business\n   details. Work strictly with what is provided in the input."
            }
          ]
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "76efbb3b-4785-46ea-86a3-36ba89240fac",
      "name": "Save search terms & exclusion/inclusion suggestions",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1904,
        0
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "search_term",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "search_term",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "campaign",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "campaign",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ad_group",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ad_group",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "matched_keyword",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "matched_keyword",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "match_type",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "match_type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "impressions",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "impressions",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "clicks",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "clicks",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ctr",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ctr",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "cost",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "cost",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "avg_cpc",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "avg_cpc",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "avg_cpm",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "avg_cpm",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "conversion_rate",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "conversion_rate",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "cost_per_conversion",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "cost_per_conversion",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "intent_signal",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "intent_signal",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "relevancy",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "relevancy",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "relevancy_reasoning",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "relevancy_reasoning",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "exclusion_suggestion",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "exclusion_suggestion",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "exclusion_level",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "exclusion_level",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "priority_for_exclusion",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "priority_for_exclusion",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Create spreadsheet for storing analyzed search terms').item.json.sheets[0].properties.sheetId }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Create spreadsheet for storing analyzed search terms').item.json.spreadsheetId }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "ca76ce67-5369-4047-a4fa-c872b00acfe7",
      "name": "Extract & format global exclusions list",
      "type": "n8n-nodes-base.code",
      "position": [
        2112,
        0
      ],
      "parameters": {
        "jsCode": "const text = $('Analyze search terms & suggest keyword exclusions/inclusions').first().json.text;\n\nconst part3Match = text.match(/##\\s+(?:PART\\s+3\\s*[\u2014\\-]+\\s*)?RECOMMENDED GLOBAL EXCLUSIONS[\\s\\S]*/i);\nif (!part3Match) throw new Error(\"RECOMMENDED GLOBAL EXCLUSIONS section not found \u2014 check AI output format\");\n\nconst part3 = part3Match[0];\nconst rows = [];\nlet currentGroup = '';\n\nfor (const line of part3.split('\\n')) {\n\n  const groupMatch = line.match(/###\\s+(Group \\d+[^$]*)/);\n  if (groupMatch) {\n    currentGroup = groupMatch[1].trim();\n    continue;\n  }\n\n  if (line.startsWith('|') && !line.match(/^\\|[-| ]+\\|$/) && !line.includes('Negative Keyword')) {\n    const cols = line.split('|').slice(1, -1).map(c => c.trim());\n    if (cols[0] && cols[0] !== '') {\n      rows.push({\n        json: {\n          group:            currentGroup,\n          negative_keyword: cols[0].replace(/^\"|\"$/g, ''),\n          match_type:       cols[1],\n          rationale:        cols[2],\n        }\n      });\n    }\n  }\n}\n\nreturn rows;"
      },
      "typeVersion": 2
    },
    {
      "id": "1c9dff11-4efb-4f6a-8cce-df0c9587d5fe",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -992,
        -832
      ],
      "parameters": {
        "width": 1056,
        "height": 1152,
        "content": "## AI Google Ads Search Terms Analyzer\n\nThis workflow connects directly to your Google Ads account, pulls the last 30 days of search term data, and uses an AI model to analyze every term \u2014 assessing relevance, identifying wasted spend, and generating structured exclusion and inclusion recommendations \u2014 all saved automatically to Google Sheets.\n\n### How it works\n\n1. **Trigger** \u2014 Runs automatically every 30 days (or manually on demand)\n2. **Configure** \u2014 Sets your Google Ads IDs and business context in the Set node\n3. **Fetch** \u2014 Pulls search terms via Google Ads API (GAQL searchStream)\n4. **Clean & Format** \u2014 Normalizes and filters raw API data for AI processing\n5. **Analyze** \u2014 AI model performs deep search terms relevancy analysis, gives exclusion/inclusion suggestions, generates a global negative keyword list and inference summary\n6. **Save** \u2014 Results are written to a new Google Spreadsheet with 3 sheets: Search Terms Analysis, Global Exclusions List, and Inference Summary\n\n### Requirements\n\n- n8n (cloud or self-hosted)\n- Google Ads account with API access and a Developer Token\n- Google Ads OAuth 2.0 credentials\n- OpenAI API key (GPT-4.1-mini or higher recommended)\n- Google account with Sheets access\n\n### Before you run\n\nConnect your credentials in n8n:\n- **Google Ads OAuth 2.0** \u2192 used in the HTTP Request node\n- **OpenAI API** \u2192 used in the AI model node\n- **Google Sheets OAuth 2.0** \u2192 used in all Sheets nodes\n\nUpdate the **Set Google Ads IDs & business context** node with:\n- `google_ads_mcc` \u2014 your MCC (manager) account ID *(if accessing the account directly, leave this blank)*\n- `google_ads_customer` \u2014 your Google Ads Customer ID\n- `google_ads_developer_token` \u2014 your Google Ads Developer Token\n- Business context fields: company name, website, industry, company overview, target audience, campaign goals, key offerings, known irrelevant topics\n\n### Additional customization\n\n- **Change the AI model** \u2014 swap GPT-5.4-mini for Claude, Gemini, or any LLM supported by n8n\n- **Adjust the date range** \u2014 modify the `DURING LAST_30_DAYS` clause in the GAQL query to use a custom range (e.g. last 7 days, last quarter, current month)\n- **Change the schedule trigger interval** \u2014 edit the trigger interval to run the workflow weekly, monthly, or on any custom schedule\n- **Change the metrics threshold** \u2014 the Clean & Format node has a metrics filter that excludes terms with fewer than 5 impressions or 0 clicks; adjust the threshold to match your account's activity level\n- **Add Slack or email notifications** \u2014 append a notification node at the end to alert your team when a new analysis is ready\n- **Customize the AI business context** \u2014 the quality of AI analysis scales directly with the richness of the business context provided in the Set node; the more specific the company overview, target audience, and known irrelevant topics, the more precise the relevancy scoring\n- **Connect to Google Ads for direct upload** \u2014 extend the workflow to push approved exclusions back to Google Ads shared negative keyword lists via the Google Ads API"
      },
      "typeVersion": 1
    },
    {
      "id": "8470e139-0acb-46eb-b391-d4b056ce851a",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        144,
        -544
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 480,
        "content": "## Step 1 \u2014 Configure\n\nUpdate this node with your Google Ads credentials and business context before running.\n\n**Required IDs:**\n- `google_ads_mcc` \u2014 Manager account ID *(if accessing the account directly, leave this blank)*\n- `google_ads_customer` \u2014  Customer account ID\n- `google_ads_developer_token` \u2014 API Developer Token\n\n\n**Business context fields** \u2014 the more detail you provide here, the more accurate and actionable the AI analysis will be:\n- Company name, website, industry\n- Business description\n- Target audience profile\n- Campaign goals\n- Key offerings\n- Known irrelevant topics"
      },
      "typeVersion": 1
    },
    {
      "id": "1a23ab14-4f6d-4648-9fb4-a6de0b052148",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        576,
        -544
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 480,
        "content": "## Step 2 \u2014 Fetch Search Terms Data\n\nSends a POST request to the Google Ads API (v23) using the `googleAds:searchStream` endpoint.\n\nFetches all search terms from the last 30 days with the following attributes & metrics:\n- Campaign and ad group name\n- Matched keyword and match type\n- Search term status (Added / Excluded / None)\n- Impressions, clicks, CTR, cost, avg CPC/CPM\n- Conversions, conversion rate and cost per conversion\n\n\n\u26a0\ufe0f This node requires valid Google Ads OAuth 2.0 credentials and a Developer Token in the `developer-token` header.\n\nIf you access the client account directly (not via MCC), remove the `login-customer-id` header."
      },
      "typeVersion": 1
    },
    {
      "id": "cce4232c-fda3-41af-9dee-2ce10581878f",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        992,
        -832
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 768,
        "content": "## Step 3 \u2014 AI Search Term Analysis (GPT-5.4-mini)\n\nThe AI model receives the full cleaned search terms dataset + business context and performs a comprehensive analysis of every term.\n\nFor each search term, the AI outputs:\n- **Intent signal** \u2014 Transactional / Commercial / Informational / Navigational\n- **Relevancy** \u2014 5-level scale from Directly Relevant to Directly Irrelevant\n- **Relevancy reasoning** \u2014 specific, business-context-aware explanation\n- **Exclusion suggestion** \u2014 exact negative keyword with correct match type syntax\n- **Exclusion level** \u2014 Account / Campaign / Ad Group\n- **Priority for exclusion** \u2014 High / Medium / Low / N/A\n- **Inclusion suggestion** \u2014 keyword to add, with recommended match type\n- **Inclusion type** \u2014 New keyword / New ad group / New campaign\n- **Inclusion level** \u2014 Account / Campaign / Ad Group\n\n\nAdditionally produces:\n- **Recommended Global Exclusions** \u2014 grouped negative keyword list for account-wide application\n- **Inference Summary** \u2014 if business context was incomplete, the AI documents its assumptions\n\n\n\u26a0\ufe0f Processing time depends on dataset size. Default timeout is set to 180 seconds.\nYou can swap GPT for Claude, Gemini, or any LLM supported by n8n by changing the model node."
      },
      "typeVersion": 1
    },
    {
      "id": "898bf1a7-5d1c-47a1-b3bd-c20422d7371c",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1424,
        -400
      ],
      "parameters": {
        "color": 7,
        "width": 1392,
        "height": 336,
        "content": "## Step 4 \u2014 Save to Google Sheets\n\nA new Google Spreadsheet is automatically created on every run, named:\n\"Search Terms Analysis & Keyword Exclusion/Inclusion Suggestions (Company Name \u2014 DD/MM/YYYY HH:MM)\"\n\nThree sheets are written automatically:\n\n\ud83d\udcca **Search Terms Analysis**\nFull analysis table \u2014 every search term with all original metrics + AI-generated columns (intent, relevancy, reasoning, exclusion/inclusion recommendations).\n\n\ud83d\udeab **Global Exclusions List**\nGrouped negative keyword recommendations ready for direct upload to Google Ads shared negative keyword lists.\n\n\ud83d\udccb **Inference Summary**\nAI inference summary \u2014 documents assumptions made from business context fields and search terms dataset."
      },
      "typeVersion": 1
    },
    {
      "id": "5fd0a95c-f84b-4f58-ad2b-84703e77a398",
      "name": "Extract & format search terms inference summary",
      "type": "n8n-nodes-base.code",
      "position": [
        2512,
        0
      ],
      "parameters": {
        "jsCode": "const text = $('Analyze search terms & suggest keyword exclusions/inclusions').first().json.text;\n\nconst inferenceMatch = text.match(/## PART 1 \u2014 INFERENCE SUMMARY([\\s\\S]*?)(?=## PART 2 \u2014 SEARCH TERMS ANALYSIS TABLE)/);\n\nif (!inferenceMatch || !inferenceMatch[1].trim()) {\n  return [{ json: { inference_summary: null } }];\n}\n\nconst inferenceText = inferenceMatch[1]\n  .trim()\n  .replace(/\\n{3,}/g, '\\n\\n');\n\nreturn [{\n  json: {\n    inference_summary: inferenceText\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ae4b281c-43ea-4a47-8de4-666cdaa15251",
      "name": "Save inference summary",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2720,
        0
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "inference_summary",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "inference_summary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [
            "inference_summary"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Create spreadsheet for storing analyzed search terms').first().json.sheets[2].properties.sheetId }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Create spreadsheet for storing analyzed search terms').first().json.spreadsheetId }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "36029acb-9d07-4ed5-8008-be3318455b5d",
      "name": "Create spreadsheet for storing analyzed search terms",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1488,
        0
      ],
      "parameters": {
        "title": "=Search Terms Analysis & Keyword Exlusion/Inclusion Suggestions ({{ $('Set Google Ads IDs & business context').first().json.company_name }} - {{ $now.toFormat('dd/MM/yyyy HH:mm') }})",
        "options": {},
        "resource": "spreadsheet",
        "sheetsUi": {
          "sheetValues": [
            {
              "title": "Search Terms Analysis"
            },
            {
              "title": "Global Exclusions List"
            },
            {
              "title": "Inference Summary"
            }
          ]
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "c356e457-3540-4300-b7c8-891c79731732",
  "connections": {
    "GPT 5.4-mini": {
      "ai_languageModel": [
        [
          {
            "node": "Analyze search terms & suggest keyword exclusions/inclusions",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Execute workflow manually": {
      "main": [
        [
          {
            "node": "Set Google Ads IDs & business context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute workflow every 30 days": {
      "main": [
        [
          {
            "node": "Set Google Ads IDs & business context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save global exclusion suggestions": {
      "main": [
        [
          {
            "node": "Extract & format search terms inference summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean & format fetched data for AI": {
      "main": [
        [
          {
            "node": "Analyze search terms & suggest keyword exclusions/inclusions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Google Ads IDs & business context": {
      "main": [
        [
          {
            "node": "Fetch search terms data from Google Ads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract & format global exclusions list": {
      "main": [
        [
          {
            "node": "Save global exclusion suggestions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch search terms data from Google Ads": {
      "main": [
        [
          {
            "node": "Clean & format fetched data for AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract & format search terms analysis table": {
      "main": [
        [
          {
            "node": "Save search terms & exclusion/inclusion suggestions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract & format search terms inference summary": {
      "main": [
        [
          {
            "node": "Save inference summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save search terms & exclusion/inclusion suggestions": {
      "main": [
        [
          {
            "node": "Extract & format global exclusions list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create spreadsheet for storing analyzed search terms": {
      "main": [
        [
          {
            "node": "Extract & format search terms analysis table",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze search terms & suggest keyword exclusions/inclusions": {
      "main": [
        [
          {
            "node": "Create spreadsheet for storing analyzed search terms",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow is built for PPC managers, digital marketing agencies, and in-house marketing teams who run Google Ads search campaigns and want to stop wasting budget on irrelevant queries. Instead of manually reviewing hundreds or thousands of search terms in spreadsheets, this…

Source: https://n8n.io/workflows/15385/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

This n8n template demonstrates how to audit your brand’s visibility across multiple AI systems and automatically log the results to Google Sheets. It sends the same prompt to OpenAI, Perplexity, and (

Agent, Google Sheets, OpenAI Chat +4
AI & RAG

Transform a single quote into a fully-rendered cinematic short video — with voice-over, visuals, and music — then publish it directly to TikTok, Instagram Reels, and YouTube Shorts. This isn’t just au

Agent, HTTP Request, Jwt +7
AI & RAG

Disclaimer: As this workflow uses a Community node, it is available only to self-hosted installation of n8n

Google Sheets, HTTP Request, OpenAI Chat +4
AI & RAG

&gt; *Trend-style celebrity selfie videos

Form Trigger, OpenAI Chat, Chain Llm +3
AI & RAG

AI Blog Publisher – Automated Blog Content Workflow This workflow is designed for individuals and teams who regularly publish content on their blog and want to automate the entire process from start t

WordPress, HTTP Request, Memory Buffer Window +9