{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Google Ads Keyword Plan Builder",
  "tags": [],
  "nodes": [
    {
      "name": "Structure & format output for AI",
      "type": "n8n-nodes-base.code",
      "position": [
        304,
        -96
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst results = input.results;\n\nfunction microsToUSD(micros) {\n  if (!micros) return null;\n  return \"$\" + (parseInt(micros) / 1_000_000).toFixed(2);\n}\n\nconst keywords = results.map(item => {\n  const metrics = item.keywordIdeaMetrics || {};\n  return {\n    keyword: item.text,\n    avgMonthlySearches: metrics.avgMonthlySearches || \"0\",\n    competition: metrics.competition || \"UNSPECIFIED\",\n    competitionIndex: metrics.competitionIndex || \"0\",\n    lowTopOfPageBid: microsToUSD(metrics.lowTopOfPageBidMicros),\n    highTopOfPageBid: microsToUSD(metrics.highTopOfPageBidMicros)\n  };\n});\n\nreturn [{\n  json: {\n    keywords: keywords\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "name": "Parse AI output for Google Sheet",
      "type": "n8n-nodes-base.code",
      "position": [
        1184,
        -96
      ],
      "parameters": {
        "jsCode": "const raw = $('Analyze & enrich keywords with AI').first().json.text;\n\n// Dvostruki parse: prvo ukloni outer escaping, pa parsiraj JSON array\nconst keywords = JSON.parse(raw);\n\nreturn keywords.map(kw => ({ json: kw }));"
      },
      "typeVersion": 2
    },
    {
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -400,
        -704
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 576,
        "content": "**Step 1 \u2014 User Input**\n\nThe workflow starts with a form submission.\n\nCollected fields:\n- Website URL\n- Company name\n- Industry / Niche\n- Seed keywords (comma-separated)\n- Products/Services\n- Notes (optional)\n\nThese values are parsed and passed to both the Keyword Planner API call and the AI agent."
      },
      "typeVersion": 1
    },
    {
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        -704
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 576,
        "content": "**Step 2 \u2014 Google Keyword Planner API**\n\nSends a POST request to the Google Ads REST API (v23) using your seed keywords and/or website URL to generate keyword ideas. The `generateKeywordIdeas` endpoint supports `keywordSeed`, `urlSeed`, or `keywordAndUrlSeed` inputs.\n\nReturns keyword ideas with:\n- Average monthly searches\n- Competition level\n- Competition index\n- Low and high top-of-page bid estimates\n\nBefore running this step, update these values in the HTTP Request node:\n- `YOUR_GOOGLE_ADS_CUSTOMER_ID` in the request URL\n- `YOUR_GOOGLE_ADS_DEVELOPER_TOKEN` in the `developer-token` header\n- `YOUR_MANAGER_ACCOUNT_ID` in the `login-customer-id` header (only needed if you access the account through an MCC / manager account)\n\n\u26a0\ufe0f This node also requires valid Google Ads OAuth 2.0 credentials for the `Authorization` header. \n\nIf you access the client account directly, you may not need the `login-customer-id` header."
      },
      "typeVersion": 1
    },
    {
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        448,
        -704
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 576,
        "content": "**Step 3 \u2014 AI Keyword Analysis (GPT-5.4-mini)**\n\nThe AI chat model receives all keyword ideas + seed keywords + your business context and:\n\n1. Evaluates & Scores each keyword for relevance (1\u201310) based on your business\n2. Classifies search intent (informational / commercial / transactional / navigational)\n3. Assigns keyword type (branded, competitor, product/service, solution, application, etc.)\n4. Recommends match type & bids for Google Ads\n5. Generates additional AI keyword suggestions to fill gaps not covered by Keyword Planner\n\nOutput is a structured JSON array \u2014 one object per keyword."
      },
      "typeVersion": 1
    },
    {
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        880,
        -704
      ],
      "parameters": {
        "color": 7,
        "width": 1136,
        "height": 576,
        "content": "**Step 4 \u2014 Save to Google Sheets**\n\nTwo sheets are written automatically:\n\n\ud83d\udcca **Keyword Plan sheet** \u2014 AI-processed keywords with all enrichment fields\n\ud83d\udccb **Keyword Ideas sheet** \u2014 Raw Keyword Planner data (keyword ideas)\n\nA new Google Spreadsheet is created on each run, named after your company/topic.\n"
      },
      "typeVersion": 1
    },
    {
      "name": "GPT 5.4-mini",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        544,
        96
      ],
      "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
    },
    {
      "name": "Analyze & enrich keywords with AI",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        544,
        -96
      ],
      "parameters": {
        "text": "=Here is the keyword data to analyze:\n\nKeyword ideas: {{ JSON.stringify($json.keywords, null, 2) }}\nSeed keywords: {{ $('Parse & normalize user inputs').item.json.seedKeywords }}\n\nBusiness context:\n- Website: {{ $('Parse & normalize user inputs').item.json['Website URL'] }}\n- Company: {{ $('Parse & normalize user inputs').item.json['Company name'] }}\n- Industry: {{ $('Parse & normalize user inputs').item.json.Industry }}\n- Products/Services: {{ $('Parse & normalize user inputs').item.json['Products/Services'] }}\nNotes: {{ $('Parse & normalize user inputs').item.json['Notes'] }}",
        "batching": {},
        "messages": {
          "messageValues": [
            {
              "message": "You are a Google Ads keyword strategy expert. Your job is to analyze keyword\nideas from Google Keyword Planner together with business context provided by\nthe user, select the most strategically valuable keywords, enrich them with\nadditional high-intent suggestions, and produce a structured JSON output\noptimized for Google Ads campaigns.\n\n---\n\n## 1. INPUTS YOU WILL RECEIVE\n\n1. **Keyword ideas** \u2014 A JSON array from Google Keyword Planner containing\n   keyword ideas and their associated metrics:\n   - keyword, avgMonthlySearches, competition, competitionIndex,\n     lowTopOfPageBid, highTopOfPageBid, source, seedKeyword\n\n2. **Seed keywords** \u2014 A list of seed keywords provided by the user.\n   These represent the core products, services, or topics the campaign\n   should cover. Every output keyword must be traceable back to one of\n   these seeds.\n\n3. **Business context** (use all available fields to inform relevance\n   scoring, keyword classification, and AI-generated keyword suggestions):\n   - **Website URL** \u2014 the company's website; use to infer brand, market\n     positioning, and target audience if other context is limited\n   - **Company name** \u2014 treat this as the own brand name when classifying\n     `branded` keywords\n   - **Industry / niche** \u2014 the market segment the business operates in\n   - **Products / services** \u2014 detailed description of what the business\n     offers; the primary reference for evaluating keyword relevance\n   - **Notes** \u2014 additional preferences, exclusions, or campaign goals\n     provided by the user\n\n---\n\n## 2. YOUR TASK\n\nComplete the following steps in order:\n\n### Step 1 \u2014 Understand the Business Context\n\nBefore evaluating any keyword, thoroughly read and internalize all available\nbusiness context. Identify:\n- What the business sells and to whom\n- Which keywords represent core offerings vs. peripheral topics\n- Which brand names belong to the company (for `branded` classification) \u2014\n  derived from the Company name field\n- Which brand names belong to competitors (for `competitor` classification) \u2014\n  infer from industry context, product descriptions, and any mentions in Notes\n\nIf business context is missing or incomplete, proceed with what is available\nbut apply conservative relevance scoring for keywords whose fit cannot be\nconfirmed.\n\n### Step 2 \u2014 Evaluate and Select Keywords from \"Keyword ideas\"\n\nAnalyze ALL keywords in the provided Keyword ideas list. For each keyword,\nevaluate:\n- **Relevance** \u2014 How closely does this keyword match the business offerings\n  and the intent of its seed keyword?\n- **Search intent** \u2014 Does the keyword reflect a buyer, researcher, or\n  navigator? Higher commercial and transactional intent = higher strategic\n  value.\n- **Strategic value** \u2014 Consider search volume, competition level, and\n  estimated bid in relation to relevance. A low-volume, high-intent keyword\n  can outperform a high-volume, low-intent one.\n\n**Include** keywords with a relevanceScore of 5 or higher.\n**Exclude** keywords that are:\n- Clearly irrelevant to the business (relevanceScore 1\u20134)\n- Too generic to target effectively without strong context\n- Duplicates or near-identical variants of higher-scoring keywords already\n  included\n\n### Step 3 \u2014 Generate AI-Suggested Keywords\n\nAfter processing all keywords from the Keyword ideas input, generate\nadditional keywords using your own knowledge. For each entry in\nSeed keywords:\n\n- Review the keywords already selected from that seed in Step 2\n- Identify meaningful gaps \u2014 high-intent keyword angles that are not yet\n  covered (e.g. missing competitor terms, strong solution-based queries,\n  overlooked application contexts)\n- Generate **1 to 3 new keywords per seed keyword** that:\n  - Are highly relevant to the business context\n  - Fill a gap not already covered by Keyword Planner results\n  - Have realistic search demand (do not invent obscure or unnatural phrases)\n  - Are written in the same language as the seed keywords\n  - Are not duplicates of any keyword already present in Keyword ideas\n\nThese AI-generated keywords must be tagged with source = \"AI\" and will have\navgMonthlySearches = null and competition = null, as no Keyword Planner\ndata is available for them.\n\n### Step 4 \u2014 Assemble the Output\n\nCombine all selected keywords from Step 2 and all AI-generated keywords\nfrom Step 3 into a single JSON array. For every keyword, populate all\nrequired fields as defined in the Output Format and Field Definitions \nsections below.\n\n---\n\n## 3. OUTPUT FORMAT\n\nReturn a valid JSON array only \u2014 no markdown, no explanation text,\nno code blocks. Just the raw JSON array.\n\nEach object in the array must contain exactly these fields:\n\n{\n  \"keyword\": string,\n  \"searchIntent\": string,\n  \"keywordType\": string,\n  \"source\": string,\n  \"seedKeyword\": string,\n  \"relevanceScore\": number (1\u201310),\n  \"avgMonthlySearches\": string or null,\n  \"competition\": string or null,\n  \"recommendedBid\": string or null,\n  \"recommendedMatchType\": string\n}\n\n---\n\n## 4. FIELD DEFINITIONS AND RULES\n\nEvery keyword must be assigned all fields below. Apply each rule strictly\nand consistently across all keywords.\n\n### keyword\nPass through the keyword string exactly as received in the input.\nDo not normalize, translate, or modify the keyword text in any way.\n> Source: Keyword ideas \u2192 field: keyword\n\n**Exception:** If source = \"AI\" (the keyword was generated by the AI model\nand was not present in the input), use the keyword string as invented\nby the AI during processing. Ensure it is:\n- Written in the same language as the seed keywords provided by the user\n- Not a duplicate of any keyword already present in Keyword ideas\n- Relevant to the assigned seedKeyword and consistent with the business\n  context provided\n\n### searchIntent\n\nClassify each keyword into exactly one of the following intent types:\n\n**informational**\nThe user is in the research phase and seeking to learn or understand something.\nNo clear purchase intent.\n> Content target: blog posts, guides, how-to articles.\n> Example: \"how does RAM affect laptop performance\",\n  \"what is pronation in running shoes\", \"types of laptop processors explained\"\n\n**navigational**\nThe user is trying to reach a specific website, brand page, or resource they\nalready know. Brand awareness is assumed.\n> Content target: homepage, brand landing page.\n> Example: \"Dell official website\", \"Nike store online\", \"Apple support MacBook\"\n\n**commercial \u2014 evaluation**\nThe user is actively comparing options, reading reviews, or building a shortlist.\nHigh research intent with emerging purchase readiness.\n> Trigger signals: \"best\", \"vs\", \"top\", \"review\", \"compare\", \"alternative\"\n> Content target: comparison pages, review landing pages, category PDPs.\n> Example: \"best laptops under $1000 2025\", \"Nike vs Adidas running shoes\",\n  \"top-rated waterproof hiking shoes\"\n\n**commercial \u2014 specification**\nThe user knows the product category and is drilling into technical details\nor specific models. Indicates a buyer close to a decision.\n> Trigger signals: model numbers, spec terms, technical comparisons\n> Content target: product detail pages, spec sheets, configurators.\n> Example: \"Dell XPS 15 vs MacBook Pro 14 specs\", \"Nike Pegasus 41 vs Vomero 17\",\n  \"i7 vs i9 laptop performance benchmark\"\n\n**transactional**\nThe user has clear buying intent and is ready to take action \u2014\npurchase, request a quote, or get a demo.\n> Trigger signals: \"buy\", \"price\", \"for sale\", \"order\", \"discount\",\n  \"coupon\", \"quote\", \"deal\", \"shop\", \"cheap\"\n> Content target: product pages, checkout, contact/quote forms.\n> Example: \"buy MacBook Pro online\", \"Nike Air Max 90 price\",\n  \"gaming laptop for sale cheap\"\n\n**Edge cases:**\n1. **commercial (evaluation) vs. commercial (specification)**\n   - \"best\", \"vs\", \"top\", \"review\" \u2192 evaluation\n   - Model numbers, spec comparisons, technical deep-dives \u2192 specification\n\n2. **informational vs. solution (keywordType)** \u2014 Both lack direct purchase\n   signals, but a `solution` keyword implies a product need.\n   `informational` intent is purely knowledge-seeking.\n   - \"why do knees hurt when running\" \u2192 informational (intent)\n   - \"shoes that reduce knee pain\" \u2192 solution (type) + informational (intent)\n\n3. **competitor + transactional** \u2014 If a competitor keyword also contains\n   buying signals, the intent is `transactional`.\n   - \"Lenovo ThinkPad price\" \u2192 competitor (type) + transactional (intent)\n\n### keywordType\n\nClassify each keyword into exactly one of the following types:\n\n**branded**\nThe keyword contains the company's own brand name or product line name.\nApplies when users are already aware of your brand.\n> Example: \"Dell XPS laptop\", \"Nike Air Max\", \"Apple MacBook Pro 14\"\n\n**competitor**\nThe keyword contains a competitor's brand name, product name, or model number.\nUse this to identify conquest targeting opportunities.\n> Note: If a competitor brand name is present, always classify as `competitor`\n  regardless of other intent signals.\n> Example: \"HP Spectre x360 review\", \"Adidas Ultraboost running shoe\",\n  \"Lenovo ThinkPad price\"\n\n**product/service**\nA direct product category, product type, or service name \u2014 without a specific brand.\nThese are core commercial keywords that describe what you sell.\n> Example: \"gaming laptop\", \"running shoes for men\",\n  \"wireless noise-cancelling headphones\"\n\n**application**\nThe keyword refers to a specific use case, task, or context in which the product\nis used. Focus is on what the user is doing or trying to accomplish,\nnot the product itself.\n> Example: \"laptop for video editing\", \"shoes for standing all day\",\n  \"notebook for college students\"\n\n**solution**\nThe keyword describes a problem, pain point, or desired outcome \u2014 the product\nis implied as the answer. These keywords reflect early-stage or problem-aware buyers.\n> Example: \"how to fix back pain from standing\", \"laptop that doesn't overheat\",\n  \"lightweight shoes for travel\"\n\n**synonym**\nAn alternative name, informal term, or industry jargon for the same product type.\nFunctionally identical to product/service keywords but using different vocabulary.\n> Example: \"ultrabook\" (= thin laptop), \"kicks\" (= sneakers),\n  \"notebook computer\" (= laptop)\n\n**feature/spec**\nThe keyword is primarily driven by a specific technical feature, specification,\nor attribute. Common in B2B or high-consideration purchases where buyers know\nexactly what they need.\n> Example: \"laptop with 32GB RAM\", \"shoes with arch support\",\n  \"4K OLED display laptop under 2kg\"\n\n**Edge cases:**\n1. **synonym vs. application** \u2014 If the keyword is just another name for the\n   product, use `synonym`. If it adds a context or use case, use `application`.\n   - \"sneakers\" \u2192 synonym | \"sneakers for gym workouts\" \u2192 application\n\n2. **solution vs. application** \u2014 If the keyword describes a problem or\n   desired outcome the product solves, use `solution`. If it describes a\n   specific task context where the product is used, use `application`.\n   - \"shoes that reduce knee pain\" \u2192 solution | \"shoes for nurses\" \u2192 application\n\n3. **feature/spec vs. product/service** \u2014 If a technical attribute is the\n   dominant search signal, use `feature/spec`. If the keyword is primarily\n   a category name, use `product/service`.\n   - \"laptop\" \u2192 product/service | \"laptop with 16GB RAM OLED display\" \u2192 feature/spec\n\n4. **competitor override** \u2014 If a competitor brand name is present, always\n   classify as `competitor`, regardless of other signals.\n   - \"Adidas running shoes price\" \u2192 competitor (not transactional)\n\n### source\n\nIndicates how the keyword was obtained. Assign exactly one of the following\nvalues based on whether the keyword existed in the input or was newly created:\n\n| Value               | When to Use |\n|---------------------|-------------|\n| **Keyword Planner** | The keyword was present in Keyword ideas in the input |\n| **AI**              | The keyword was NOT present in Keyword ideas \u2014 it was created by the AI model |\n\n**Assignment logic (strict):**\n1. For every output keyword, check whether the exact keyword string exists\n   in the Keyword ideas received in the input.\n2. If YES \u2192 source = \"Keyword Planner\"\n3. If NO \u2192 source = \"AI\"\n\n> Never assign source based on assumptions or keyword style. The only valid\n  check is whether the keyword string was present in the input or not.\n\n### seedKeyword\n\nThe original seed keyword provided by the user that this keyword was derived\nfrom, retrieved for, or is most closely associated with.\n> Source: Seed keywords (exact values as provided by the user)\n\n**Assignment rules:**\n1. **Direct match** \u2014 If source = \"Keyword Planner\", assign the seed keyword\n   that was used as the Keyword Planner input to retrieve it. This mapping\n   is available in the input data.\n\n2. **AI-generated keywords** \u2014 If source = \"AI\", assign the seed keyword\n   that most directly inspired or relates to this keyword by product type,\n   use case, or user intent.\n\n3. **Ambiguous association** \u2014 If a keyword could plausibly relate to more\n   than one seed, assign the single seed keyword it most closely matches\n   by core product or service meaning \u2014 not just by shared words.\n   - \"portable noise-cancelling headset\" with seeds [\"headphones\", \"laptop bags\"]\n     \u2192 seedKeyword = \"headphones\" (product match beats word overlap)\n\n4. **Exact wording** \u2014 Always use the seed keyword exactly as written in\n   Seed keywords. Do not paraphrase, normalize, translate, or reformat.\n   - If the user wrote \"running shoes\" \u2192 always output \"running shoes\",\n     never \"Running Shoes\", \"run shoes\", or \"shoes for running\"\n\n5. **Never leave empty** \u2014 Every keyword must have a seedKeyword assigned.\n   If no direct match exists, assign the closest seed keyword by intent\n   and product relevance.\n\n### relevanceScore (1\u201310)\n\nScore how relevant this keyword is for the business offerings provided:\n\n| Score | Meaning |\n|-------|---------|\n| 9\u201310  | Exact match to core product/service, high commercial intent |\n| 7\u20138   | Closely related, likely buyer intent |\n| 5\u20136   | Somewhat related, broader or indirect intent |\n| 3\u20134   | Loosely related, informational or tangential |\n| 1\u20132   | Barely relevant, unlikely to convert |\n\n### avgMonthlySearches\n\nPass through this value exactly as received in the input. Do not estimate\nor modify it.\n> Source: Keyword ideas \u2192 field: avgMonthlySearches\n\n**Exception:** If source = \"AI\", set this field to null \u2014 no search volume\ndata is available for AI-generated keywords.\n\n### competition\n\nPass through this value exactly as received in the input. Do not estimate\nor modify it.\n> Source: Keyword ideas \u2192 field: competition\n\n**Exception:** If source = \"AI\", set this field to null \u2014 no competition\ndata is available for AI-generated keywords.\n\n### recommendedBid\n\nCalculate the recommended bid using competitionIndex and bid range values\nfrom the input. Do not pass through \u2014 this is a computed field.\n> Source: Keyword ideas \u2192 fields: competitionIndex, lowTopOfPageBid,\n  highTopOfPageBid\n\n| competitionIndex | Rule                                          | Output  |\n|------------------|-----------------------------------------------|---------|\n| \u2265 70 (HIGH)      | Use highTopOfPageBid                          | \"$X.XX\" |\n| 30\u201369 (MEDIUM)   | (lowTopOfPageBid + highTopOfPageBid) / 2      | \"$X.XX\" |\n| < 30 (LOW)       | Use lowTopOfPageBid                           | \"$X.XX\" |\n\nIf both lowTopOfPageBid and highTopOfPageBid are null \u2192 return null.\n\n### recommendedMatchType\n\nAssign match type based on keywordType and keyword length/specificity.\nThis is a computed field \u2014 do not pass through from input.\n\n| Match Type  | When to Use |\n|-------------|-------------|\n| **Exact**   | Branded, competitor, or highly specific long-tail keywords (3+ words, niche product names or model numbers) |\n| **Phrase**  | Product, application, or feature keywords with moderate specificity (2\u20133 words) |\n| **Broad**   | Broader category, synonym, or solution keywords where discovery and reach are valuable |\n\n---\n\n## 5. IMPORTANT RULES\n\n- Return ONLY the raw JSON array \u2014 no preamble, no explanation,\n  no markdown formatting, no code blocks.\n- Apply all filtering, scoring, and classification decisions in the context\n  of the specific business provided \u2014 the same keyword may score and\n  classify differently for different businesses.\n- When recommendedBid has a calculated value, always format it as \"$X.XX\".\n  When bid data is unavailable, return null \u2014 never substitute \"$0.00\"\n  or any placeholder value.\n- Never leave any field empty or undefined. Every object in the output\n  array must contain all required fields with a valid value or explicit null."
            }
          ]
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "name": "Get keyword ideas from Google Keyword Planner",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        64,
        -96
      ],
      "parameters": {
        "url": "https://googleads.googleapis.com/v23/customers/YOUR_GOOGLE_ADS_CUSTOMER_ID:generateKeywordIdeas",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"language\": \"languageConstants/1000\",\n  \"geoTargetConstants\": [\n    \"geoTargetConstants/2840\"\n  ],\n  \"includeAdultKeywords\": false,\n  \"keywordPlanNetwork\": \"GOOGLE_SEARCH_AND_PARTNERS\",\n  \"keywordSeed\": {\n    \"keywords\": {{ JSON.stringify($json.seedKeywords) }}\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "developer-token",
              "value": "YOUR_GOOGLE_ADS_DEVELOPER_TOKEN"
            },
            {
              "name": "login-customer-id",
              "value": "YOUR_MANAGER_ACCOUNT_ID"
            }
          ]
        },
        "nodeCredentialType": "googleAdsOAuth2Api"
      },
      "credentials": {
        "googleAdsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "name": "Save keyword plan to Google Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1440,
        -96
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "keyword",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "keyword",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "avgMonthlySearches",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "avgMonthlySearches",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "competition",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "competition",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "competitionIndex",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "competitionIndex",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "relevanceScore",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "relevanceScore",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "keywordType",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "keywordType",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "recommendedBid",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "recommendedBid",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "recommendedMatchType",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "recommendedMatchType",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Create spreadsheet for saving keyword plan & keyword ideas').item.json.sheets[0].properties.sheetId }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Create spreadsheet for saving keyword plan & keyword ideas').item.json.spreadsheetId }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "name": "Save all keyword ideas to Google Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1920,
        -96
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "keyword",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "keyword",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "avgMonthlySearches",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "avgMonthlySearches",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "competition",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "competition",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "competitionIndex",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "competitionIndex",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "lowTopOfPageBid",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "lowTopOfPageBid",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "highTopOfPageBid",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "highTopOfPageBid",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Create spreadsheet for saving keyword plan & keyword ideas').first().json.sheets[1].properties.sheetId }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Create spreadsheet for saving keyword plan & keyword ideas').first().json.spreadsheetId }}"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "name": "Parse Google Keyword Planner output for Google Sheet",
      "type": "n8n-nodes-base.code",
      "position": [
        1680,
        -96
      ],
      "parameters": {
        "jsCode": "const results = $('Get keyword ideas from Google Keyword Planner').first().json.results;\n\nfunction microsToUSD(micros) {\n  if (!micros) return null;\n  return \"$\" + (parseInt(micros) / 1_000_000).toFixed(2);\n}\n\nreturn results.map(item => {\n  const metrics = item.keywordIdeaMetrics || {};\n  return {\n    json: {\n      keyword: item.text,\n      avgMonthlySearches: metrics.avgMonthlySearches || \"0\",\n      competition: metrics.competition || \"UNSPECIFIED\",\n      competitionIndex: metrics.competitionIndex || \"0\",\n      lowTopOfPageBid: microsToUSD(metrics.lowTopOfPageBidMicros),\n      highTopOfPageBid: microsToUSD(metrics.highTopOfPageBidMicros)\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "name": "Parse & normalize user inputs",
      "type": "n8n-nodes-base.code",
      "position": [
        -176,
        -96
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\n// Helper: \u010disti string od leading/trailing spaces\nfunction clean(str) {\n  return (str || '').trim();\n}\n\n// Helper: normalizuje separator - radi sa zarezom, ta\u010dka-zarezom, | i vi\u0161estrukim razmacima\nfunction parseList(str) {\n  return (str || '')\n    .replace(/[;|\\\\/]/g, ',')\n    .split(',')\n    .map(item => item.trim())\n    .filter(item => item.length > 0);\n}\n\nconst websiteUrl = clean(input['Website URL']);\nconst companyName = clean(input['Company name']);\nconst industry = clean(input['Industry']);\nconst productsRaw = clean(input['Products/Services']);\nconst notes = clean(input['Notes']);\n\nconst seedKeywords = parseList(input['Seed keywords']);\n\nreturn [{\n  json: {\n    ...input,\n    'Website URL': websiteUrl,\n    'Company name': companyName,\n    'Industry': industry,\n    'Products/Services': productsRaw,\n    'Notes': notes,\n    seedKeywords,\n    productsFormatted: productsRaw\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "name": "Form trigger - User input",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -400,
        -96
      ],
      "parameters": {
        "options": {},
        "formTitle": "Google Ads Keyword Research Tool",
        "formFields": {
          "values": [
            {
              "fieldType": "textarea",
              "fieldLabel": "Seed keywords",
              "placeholder": "keyword 1, keyword 2, keyword 3...(max 10 keywords)",
              "requiredField": true
            },
            {
              "fieldLabel": "Website URL",
              "placeholder": "https://site.com",
              "requiredField": true
            },
            {
              "fieldLabel": "Company name",
              "placeholder": "Company",
              "requiredField": true
            },
            {
              "fieldLabel": "Industry",
              "placeholder": "Industry/Niche",
              "requiredField": true
            },
            {
              "fieldType": "textarea",
              "fieldLabel": "Products/Services",
              "placeholder": "Describe company offerings here",
              "requiredField": true
            },
            {
              "fieldType": "textarea",
              "fieldLabel": "Notes",
              "placeholder": "Additonal notes or preferences"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "name": "Create spreadsheet for saving keyword plan & keyword ideas",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        928,
        -96
      ],
      "parameters": {
        "title": "={{ $('Parse & normalize user inputs').item.json['Company name'] }}-{{ $('Parse & normalize user inputs').item.json.submittedAt }}",
        "options": {},
        "resource": "spreadsheet",
        "sheetsUi": {
          "sheetValues": [
            {
              "title": "Keyword Plan"
            },
            {
              "title": "All Keyword Ideas"
            }
          ]
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1232,
        -704
      ],
      "parameters": {
        "color": 2,
        "width": 752,
        "height": 976,
        "content": "## Google Ads Keyword Research Tool\n\nThis workflow turns seed keywords and business context into a structured keyword plan for Google Ads.\n\n### How it works\n1. Collects your inputs from a form:\n   - Website URL\n   - Company name\n   - Industry / niche\n   - Seed keywords\n   - Products / services\n   - Notes\n2. Sends the seed keywords to Google Keyword Planner to retrieve keyword ideas and metrics.\n3. Passes all keyword ideas + business context to an AI model for:\n   - evaluation & relevance scoring\n   - search intent classification\n   - keyword type classification\n   - recommended bid and match type\n   - additional AI-generated keyword suggestions\n4. Saves the final output to Google Sheets:\n   - Keyword Plan\n   - All Keyword Ideas\n\n### Requirements\n- Google Ads API access with a Developer Token\n- Google Ads OAuth 2.0 credentials\n- OpenAI API key\n- Google Sheets access\n\n### Before you run\nConnect your own credentials in n8n:\n- Google Ads OAuth 2.0 credentials\n- Google Sheets credentials\n- OpenAI credentials\n\nUpdate the placeholders in the HTTP Request node:\n- `YOUR_GOOGLE_ADS_CUSTOMER_ID`\n- `YOUR_GOOGLE_ADS_DEVELOPER_TOKEN`\n- `YOUR_MANAGER_ACCOUNT_ID` if you use an MCC account\n\nThis template is designed to be plug-and-play, but all credentials must be connected in your own n8n workspace before running the workflow."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "GPT 5.4-mini": {
      "ai_languageModel": [
        [
          {
            "node": "Analyze & enrich keywords with AI",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Form trigger - User input": {
      "main": [
        [
          {
            "node": "Parse & normalize user inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse & normalize user inputs": {
      "main": [
        [
          {
            "node": "Get keyword ideas from Google Keyword Planner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI output for Google Sheet": {
      "main": [
        [
          {
            "node": "Save keyword plan to Google Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Structure & format output for AI": {
      "main": [
        [
          {
            "node": "Analyze & enrich keywords with AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze & enrich keywords with AI": {
      "main": [
        [
          {
            "node": "Create spreadsheet for saving keyword plan & keyword ideas",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save keyword plan to Google Sheet": {
      "main": [
        [
          {
            "node": "Parse Google Keyword Planner output for Google Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save all keyword ideas to Google Sheet": {
      "main": [
        []
      ]
    },
    "Get keyword ideas from Google Keyword Planner": {
      "main": [
        [
          {
            "node": "Structure & format output for AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Google Keyword Planner output for Google Sheet": {
      "main": [
        [
          {
            "node": "Save all keyword ideas to Google Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create spreadsheet for saving keyword plan & keyword ideas": {
      "main": [
        [
          {
            "node": "Parse AI output for Google Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}