This workflow corresponds to n8n.io template #6006 — we link there as the canonical source.
This workflow follows the Gmail → HTTP Request 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 →
{
"id": "83yfuiWClSX51Ebj",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Weekly GSC Pulse Check - Generic",
"tags": [
{
"id": "10",
"name": "SEO",
"createdAt": "2022-11-25T12:57:02.999Z",
"updatedAt": "2022-11-25T12:57:02.999Z"
},
{
"id": "dO9obbaAMg8UNnbo",
"name": "Scheduled",
"createdAt": "2025-07-10T15:12:41.643Z",
"updatedAt": "2025-07-10T15:12:41.643Z"
}
],
"nodes": [
{
"id": "fdcaa6bc-9f69-45db-9d87-6fde0f2266a6",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
860,
1580
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 15 * * 1"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "cb54e593-9335-4252-a69f-5541ab17b32b",
"name": "If",
"type": "n8n-nodes-base.if",
"position": [
1180,
1580
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "8ab18755-9c4f-40d8-a0c0-f16ab3b7d940",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.label }}",
"rightValue": "priorWeek"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "56e3f13b-c094-45d4-9832-75a1621c44eb",
"name": "priorWeek",
"type": "n8n-nodes-base.httpRequest",
"notes": "Connect GSC account",
"position": [
1360,
1500
],
"parameters": {
"method": "POST",
"options": {},
"jsonBody": "={\n \"startDate\": \"{{ $json.startDate }}\",\n \"endDate\": \"{{ $json.endDate }}\",\n \"dimensions\": [\"page\", \"query\"],\n \"rowLimit\": 2500,\n \"dataState\": \"all\"\n}\n",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleOAuth2Api"
},
"credentials": {
"googleOAuth2Api": {
"name": "<your credential>"
}
},
"notesInFlow": true,
"typeVersion": 4.2
},
{
"id": "0b192fcb-02fb-4220-b6fe-2132c0632129",
"name": "lastWeek",
"type": "n8n-nodes-base.httpRequest",
"notes": "Connect GSC account",
"position": [
1360,
1660
],
"parameters": {
"method": "POST",
"options": {},
"jsonBody": "={\n \"startDate\": \"{{ $json.startDate }}\",\n \"endDate\": \"{{ $json.endDate }}\",\n \"dimensions\": [\"page\", \"query\"],\n \"rowLimit\": 2500,\n \"dataState\": \"all\"\n}\n",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleOAuth2Api"
},
"credentials": {
"googleOAuth2Api": {
"name": "<your credential>"
}
},
"notesInFlow": true,
"typeVersion": 4.2
},
{
"id": "8221a2d9-c93d-4675-8090-b4559dcce4f4",
"name": "label",
"type": "n8n-nodes-base.code",
"position": [
1500,
1500
],
"parameters": {
"jsCode": "return items.map(item => {\n return {\n json: {\n week: \"priorWeek\", // or \"lastWeek\" in the other branch\n ...item.json // preserve everything from GSC response\n }\n };\n});\n"
},
"typeVersion": 2
},
{
"id": "8e242ef0-3aa0-43dd-8472-9dfadb5b868d",
"name": "label1",
"type": "n8n-nodes-base.code",
"position": [
1500,
1660
],
"parameters": {
"jsCode": "return items.map(item => {\n return {\n json: {\n week: \"lastWeek\", // or \"priorWeek\" in the other branch\n ...item.json // preserve everything from GSC response\n }\n };\n});\n"
},
"typeVersion": 2
},
{
"id": "134f84ca-370c-4d17-8711-728a3402ddc6",
"name": "Flatten",
"type": "n8n-nodes-base.code",
"position": [
1640,
1500
],
"parameters": {
"jsCode": "const weekLabel = $json.week;\n\nif (!items[0].json.rows) {\n return [];\n}\n\nreturn items[0].json.rows.map(row => {\n return {\n json: {\n week: weekLabel,\n page: row.keys[0] || null,\n query: row.keys[1] || null,\n clicks: row.clicks,\n impressions: row.impressions,\n ctr: row.ctr,\n position: row.position\n }\n };\n});\n"
},
"typeVersion": 2
},
{
"id": "e6ca5fa7-2c85-4125-9b3b-11534b89edbe",
"name": "Flatten1",
"type": "n8n-nodes-base.code",
"position": [
1640,
1660
],
"parameters": {
"jsCode": "const weekLabel = $json.week;\n\nif (!items[0].json.rows) {\n return [];\n}\n\nreturn items[0].json.rows.map(row => {\n return {\n json: {\n week: weekLabel,\n page: row.keys[0] || null,\n query: row.keys[1] || null,\n clicks: row.clicks,\n impressions: row.impressions,\n ctr: row.ctr,\n position: row.position\n }\n };\n});\n"
},
"typeVersion": 2
},
{
"id": "4105dc33-c7b0-4675-8e91-dbf0a130d6c7",
"name": "Define Weeks",
"type": "n8n-nodes-base.code",
"notes": "Defines what the weeks are",
"position": [
1020,
1580
],
"parameters": {
"jsCode": "function formatDate(date) {\n return date.toISOString().split('T')[0];\n}\n\nconst today = new Date();\n\n// Prior week (14 to 8 days ago)\nconst priorStart = new Date(today);\npriorStart.setDate(today.getDate() - 14);\n\nconst priorEnd = new Date(today);\npriorEnd.setDate(today.getDate() - 8);\n\n// Last week (7 to 1 days ago)\nconst lastStart = new Date(today);\nlastStart.setDate(today.getDate() - 7);\n\nconst lastEnd = new Date(today);\nlastEnd.setDate(today.getDate() - 1);\n\nreturn [\n {\n json: {\n label: \"priorWeek\",\n startDate: formatDate(priorStart),\n endDate: formatDate(priorEnd)\n }\n },\n {\n json: {\n label: \"lastWeek\",\n startDate: formatDate(lastStart),\n endDate: formatDate(lastEnd)\n }\n }\n];\n"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "3006c533-6749-4661-a19e-0d35b91029b9",
"name": "Merge",
"type": "n8n-nodes-base.merge",
"position": [
1820,
1580
],
"parameters": {},
"typeVersion": 3
},
{
"id": "eb343549-1c73-4ba0-9b97-fc19ea51a72c",
"name": "Merge Weeks",
"type": "n8n-nodes-base.code",
"position": [
1960,
1580
],
"parameters": {
"jsCode": "// Split all items by week\nconst prior = [];\nconst last = [];\n\nfor (const item of items) {\n if (item.json.week === \"priorWeek\") {\n prior.push(item.json);\n } else if (item.json.week === \"lastWeek\") {\n last.push(item.json);\n }\n}\n\n// Index prior week rows by page+query\nconst priorMap = new Map();\nfor (const row of prior) {\n const key = `${row.page}|||${row.query}`;\n priorMap.set(key, row);\n}\n\n// Match last week rows and compute deltas + formatted % changes\nconst results = [];\n\nfor (const row of last) {\n const key = `${row.page}|||${row.query}`;\n const previous = priorMap.get(key);\n if (!previous) continue;\n\n const clicksDelta = row.clicks - previous.clicks;\n const ctrDeltaRaw = row.ctr - previous.ctr;\n const impressionsDelta = row.impressions - previous.impressions;\n const positionDelta = row.position - previous.position;\n\n const percentChangeClicks = previous.clicks !== 0 ? `${((clicksDelta / previous.clicks) * 100).toFixed(1)}%` : null;\n const percentChangeCTR = previous.ctr !== 0 ? `${((ctrDeltaRaw / previous.ctr) * 100).toFixed(1)}%` : null;\n const percentChangeImpressions = previous.impressions !== 0 ? `${((impressionsDelta / previous.impressions) * 100).toFixed(1)}%` : null;\n const percentChangePosition = previous.position !== 0 ? `${((positionDelta / previous.position) * 100).toFixed(1)}%` : null;\n\n const ctrLastWeek = `${(row.ctr * 100).toFixed(1)}%`;\n const ctrPriorWeek = `${(previous.ctr * 100).toFixed(1)}%`;\n const deltaCTR = `${(ctrDeltaRaw * 100).toFixed(1)}%`;\n\n results.push({\n json: {\n page: row.page,\n query: row.query,\n\n deltaClicks: clicksDelta,\n percentChangeClicks,\n clicksLastWeek: row.clicks,\n clicksPriorWeek: previous.clicks,\n\n deltaCTR,\n percentChangeCTR,\n ctrLastWeek,\n ctrPriorWeek,\n\n deltaImpressions: impressionsDelta,\n percentChangeImpressions,\n impressionsLastWeek: row.impressions,\n impressionsPriorWeek: previous.impressions,\n\n deltaPosition: positionDelta,\n percentChangePosition,\n positionLastWeek: row.position,\n positionPriorWeek: previous.position\n }\n });\n}\n\nreturn results;\n"
},
"typeVersion": 2
},
{
"id": "5e8378a4-ecda-435d-ab92-796c008c7d00",
"name": "Tag Brand / Recipes / Nonbrand",
"type": "n8n-nodes-base.code",
"notes": "Create KW segments",
"position": [
2100,
1580
],
"parameters": {
"jsCode": "// Step 1: Map the items and add properties\nconst mappedItems = items.map(item => {\n const query = (item.json.query || \"\").toLowerCase();\n const page = (item.json.page || \"\").toLowerCase();\n\n // Update with your own brand terms\"\n const isBrand = query.includes(\"BRAND TERM 1\", \"BRAND TERM 2\", \"ETC.\");\n // Remove if you don't want to also segment brand terms by page group, or update with your own group, or add more as needed\n const isRecipes = page.includes(\"/recipes\");\n\n // Tag segment type \u2014 this is optional now that isBrand + isRecipes can coexist\n let segment = \"nonbrand\";\n if (isBrand && isRecipes) {\n segment = \"brand+recipes\";\n } else if (isBrand) {\n segment = \"brand\";\n } else if (isRecipes) {\n segment = \"recipes\";\n }\n\n return {\n json: {\n ...item.json,\n isBrand,\n isRecipes,\n segment\n }\n };\n});\n\n// Step 2: Sort by deltaClicks\nconst sortedItems = mappedItems.sort((a, b) => {\n const deltaClicksA = a.json.deltaClicks || 0;\n const deltaClicksB = b.json.deltaClicks || 0;\n\n // Sorting in descending order (highest deltaClicks first)\n return deltaClicksB - deltaClicksA;\n});\n\n// Return the sorted items\nreturn sortedItems;\n"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "137aa32d-2dd0-4f18-9843-20afb836f682",
"name": "Top Movers Filter",
"type": "n8n-nodes-base.code",
"position": [
2680,
1080
],
"parameters": {
"jsCode": "const flagged = [];\n\nfor (const item of items) {\n const delta = item.json.deltaClicks || 0;\n const pct = parseFloat(item.json.percentChangeClicks?.replace('%', '') || 0);\n\n if (Math.abs(delta) >= 200 && Math.abs(pct) >= 30) {\n const direction = delta > 0 ? '\ud83d\udcc8 UP' : '\ud83d\udcc9 DOWN';\n const line = `\u2022 ${direction} ${item.json.query} \u2192 ${delta > 0 ? '+' : ''}${delta} clicks (${item.json.percentChangeClicks})\\nPage: ${item.json.page}`;\n \n flagged.push(line);\n }\n}\n\nif (flagged.length === 0) {\n return []; // No alerts to send\n}\n\nreturn [\n {\n json: {\n text: `*\ud83d\udea8 Big WoW Movers Alert - BRAND:*\\n\\n${flagged.join('\\n\\n')}`\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "68d959f5-c70e-44a7-b74e-ea8e3c0046cc",
"name": "Top 25 Filter",
"type": "n8n-nodes-base.code",
"position": [
2820,
1220
],
"parameters": {
"jsCode": "// Split into up and down movers\nconst up = [];\nconst down = [];\n\nfor (const item of items) {\n const delta = item.json.deltaClicks || 0;\n if (delta > 0) up.push(item);\n else if (delta < 0) down.push(item);\n}\n\n// Sort by absolute deltaClicks descending\nup.sort((a, b) => Math.abs(b.json.deltaClicks) - Math.abs(a.json.deltaClicks));\ndown.sort((a, b) => Math.abs(b.json.deltaClicks) - Math.abs(a.json.deltaClicks));\n\n// Take top 25 from each\nconst topUp = up.slice(0, 25);\nconst topDown = down.slice(0, 25);\n\n// HTML row formatter\nconst formatRow = (item, emoji) => {\n const q = item.json.query;\n const p = item.json.page;\n const delta = item.json.deltaClicks;\n const pct = item.json.percentChangeClicks;\n return `\n <tr>\n <td>${emoji}</td>\n <td>${q}</td>\n <td><a href=\"${p}\">${p}</a></td>\n <td>${delta > 0 ? '+' : ''}${delta}</td>\n <td>${pct}</td>\n </tr>\n `;\n};\n\n// Build table\nconst header = `\n <tr>\n <th>\ud83d\udcca</th>\n <th>Query</th>\n <th>Page</th>\n <th>Delta Clicks</th>\n <th>% Change</th>\n </tr>\n`;\n\nconst bodyRows = [\n ...topUp.map(item => formatRow(item, '\ud83d\udcc8')),\n ...topDown.map(item => formatRow(item, '\ud83d\udcc9'))\n];\n\nconst html = `\n <h2>Top Weekly Movers</h2>\n <p>Sorted by largest absolute click change</p>\n <table border=\"1\" cellpadding=\"4\" cellspacing=\"0\">\n ${header}\n ${bodyRows.join('\\n')}\n </table>\n`;\n\nreturn [\n {\n json: {\n subject: `Top WoW SEO Movers \u2013 BRAND (${new Date().toISOString().split('T')[0]})`,\n body: html\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "95054b7e-7d37-46e4-b4bd-b1a719438cf2",
"name": "Top WoW Movers Alert",
"type": "n8n-nodes-base.slack",
"position": [
2820,
1080
],
"parameters": {
"text": "={{$json.text}}",
"user": {
"__rl": true,
"mode": "id",
"value": ""
},
"select": "user",
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "2fa5b713-2346-4552-98c7-24a6d3997e14",
"name": "Code",
"type": "n8n-nodes-base.code",
"position": [
3240,
1600
],
"parameters": {
"jsCode": "const titledTables = items.map((item, index) => {\n const label = [\n \"Brand KWs\",\n \"Brand+Recipes KWs\",\n \"Recipes KWs\",\n \"Nonbrand KWs\"\n ][index] || `Segment ${index + 1}`;\n\n return `<h1>${label}</h1>\\n${item.json.body || ''}`;\n});\n\nreturn [\n {\n json: {\n subject: `Top Weekly SEO Movers \u2013 ${new Date().toISOString().split('T')[0]}`,\n body: titledTables.join('<br><br>')\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "2273a8d5-df75-46a1-94b6-ee6228e066a2",
"name": "Top WoW Movers Email",
"type": "n8n-nodes-base.gmail",
"position": [
3380,
1600
],
"parameters": {
"message": "={{ $json.body }}",
"options": {},
"subject": "={{ $json.subject }}"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "192d4640-1955-44cf-9f21-cd8292d32d9c",
"name": "Merge4",
"type": "n8n-nodes-base.merge",
"position": [
3100,
1580
],
"parameters": {
"numberInputs": 4
},
"typeVersion": 3
},
{
"id": "f71cf77a-24a9-42a5-bd2c-7c3fdf6974b7",
"name": "Top WoW Movers Alert1",
"type": "n8n-nodes-base.slack",
"position": [
2820,
1380
],
"parameters": {
"text": "={{$json.text}}",
"user": {
"__rl": true,
"mode": "id",
"value": ""
},
"select": "user",
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "84905892-f99e-4934-9377-942d16fb98bc",
"name": "Top 25 Filter1",
"type": "n8n-nodes-base.code",
"position": [
2820,
1520
],
"parameters": {
"jsCode": "// Split into up and down movers\nconst up = [];\nconst down = [];\n\nfor (const item of items) {\n const delta = item.json.deltaClicks || 0;\n if (delta > 0) up.push(item);\n else if (delta < 0) down.push(item);\n}\n\n// Sort by absolute deltaClicks descending\nup.sort((a, b) => Math.abs(b.json.deltaClicks) - Math.abs(a.json.deltaClicks));\ndown.sort((a, b) => Math.abs(b.json.deltaClicks) - Math.abs(a.json.deltaClicks));\n\n// Take top 25 from each\nconst topUp = up.slice(0, 25);\nconst topDown = down.slice(0, 25);\n\n// HTML row formatter\nconst formatRow = (item, emoji) => {\n const q = item.json.query;\n const p = item.json.page;\n const delta = item.json.deltaClicks;\n const pct = item.json.percentChangeClicks;\n return `\n <tr>\n <td>${emoji}</td>\n <td>${q}</td>\n <td><a href=\"${p}\">${p}</a></td>\n <td>${delta > 0 ? '+' : ''}${delta}</td>\n <td>${pct}</td>\n </tr>\n `;\n};\n\n// Build table\nconst header = `\n <tr>\n <th>\ud83d\udcca</th>\n <th>Query</th>\n <th>Page</th>\n <th>Delta Clicks</th>\n <th>% Change</th>\n </tr>\n`;\n\nconst bodyRows = [\n ...topUp.map(item => formatRow(item, '\ud83d\udcc8')),\n ...topDown.map(item => formatRow(item, '\ud83d\udcc9'))\n];\n\nconst html = `\n <h2>Top Weekly Movers</h2>\n <p>Sorted by largest absolute click change</p>\n <table border=\"1\" cellpadding=\"4\" cellspacing=\"0\">\n ${header}\n ${bodyRows.join('\\n')}\n </table>\n`;\n\nreturn [\n {\n json: {\n subject: `Top WoW SEO Movers \u2013 BRAND + RECIPES (${new Date().toISOString().split('T')[0]})`,\n body: html\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "0dc0cca9-85b7-4ddb-9e0b-35e504c24212",
"name": "Top Movers Filter1",
"type": "n8n-nodes-base.code",
"position": [
2680,
1380
],
"parameters": {
"jsCode": "const flagged = [];\n\nfor (const item of items) {\n const delta = item.json.deltaClicks || 0;\n const pct = parseFloat(item.json.percentChangeClicks?.replace('%', '') || 0);\n\n if (Math.abs(delta) >= 200 && Math.abs(pct) >= 30) {\n const direction = delta > 0 ? '\ud83d\udcc8 UP' : '\ud83d\udcc9 DOWN';\n const line = `\u2022 ${direction} ${item.json.query} \u2192 ${delta > 0 ? '+' : ''}${delta} clicks (${item.json.percentChangeClicks})\\nPage: ${item.json.page}`;\n \n flagged.push(line);\n }\n}\n\nif (flagged.length === 0) {\n return []; // No alerts to send\n}\n\nreturn [\n {\n json: {\n text: `*\ud83d\udea8 Big WoW Movers Alert - BRAND + RECIPES:*\\n\\n${flagged.join('\\n\\n')}`\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "285477c6-4b90-4fd4-bb1d-b795f57e2a47",
"name": "Top WoW Movers Alert3",
"type": "n8n-nodes-base.slack",
"position": [
2820,
2120
],
"parameters": {
"text": "={{$json.text}}",
"user": {
"__rl": true,
"mode": "id",
"value": ""
},
"select": "user",
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "645895c3-4c7d-46e8-83d4-59b740cc9879",
"name": "Top WoW Movers Alert2",
"type": "n8n-nodes-base.slack",
"position": [
2820,
1820
],
"parameters": {
"text": "={{$json.text}}",
"user": {
"__rl": true,
"mode": "id",
"value": ""
},
"select": "user",
"otherOptions": {}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "8366e03b-10af-4917-b702-cd776f9c4f57",
"name": "Top 25 Filter3",
"type": "n8n-nodes-base.code",
"position": [
2820,
1980
],
"parameters": {
"jsCode": "// Split into up and down movers\nconst up = [];\nconst down = [];\n\nfor (const item of items) {\n const delta = item.json.deltaClicks || 0;\n if (delta > 0) up.push(item);\n else if (delta < 0) down.push(item);\n}\n\n// Sort by absolute deltaClicks descending\nup.sort((a, b) => Math.abs(b.json.deltaClicks) - Math.abs(a.json.deltaClicks));\ndown.sort((a, b) => Math.abs(b.json.deltaClicks) - Math.abs(a.json.deltaClicks));\n\n// Take top 25 from each\nconst topUp = up.slice(0, 25);\nconst topDown = down.slice(0, 25);\n\n// HTML row formatter\nconst formatRow = (item, emoji) => {\n const q = item.json.query;\n const p = item.json.page;\n const delta = item.json.deltaClicks;\n const pct = item.json.percentChangeClicks;\n return `\n <tr>\n <td>${emoji}</td>\n <td>${q}</td>\n <td><a href=\"${p}\">${p}</a></td>\n <td>${delta > 0 ? '+' : ''}${delta}</td>\n <td>${pct}</td>\n </tr>\n `;\n};\n\n// Build table\nconst header = `\n <tr>\n <th>\ud83d\udcca</th>\n <th>Query</th>\n <th>Page</th>\n <th>Delta Clicks</th>\n <th>% Change</th>\n </tr>\n`;\n\nconst bodyRows = [\n ...topUp.map(item => formatRow(item, '\ud83d\udcc8')),\n ...topDown.map(item => formatRow(item, '\ud83d\udcc9'))\n];\n\nconst html = `\n <h2>Top Weekly Movers</h2>\n <p>Sorted by largest absolute click change</p>\n <table border=\"1\" cellpadding=\"4\" cellspacing=\"0\">\n ${header}\n ${bodyRows.join('\\n')}\n </table>\n`;\n\nreturn [\n {\n json: {\n subject: `Top WoW SEO Movers \u2013 NONBRAND (${new Date().toISOString().split('T')[0]})`,\n body: html\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "57ec0dd1-e52c-4599-a646-b2bb23621391",
"name": "Top Movers Filter3",
"type": "n8n-nodes-base.code",
"position": [
2680,
2120
],
"parameters": {
"jsCode": "const flagged = [];\n\nfor (const item of items) {\n const delta = item.json.deltaClicks || 0;\n const pct = parseFloat(item.json.percentChangeClicks?.replace('%', '') || 0);\n\n if (Math.abs(delta) >= 200 && Math.abs(pct) >= 30) {\n const direction = delta > 0 ? '\ud83d\udcc8 UP' : '\ud83d\udcc9 DOWN';\n const line = `\u2022 ${direction} ${item.json.query} \u2192 ${delta > 0 ? '+' : ''}${delta} clicks (${item.json.percentChangeClicks})\\nPage: ${item.json.page}`;\n \n flagged.push(line);\n }\n}\n\nif (flagged.length === 0) {\n return []; // No alerts to send\n}\n\nreturn [\n {\n json: {\n text: `*\ud83d\udea8 Big WoW Movers Alert - NONBRAND:*\\n\\n${flagged.join('\\n\\n')}`\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "a86b34c7-60b3-4ee9-9431-68530a0e6996",
"name": "Top 25 Filter2",
"type": "n8n-nodes-base.code",
"position": [
2820,
1680
],
"parameters": {
"jsCode": "// Split into up and down movers\nconst up = [];\nconst down = [];\n\nfor (const item of items) {\n const delta = item.json.deltaClicks || 0;\n if (delta > 0) up.push(item);\n else if (delta < 0) down.push(item);\n}\n\n// Sort by absolute deltaClicks descending\nup.sort((a, b) => Math.abs(b.json.deltaClicks) - Math.abs(a.json.deltaClicks));\ndown.sort((a, b) => Math.abs(b.json.deltaClicks) - Math.abs(a.json.deltaClicks));\n\n// Take top 25 from each\nconst topUp = up.slice(0, 25);\nconst topDown = down.slice(0, 25);\n\n// HTML row formatter\nconst formatRow = (item, emoji) => {\n const q = item.json.query;\n const p = item.json.page;\n const delta = item.json.deltaClicks;\n const pct = item.json.percentChangeClicks;\n return `\n <tr>\n <td>${emoji}</td>\n <td>${q}</td>\n <td><a href=\"${p}\">${p}</a></td>\n <td>${delta > 0 ? '+' : ''}${delta}</td>\n <td>${pct}</td>\n </tr>\n `;\n};\n\n// Build table\nconst header = `\n <tr>\n <th>\ud83d\udcca</th>\n <th>Query</th>\n <th>Page</th>\n <th>Delta Clicks</th>\n <th>% Change</th>\n </tr>\n`;\n\nconst bodyRows = [\n ...topUp.map(item => formatRow(item, '\ud83d\udcc8')),\n ...topDown.map(item => formatRow(item, '\ud83d\udcc9'))\n];\n\nconst html = `\n <h2>Top Weekly Movers</h2>\n <p>Sorted by largest absolute click change</p>\n <table border=\"1\" cellpadding=\"4\" cellspacing=\"0\">\n ${header}\n ${bodyRows.join('\\n')}\n </table>\n`;\n\nreturn [\n {\n json: {\n subject: `Top WoW SEO Movers \u2013 RECIPES (${new Date().toISOString().split('T')[0]})`,\n body: html\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "3a540e0b-2d24-4ddc-8142-572624da1008",
"name": "Top Movers Filter2",
"type": "n8n-nodes-base.code",
"position": [
2680,
1820
],
"parameters": {
"jsCode": "const flagged = [];\n\nfor (const item of items) {\n const delta = item.json.deltaClicks || 0;\n const pct = parseFloat(item.json.percentChangeClicks?.replace('%', '') || 0);\n\n if (Math.abs(delta) >= 200 && Math.abs(pct) >= 30) {\n const direction = delta > 0 ? '\ud83d\udcc8 UP' : '\ud83d\udcc9 DOWN';\n const line = `\u2022 ${direction} ${item.json.query} \u2192 ${delta > 0 ? '+' : ''}${delta} clicks (${item.json.percentChangeClicks})\\nPage: ${item.json.page}`;\n \n flagged.push(line);\n }\n}\n\nif (flagged.length === 0) {\n return []; // No alerts to send\n}\n\nreturn [\n {\n json: {\n text: `*\ud83d\udea8 Big WoW Movers Alert - RECIPES:*\\n\\n${flagged.join('\\n\\n')}`\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "f509329a-ffe4-419b-a085-a995376f6490",
"name": "Switch",
"type": "n8n-nodes-base.switch",
"position": [
2420,
1560
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "Brand Flow",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.segment }}",
"rightValue": "brand"
}
]
},
"renameOutput": true
},
{
"outputKey": "Brand+Recipes Flow",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "84a4654e-3d07-4bde-a7cf-aadf9b61301d",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.segment }}",
"rightValue": "brand+recipes"
}
]
},
"renameOutput": true
},
{
"outputKey": "Recipes Flow",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "eee379f6-8678-4f7c-8fb4-68a0be79aa01",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.segment }}",
"rightValue": "recipes"
}
]
},
"renameOutput": true
},
{
"outputKey": "Nonbrand Flow",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "e9624b23-70a7-4e4e-bd4c-72cf716d4fe7",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.segment }}",
"rightValue": "nonbrand"
}
]
},
"renameOutput": true
}
]
},
"options": {}
},
"typeVersion": 3.2
},
{
"id": "4c52027f-871e-4c67-b7b7-ee19e3492660",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
1320,
1380
],
"parameters": {
"width": 180,
"height": 100,
"content": "## Connect to GSC account"
},
"typeVersion": 1
},
{
"id": "6c7fe161-38db-4198-b426-dd0e0c2307ea",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
2780,
940
],
"parameters": {
"width": 200,
"height": 100,
"content": "## Connect to Slack account"
},
"typeVersion": 1
},
{
"id": "b0760a35-69f2-44df-963e-9d11149bf3a4",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
3300,
1440
],
"parameters": {
"height": 120,
"content": "## Connect to Gmail account or update to something else"
},
"typeVersion": 1
},
{
"id": "773bbb5a-51b4-46ba-b03d-eb663d98fe65",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2040,
1460
],
"parameters": {
"width": 200,
"height": 80,
"content": "## Create KW segments here"
},
"typeVersion": 1
},
{
"id": "f8643470-af97-4e57-a915-0bf034a18c97",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
2600,
2300
],
"parameters": {
"height": 140,
"content": "### Current threshold is set to greater than 200 absolute delta clicks (i.e. positive or negative) and 30% absolute change."
},
"typeVersion": 1
},
{
"id": "37bc1268-1dd0-49aa-a53e-f789db9541ed",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
180,
1260
],
"parameters": {
"width": 620,
"height": 760,
"content": "## This workflow tracks week-over-week changes in Google Search Console performance and highlights the top movers across keyword segments like brand, nonbrand, and content categories.\n\nInstead of providing a routine check, it focuses on significant movements by:\n- Sending a Slack alert only if a query crosses a defined movement threshold.\n- Emailing a structured report with the Top 25 increases and Top 25 decreases for clicks, including % changes and linked URLs (top # can be updated to whatever you want)\n\nIt\u2019s designed to surface the most important shifts, helping SEO teams catch big wins, losses, or anomalies early.\n\n### How it works\n1. Runs weekly (e.g. every Monday) to compare last week\u2019s GSC data to the week prior.\n2. Segments traffic based on query and page (e.g. brand terms, category page URLs, etc.).\n3. Calculates delta and % change for clicks, CTR, impressions, and position.\n4. Filters and flags top movers with large shifts (default: \u00b1200 clicks and \u00b130%).\n5. Sends Slack alerts only if meaningful changes are detected.\n6. Emails a full HTML table report showing the Top 25 up/down queries per segment.\n\n### Setup steps\n- Requires a connected Google Search Console account.\n- Slack alert is included by default (can be replaced with email, webhook, or other tools).\n- Customize your brand terms and URL filters to match your segments (e.g. recipes, blog, category pages).\n- Typical setup time: 15\u201325 minutes depending on the number of segments and filters you want.\n\n*Note: \u201cRecipes\u201d is used in the example to show how to segment by content type. You can update this to reflect your own site\u2019s structure.*"
},
"typeVersion": 1
},
{
"id": "e3db3fd9-6388-437b-886d-d7cfc0243b8c",
"name": "Aggregate by KW",
"type": "n8n-nodes-base.code",
"position": [
2260,
1580
],
"parameters": {
"jsCode": "// Group by query (case-insensitive)\nconst queryMap = {};\n\nfor (const item of items) {\n const { query, clicksLastWeek = 0 } = item.json;\n const normalizedQuery = (query || \"\").toLowerCase();\n\n if (!queryMap[normalizedQuery]) {\n queryMap[normalizedQuery] = item;\n } else {\n // Replace if this page has more clicks\n if ((item.json.clicksLastWeek || 0) > (queryMap[normalizedQuery].json.clicksLastWeek || 0)) {\n queryMap[normalizedQuery] = item;\n }\n }\n}\n\n// Return only one result per query (the page with highest clicks)\nreturn Object.values(queryMap);\n"
},
"typeVersion": 2
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "8b67b4bb-ec67-41bb-a182-4a7a2757c3ba",
"connections": {
"If": {
"main": [
[
{
"node": "priorWeek",
"type": "main",
"index": 0
}
],
[
{
"node": "lastWeek",
"type": "main",
"index": 0
}
]
]
},
"Code": {
"main": [
[
{
"node": "Top WoW Movers Email",
"type": "main",
"index": 0
}
]
]
},
"Merge": {
"main": [
[
{
"node": "Merge Weeks",
"type": "main",
"index": 0
}
]
]
},
"label": {
"main": [
[
{
"node": "Flatten",
"type": "main",
"index": 0
}
]
]
},
"Merge4": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Switch": {
"main": [
[
{
"node": "Top Movers Filter",
"type": "main",
"index": 0
},
{
"node": "Top 25 Filter",
"type": "main",
"index": 0
}
],
[
{
"node": "Top 25 Filter1",
"type": "main",
"index": 0
},
{
"node": "Top Movers Filter1",
"type": "main",
"index": 0
}
],
[
{
"node": "Top 25 Filter2",
"type": "main",
"index": 0
},
{
"node": "Top Movers Filter2",
"type": "main",
"index": 0
}
],
[
{
"node": "Top 25 Filter3",
"type": "main",
"index": 0
},
{
"node": "Top Movers Filter3",
"type": "main",
"index": 0
}
]
]
},
"label1": {
"main": [
[
{
"node": "Flatten1",
"type": "main",
"index": 0
}
]
]
},
"Flatten": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"Flatten1": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"lastWeek": {
"main": [
[
{
"node": "label1",
"type": "main",
"index": 0
}
]
]
},
"priorWeek": {
"main": [
[
{
"node": "label",
"type": "main",
"index": 0
}
]
]
},
"Merge Weeks": {
"main": [
[
{
"node": "Tag Brand / Recipes / Nonbrand",
"type": "main",
"index": 0
}
]
]
},
"Define Weeks": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
},
"Top 25 Filter": {
"main": [
[
{
"node": "Merge4",
"type": "main",
"index": 0
}
]
]
},
"Top 25 Filter1": {
"main": [
[
{
"node": "Merge4",
"type": "main",
"index": 1
}
]
]
},
"Top 25 Filter2": {
"main": [
[
{
"node": "Merge4",
"type": "main",
"index": 2
}
]
]
},
"Top 25 Filter3": {
"main": [
[
{
"node": "Merge4",
"type": "main",
"index": 3
}
]
]
},
"Aggregate by KW": {
"main": [
[
{
"node": "Switch",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Define Weeks",
"type": "main",
"index": 0
}
]
]
},
"Top Movers Filter": {
"main": [
[
{
"node": "Top WoW Movers Alert",
"type": "main",
"index": 0
}
]
]
},
"Top Movers Filter1": {
"main": [
[
{
"node": "Top WoW Movers Alert1",
"type": "main",
"index": 0
}
]
]
},
"Top Movers Filter2": {
"main": [
[
{
"node": "Top WoW Movers Alert2",
"type": "main",
"index": 0
}
]
]
},
"Top Movers Filter3": {
"main": [
[
{
"node": "Top WoW Movers Alert3",
"type": "main",
"index": 0
}
]
]
},
"Top WoW Movers Alert": {
"main": [
[]
]
},
"Tag Brand / Recipes / Nonbrand": {
"main": [
[
{
"node": "Aggregate by KW",
"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.
gmailOAuth2googleOAuth2ApislackApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Instead of providing a routine check, it focuses on significant movements by: Sending a Slack alert only if a query crosses a defined movement threshold. Emailing a structured report with the Top 25 increases and Top 25 decreases for clicks, including % changes and linked URLs
Source: https://n8n.io/workflows/6006/ — 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 automates the complete end-to-end processing of daily revenue transactions for finance and accounting teams. It systematically retrieves, validates, and standardizes transaction data fro
E-commerce store owners, product managers, marketplace sellers, and pricing analysts who want to automatically track competitor pricing and get actionable alerts when their products are overpriced or
This template is ideal for developers, agencies, hosting providers, and website owners who need real-time alerts when a website goes down. It helps teams react quickly to downtime by sending multi-cha
Schedule Slack. Uses scheduleTrigger, googleSheets, slack, gmail. Scheduled trigger; 15 nodes.
This n8n workflow demonstrates how to build a simple uptime monitoring service using scheduled triggers.