This workflow corresponds to n8n.io template #9750 — 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 →
{
"meta": {
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "21097b90-82b1-4c0b-aa25-889a470b99bd",
"name": "Get row(s) in sheet1",
"type": "n8n-nodes-base.googleSheets",
"position": [
-2000,
192
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupValue": "New",
"lookupColumn": "Status"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1807322001,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1goD6RasKx7GMx7yMa1ruigSE56IsnIkHnoUs6tFxTiE/edit#gid=1807322001",
"cachedResultName": "Control Panel"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1goD6RasKx7GMx7yMa1ruigSE56IsnIkHnoUs6tFxTiE",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1goD6RasKx7GMx7yMa1ruigSE56IsnIkHnoUs6tFxTiE/edit?usp=drivesdk",
"cachedResultName": "Seo Meta generation"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "a95a3e5d-4bce-47a0-9fc6-8adf5a4ca8d4",
"name": "Structured Output Parser",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
-528,
240
],
"parameters": {
"schemaType": "manual",
"inputSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"primary_keyword\": {\n \"type\": \"string\",\n \"description\": \"Main keyword or topic for SEO or content generation.\"\n },\n \"semantic_keyword_cluster\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n },\n \"description\": \"List of related semantic keywords around the primary keyword.\"\n },\n \"search_intent\": {\n \"type\": \"string\",\n \"enum\": [\"Informational\", \"Navigational\", \"Transactional\", \"Commercial\"],\n \"description\": \"The search intent type of the keyword.\"\n },\n \"target_audience\": {\n \"type\": \"string\",\n \"description\": \"The main audience or customer segment targeted.\"\n },\n \"content_angle\": {\n \"type\": \"string\",\n \"description\": \"The content style or perspective, e.g., Deep Dive, Case Study, How-To.\"\n },\n \"content_summary\": {\n \"type\": \"string\",\n \"description\": \"A short explanation or summary of the content.\"\n }\n },\n \"required\": [\n \"primary_keyword\",\n \"semantic_keyword_cluster\",\n \"search_intent\",\n \"target_audience\",\n \"content_angle\",\n \"content_summary\"\n ]\n}\n"
},
"typeVersion": 1.3
},
{
"id": "17a49a52-d070-4bf1-83d1-c0c74218ac56",
"name": "Googl SERP",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
-304,
16
],
"parameters": {
"url": "=https://serpapi.com/search",
"options": {
"redirect": {
"redirect": {}
},
"response": {
"response": {
"responseFormat": "text"
}
},
"allowUnauthorizedCerts": true
},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "api_key",
"value": "<your-api-key>"
},
{
"name": "engine",
"value": "=google"
},
{
"name": "q",
"value": "={{ $json.output.primary_keyword }}"
},
{
"name": "google_domain",
"value": "google.com"
},
{
"name": "hl",
"value": "en"
}
]
}
},
"typeVersion": 4.2,
"alwaysOutputData": false
},
{
"id": "4b99ad81-9984-4f03-ab7c-f891d182178f",
"name": "Code1",
"type": "n8n-nodes-base.code",
"position": [
-80,
16
],
"parameters": {
"jsCode": "// --- DYNAMIC SETUP ---\n\n// 1. Get the raw string from the 'data' key (Your correct fix!)\nconst rawSerpDataString = $input.first().json.data; \n\n// 2. THIS IS THE KEY: Parse that string into a proper JSON object\nconst serpApiOutput = JSON.parse(rawSerpDataString);\n\n// 3. Now we can safely access the organic_results\nconst organicResults = serpApiOutput.organic_results || [];\n\n// 4. Get our AI's analysis from the VERY FIRST Gemini node in the loop\n// **IMPORTANT**: This line assumes your first Gemini analyzer node is named 'Basic LLM Chain'\nconst ourPageAnalysis = $('AI Analyzer').first().json.output;\n\n// --- INTELLIGENT \"BAG OF WORDS\" ALLOWLIST ---\n\n// 5. Combine all our keywords into one big string\nconst allKeywordsString = [ourPageAnalysis.primary_keyword, ...ourPageAnalysis.semantic_keyword_cluster].join(' ');\n\n// 6. Define common \"stop words\" to ignore\nconst stopWords = new Set(['a', 'an', 'and', 'the', 'for', 'to', 'in', 'of', 'with', 'on', 'is', 'it', 'custom', 'websites', 'services']);\n\n// 7. Create our dynamic \"bag of words\"\nconst allowlist = allKeywordsString\n .toLowerCase()\n .split(/\\s+/) // Split by spaces\n .filter(word => word.length > 2 && !stopWords.has(word)); // Remove short words and stop words\n\nconst cleanedResults = [];\n\nfor (const result of organicResults) {\n const title = result.title ? result.title.trim() : '';\n const description = result.snippet ? result.snippet.trim() : '';\n const link = result.link ? result.link.trim() : '';\n\n const textToScan = (title + ' ' + description).toLowerCase();\n\n // --- DYNAMIC FILTERING LOGIC ---\n // Check if the competitor text contains ANY of the words from our \"bag of words\"\n const isRelevant = allowlist.some(keyword => textToScan.includes(keyword));\n\n if (title && description && link && isRelevant) {\n cleanedResults.push({\n title: title,\n description: description,\n link: link\n });\n }\n}\n\n// We only need the top 5-7 for AI analysis\nconst topRelevantResults = cleanedResults.slice(0, 7);\n\n// Pass the cleaned results to the next node\nreturn [{ json: { competitor_data_for_ai: topRelevantResults } }];"
},
"typeVersion": 2
},
{
"id": "b7c1c026-7fcb-471c-927d-f824c3d8a227",
"name": "Update row in sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
1776,
304
],
"parameters": {
"columns": {
"value": {
"URL": "={{ $('Update row in sheet1').item.json.URL }}",
"Status": "Generated",
"Ranking Factor ": "={{ $('Code').item.json.competitor_patterns }}",
"Current Meta Title": "={{ $('Edit Fields').item.json.current_title }}",
"Generated Meta Title": "={{ $json.optimized_title }}",
"Current Meta Description": "={{ $('Edit Fields').item.json.description }}",
"Generated Meta Description": "={{ $json.optimized_meta }}"
},
"schema": [
{
"id": "URL",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Current Meta Title",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Current Meta Title",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Current Meta Description",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Current Meta Description",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Generated Meta Title",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Generated Meta Title",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Generated Meta Description",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Generated Meta Description",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Ranking Factor ",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Ranking Factor ",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "row_number",
"type": "number",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "row_number",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"URL"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1807322001,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1goD6RasKx7GMx7yMa1ruigSE56IsnIkHnoUs6tFxTiE/edit#gid=1807322001",
"cachedResultName": "Control Panel"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1goD6RasKx7GMx7yMa1ruigSE56IsnIkHnoUs6tFxTiE",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1goD6RasKx7GMx7yMa1ruigSE56IsnIkHnoUs6tFxTiE/edit?usp=drivesdk",
"cachedResultName": "Seo Meta generation"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "75ec0306-25d5-44a1-a8b8-f26a6d0323d7",
"name": "Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-1776,
192
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "981f9de9-0b0e-4131-93b6-b10cb86cf9cf",
"name": "Update row in sheet1",
"type": "n8n-nodes-base.googleSheets",
"position": [
-1552,
16
],
"parameters": {
"columns": {
"value": {
"URL": "={{ $json.URL }}",
"Status": "Generating- wait for a few minutes"
},
"schema": [
{
"id": "URL",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Current Meta Title",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Current Meta Title",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Current Meta Description",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Current Meta Description",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Suggested Meta Title",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Suggested Meta Title",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Suggested Meta Description",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Suggested Meta Description",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Key Patterns Found",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Key Patterns Found",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "row_number",
"type": "number",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "row_number",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"URL"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1807322001,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1goD6RasKx7GMx7yMa1ruigSE56IsnIkHnoUs6tFxTiE/edit#gid=1807322001",
"cachedResultName": "Control Panel"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1goD6RasKx7GMx7yMa1ruigSE56IsnIkHnoUs6tFxTiE",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1goD6RasKx7GMx7yMa1ruigSE56IsnIkHnoUs6tFxTiE/edit?usp=drivesdk",
"cachedResultName": "Seo Meta generation"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "80628c57-5dcc-4302-85f9-947d04e47895",
"name": "HTML",
"type": "n8n-nodes-base.html",
"position": [
-1104,
16
],
"parameters": {
"options": {},
"operation": "extractHtmlContent",
"extractionValues": {
"values": [
{
"key": "current_title",
"cssSelector": "title",
"returnArray": true
},
{
"key": "description",
"attribute": "content",
"cssSelector": "meta[name=\"description\"]",
"returnArray": true,
"returnValue": "attribute"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "aaf1562b-8ff7-4f61-b696-31441d296df5",
"name": "Google Sheets Trigger",
"type": "n8n-nodes-base.googleSheetsTrigger",
"position": [
-2224,
192
],
"parameters": {
"event": "rowAdded",
"options": {},
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1807322001,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1goD6RasKx7GMx7yMa1ruigSE56IsnIkHnoUs6tFxTiE/edit#gid=1807322001",
"cachedResultName": "Control Panel"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1goD6RasKx7GMx7yMa1ruigSE56IsnIkHnoUs6tFxTiE",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1goD6RasKx7GMx7yMa1ruigSE56IsnIkHnoUs6tFxTiE/edit?usp=drivesdk",
"cachedResultName": "Seo Meta generation"
}
},
"credentials": {
"googleSheetsTriggerOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "ffa6f621-4744-4003-99d1-7172161a558a",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"position": [
-880,
16
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "cc1dd4d8-f155-4faf-be28-85bb4022dc3b",
"name": "current_title",
"type": "string",
"value": "={{ $json.current_title[0] }}"
},
{
"id": "80ba3fd4-9dae-4d35-8c94-2ab8d6a66a33",
"name": "description",
"type": "string",
"value": "={{ $json.description[0] }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "43ded5ba-a2dc-44f1-88a4-88acc2c886f8",
"name": "Google Gemini Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
-784,
304
],
"parameters": {
"options": {}
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "ed7b7a51-ca89-4e48-b7f3-3aabf6f824e2",
"name": "Google Gemini Chat Model1",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
80,
320
],
"parameters": {
"options": {}
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "3f0be34f-b1ba-4b02-bd7b-fe7f76852bb8",
"name": "Google Gemini Chat Model2",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
656,
240
],
"parameters": {
"options": {}
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "017bb8e3-82a8-4d24-85c7-a67f94f85850",
"name": "Code",
"type": "n8n-nodes-base.code",
"position": [
528,
16
],
"parameters": {
"jsCode": "// Get the messy text output from the Master Generator AI\nconst messyText = $json.text || '';\n\n// This regex finds and extracts ONLY the JSON part, ignoring everything else\nconst match = messyText.match(/\\{[\\s\\S]*\\}/);\n\nlet finalOutput = {};\n\nif (match && match[0]) {\n try {\n // Try to parse the clean JSON string\n finalOutput = JSON.parse(match[0]);\n } catch (error) {\n // If parsing fails, create an error object\n finalOutput = {\n optimized_title: 'PARSE_ERROR',\n optimized_meta: `Failed to parse JSON: ${error.message}`\n };\n }\n} else {\n // If no JSON is found at all, create an error object\n finalOutput = {\n optimized_title: 'NO_JSON_FOUND',\n optimized_meta: 'The AI did not return a valid JSON object.'\n };\n}\n\n// Pass the perfect, clean JSON object to the next node\nreturn [{ json: finalOutput }];"
},
"typeVersion": 2
},
{
"id": "14b14114-3cde-45d5-a646-f4e1ff640e1c",
"name": "Code2",
"type": "n8n-nodes-base.code",
"position": [
1152,
0
],
"parameters": {
"jsCode": "// Get the JSON string directly from the 'text' property\nconst jsonString = $json.text || '{}';\n\nlet finalOutput = {};\n\ntry {\n // Try to parse the clean JSON string\n finalOutput = JSON.parse(jsonString);\n} catch (error) {\n // If parsing fails for any reason, create an error object\n finalOutput = {\n optimized_title: 'PARSE_ERROR',\n optimized_meta: `Failed to parse JSON from AI: ${error.message}`\n };\n}\n\n// Pass the final JSON object to the next node\nreturn [{ json: finalOutput }];"
},
"typeVersion": 2
},
{
"id": "353ce1ea-4f1a-474f-b651-ede4fadf622e",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2288,
-672
],
"parameters": {
"width": 448,
"height": 1200,
"content": "# 1. START HERE: Get the To-Do List\n\n## This is the starting point for the generation process.\n\n### 1. **Trigger:** The workflow starts when you enter a new row in the Sheets\n### 2. **Read Sheet:** It reads the \"Control Panel\" Google Sheet.\n### 3. **Filter:** It intelligently grabs **only the URLs that have the status \"New\"**.\n\n### This ensures we only work on pages that are waiting to be optimized and don't re-process completed work."
},
"typeVersion": 1
},
{
"id": "2b6ff240-48f7-435d-aba9-c399c317e6df",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1824,
-672
],
"parameters": {
"width": 448,
"height": 1200,
"content": "# 2. Process URLs One-by-One\n\n\n\n\n## This is the starting point for the generation process.\n\n### 1. The **Loop** node takes our list of URLs from the sheet and processes them individually, one at a time.\n### 2. - The **Update Sheet** node immediately changes the status of the current URL to **\"Generating...\"**\n### 3. This provides real-time feedback in the Google Sheet and prevents other systems from accidentally processing the same URL twice.\n\n"
},
"typeVersion": 1
},
{
"id": "f6e32900-1ac4-4f4c-8108-821d2cde1235",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1360,
-672
],
"parameters": {
"width": 896,
"height": 1200,
"content": "# 3. PHASE 1: Analyze Our Own Page\n\n## This is the intelligence-gathering phase. The system figures out what our own page is truly about.\n\n### 1. **Scrape Website:** It visits the current URL and downloads all its content.\n### 2. **Extract Current Meta:** It finds and saves the **Current Meta Title & Description** that are already on the page for a before-and-after comparison.\n### 3. **AI Deep Dive (AI #1):** A powerful AI reads the page content and performs a \"Deep Dive\" analysis to figure out:\n - The real **Primary Keyword**\n - The **Target Audience**\n - The **Content's Goal** (e.g., to inform, to sell)\n - A short **Summary**"
},
"typeVersion": 1
},
{
"id": "0aff16f7-3317-4e5f-80d3-cd77b7daab6d",
"name": "AI Analyzer",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
-656,
16
],
"parameters": {
"text": "=You are an expert SEO Analyst and Content Strategist. Your task is to perform a deep analysis of the provided webpage text and extract key strategic insights. Your response must be a single, valid JSON object and nothing else. Do not add any introductory text, explanations, or markdown formatting like ```json.\n\n### WEBPAGE TEXT ###\n\"{{ $('Scrape Website').item.json.data }}\"\n\n### ANALYSIS INSTRUCTIONS ###\nBased on the text provided, perform the following analysis and populate the JSON object with these exact keys:\n\n1. `primary_keyword`: Identify the single, most important primary keyword or search phrase the content is targeting.\n2. `semantic_keyword_cluster`: Provide a JSON array of 5-7 closely related secondary and LSI keywords that create a semantic cluster around the primary keyword.\n3. `search_intent`: Determine the primary search intent. Choose ONLY ONE from this list: [\"Informational\", \"Commercial Investigation\", \"Transactional\", \"Navigational\"].\n4. `target_audience`: Briefly describe the target audience for this content in a few words (e.g., \"Beginner Hobbyists\", \"IT Professionals\", \"Small Business Owners\").\n5. `content_angle`: Identify the content's angle or format. Choose ONLY ONE from this list: [\"Listicle/Top X\", \"How-To Guide/Tutorial\", \"Product Review\", \"Product Comparison\", \"Case Study\", \"News/Report\", \"Opinion/Editorial\", \"Deep Dive/Explanation\"].\n6. `content_summary`: Write a one-sentence summary that captures the core value proposition of the content for the target audience.",
"batching": {},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 1.7
},
{
"id": "0479e704-5dfb-4801-a7f1-2b0fa7df41aa",
"name": "Scrape Website",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1328,
16
],
"parameters": {
"url": "https://api.scrapingdog.com/scrape",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "api_key",
"value": "<your-api-key>"
},
{
"name": "url",
"value": "={{ $json.URL }}"
},
{
"name": "dynamic",
"value": "true"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "432adc86-31d1-461e-9421-bad8ca367b35",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-448,
-672
],
"parameters": {
"width": 512,
"height": 1200,
"content": "# 4. PHASE 2: Spy on Competitors\n\n## This block performs a live competitive analysis on Google.\n\n### 1. **Google SERP:** It takes the Primary Keyword we just found and searches for it on Google using the SerpApi.\n### 2. **Code1 (The Dynamic Filter):** This is our secret weapon. It takes the messy Google results and uses our AI's \"semantic cluster\" to intelligently filter out all the junk, leaving us with a clean list of **true competitors**."
},
"typeVersion": 1
},
{
"id": "011d1b47-04e4-4ba2-8010-d2cb0321cc4b",
"name": "Competitor Analysis",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
176,
16
],
"parameters": {
"text": "=### ROLE ###\nYou are an expert SEO pattern detection bot. Your only job is to analyze the provided list of competitor titles and descriptions from a Google search. Summarize the common patterns, tones, and formats you observe, thinking like a world-class SEO strategist.\n\n### COMPETITOR DATA ###\n{{ JSON.stringify($node[\"Code1\"].json.competitor_data_for_ai) }}\n\n### TASK & STRICT OUTPUT RULES ###\nAnalyze the data and then respond ONLY with a single, valid JSON object. Do not add any conversational text, introductions, explanations, or markdown formatting like ```json. Your output must match the structure of the example below exactly.\n\n**Example of Perfect Output structure:**\n{\n \"competitor_patterns\": \"uses numbers for lists, includes the word 'Best', mentions specific brands (e.g., Framer), focuses on inspiration and examples, asks a question\"\n}",
"batching": {},
"promptType": "define"
},
"typeVersion": 1.7
},
{
"id": "adcba714-75d6-46b6-8661-e6494424fb29",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
80,
-672
],
"parameters": {
"width": 592,
"height": 1200,
"content": "# 5. Find the Competitor Strategies\n## This AI's only job is to analyze the clean list of competitors from the previous step.\n\n### It acts like an SEO strategist, identifying the **Top Ranker Strategies**\u2014the common patterns, keywords, and tones that the winning pages are using.\n\n### This vital intel is then passed to our final generator."
},
"typeVersion": 1
},
{
"id": "70030a73-ec32-4a63-af30-6ec24a39c981",
"name": "Master Generator",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
800,
0
],
"parameters": {
"text": "=You are an SEO optimization API. Your ONLY job is to return a valid JSON object. Your primary function is to adhere to strict length constraints above all else. Do not add any conversational text, introductions, or markdown formatting.\n\n### INTEL ###\n- Real Target Keyword: \"{{ $node['AI Analyzer'].json.output.primary_keyword }}\"\n- Our Page Summary: \"{{ $node['AI Analyzer'].json.output.content_summary }}\"\n- Our Page Angle: \"{{ $node['AI Analyzer'].json.output.content_angle }}\"\n- Competitor Patterns: \"{{ $json.competitor_patterns }}\"\n\n### YOUR FINAL TASK ###\nBased on ALL of the above intel, create a title and description that will rank for the Real Target Keyword.\n\n**CRITICAL RULES (NON-NEGOTIABLE):**\n1. The `optimized_title` value MUST be strictly under 60 characters including whitespaces .\n2. The `optimized_meta` value MUST be strictly under 160 characters including whitespaces.\n3. You MUST validate the character counts yourself before generating the final JSON. Failure to meet these character limits will result in an invalid output.\n\n### STRICT OUTPUT EXAMPLE ###\nYour response MUST be a single, valid JSON object matching this exact structure.Do not add any conversational text, introductions, explanations, or markdown formatting like ```json. Your output must match the structure of the example below exactly.\n\n// This is a perfect example of a valid response that follows all length rules:\n{\n \"optimized_title\": \"10 Best Framer Websites for Inspiration (2025)\",\n \"optimized_meta\": \"Discover top Framer examples for agencies and SaaS. Get inspired by the best no-code designs and build your amazing website today.\"\n}",
"batching": {},
"promptType": "define"
},
"typeVersion": 1.7
},
{
"id": "6f5a055b-be9b-4c45-b0b6-6b3126e65cd8",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
704,
-672
],
"parameters": {
"width": 592,
"height": 1200,
"content": "# 6. PHASE 3: The Master Generator\n\n## This is the final and most powerful AI in the system.\n\n### It synthesizes ALL the intel we've gathered:\n - Info about **our page** (from Phase 1)\n - The **Real Target Keyword** a user would search\n - The **Top Ranker Strategies** (from Phase 2)\n\n### Based on everything, it generates the final, high-quality, and length-constrained **Generated Meta Title & Description**."
},
"typeVersion": 1
},
{
"id": "bbb69642-c210-4827-83da-e3a98eb914c6",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
1344,
-672
],
"parameters": {
"width": 592,
"height": 1200,
"content": "# 7. The Final Write-Back\n\n## This is the end of the line for one URL.\n\n### This node takes all the data we've collected and generated\u2014the current metas, the new metas, the competitor insights\u2014and writes it all back to the correct row in our Google Sheet.\n\n### It then updates the status to **\"Generated\"**, and the loop moves on to the next URL in the list."
},
"typeVersion": 1
},
{
"id": "c59503df-a7a8-42c8-a55b-e0f514e690e2",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2368,
-1088
],
"parameters": {
"color": 5,
"width": 4368,
"height": 1792,
"content": "# SEO Meta TITLE and DESCRIPTION Generator"
},
"typeVersion": 1
}
],
"connections": {
"Code": {
"main": [
[
{
"node": "Master Generator",
"type": "main",
"index": 0
}
]
]
},
"HTML": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Code1": {
"main": [
[
{
"node": "Competitor Analysis",
"type": "main",
"index": 0
}
]
]
},
"Code2": {
"main": [
[
{
"node": "Update row in sheet",
"type": "main",
"index": 0
}
]
]
},
"Googl SERP": {
"main": [
[
{
"node": "Code1",
"type": "main",
"index": 0
}
]
]
},
"AI Analyzer": {
"main": [
[
{
"node": "Googl SERP",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields": {
"main": [
[
{
"node": "AI Analyzer",
"type": "main",
"index": 0
}
]
]
},
"Scrape Website": {
"main": [
[
{
"node": "HTML",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[],
[
{
"node": "Update row in sheet1",
"type": "main",
"index": 0
}
]
]
},
"Master Generator": {
"main": [
[
{
"node": "Code2",
"type": "main",
"index": 0
}
]
]
},
"Competitor Analysis": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Update row in sheet": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Get row(s) in sheet1": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Update row in sheet1": {
"main": [
[
{
"node": "Scrape Website",
"type": "main",
"index": 0
}
]
]
},
"Google Sheets Trigger": {
"main": [
[
{
"node": "Get row(s) in sheet1",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Analyzer",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Structured Output Parser": {
"ai_outputParser": [
[
{
"node": "AI Analyzer",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Google Gemini Chat Model1": {
"ai_languageModel": [
[
{
"node": "Competitor Analysis",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Google Gemini Chat Model2": {
"ai_languageModel": [
[
{
"node": "Master Generator",
"type": "ai_languageModel",
"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.
googlePalmApigoogleSheetsOAuth2ApigoogleSheetsTriggerOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automates the entire process of creating SEO-optimized meta titles and descriptions. It analyzes your webpage, spies on top-ranking competitors for the same keywords, and then uses a multi-step AI process to generate compelling, length-constrained meta tags.
Source: https://n8n.io/workflows/9750/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
This automation is designed to help you generate AI-powered music tracks, cover art, and fully rendered music videos — all triggered from a simple Telegram chat and managed via Google Sheets.
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
This workflow contains community nodes that are only compatible with the self-hosted version of n8n.
Stop manually digging through Meta Ads data and spending hours trying to connect the dots.