Awesome n8n templates: 280+ free workflows, organised by use case
A curated, continuously updated GitHub collection of free n8n templates spanning marketing, sales, content and operations automation.
The Automation Wing
A weekly n8n workflow that pulls the last 28 days of Search Console clicks against the 28 before, writes the biggest query winners and losers to a Google Sheet, and emails the top movers.
Search Console will happily show you last month's top queries, but it is coy about change. The question that actually moves work forward is not "what ranks?" but "what moved?": which queries picked up clicks, which quietly fell away, and by how much. Answering it by hand means two exports and a spreadsheet every week, which is precisely the sort of chore that stops getting done by February.
This workflow does the two exports and the arithmetic for you. Once a week it asks the Search Console API for query clicks over the last 28 days and over the 28 days before that, works out the difference per query, appends the winners and losers to a Google Sheet so you keep a history, and emails you the top movers. It only reads Search Console and appends to a sheet; it changes nothing about your site or your search presence.
https://example.com/) or the sc-domain:example.com form for a Domain property. The API is fussy about this.webmasters.readonly scope. You attach it after import; nothing sensitive travels inside the file.The two API calls hit the Search Console searchanalytics/query endpoint:
POST https://www.googleapis.com/webmasters/v3/sites/{SITE_URL}/searchAnalytics/query
with a JSON body of startDate, endDate, dimensions: ["query"] and a rowLimit. Import the JSON below, then set your dates, site URL, sheet and credentials.
{
"name": "Search Console weekly movers",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "weeks",
"triggerAtDay": [1],
"triggerAtHour": 8
}
]
}
},
"name": "Every Monday",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [220, 300]
},
{
"parameters": {
"method": "POST",
"url": "https://www.googleapis.com/webmasters/v3/sites/https%3A%2F%2Fexample.com%2F/searchAnalytics/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"startDate\": \"{{ $now.minus({ days: 28 }).toFormat('yyyy-MM-dd') }}\",\n \"endDate\": \"{{ $now.minus({ days: 1 }).toFormat('yyyy-MM-dd') }}\",\n \"dimensions\": [\"query\"],\n \"rowLimit\": 1000\n}",
"options": {}
},
"name": "Last 28 days",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [460, 300]
},
{
"parameters": {
"method": "POST",
"url": "https://www.googleapis.com/webmasters/v3/sites/https%3A%2F%2Fexample.com%2F/searchAnalytics/query",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"startDate\": \"{{ $now.minus({ days: 56 }).toFormat('yyyy-MM-dd') }}\",\n \"endDate\": \"{{ $now.minus({ days: 29 }).toFormat('yyyy-MM-dd') }}\",\n \"dimensions\": [\"query\"],\n \"rowLimit\": 1000\n}",
"options": {}
},
"name": "Previous 28 days",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [700, 300]
},
{
"parameters": {
"jsCode": "// Both API responses carry a `rows` array of { keys: [query], clicks, ... }.\nconst current = $('Last 28 days').first().json.rows || [];\nconst previous = $('Previous 28 days').first().json.rows || [];\n\nconst prevByQuery = {};\nfor (const r of previous) prevByQuery[r.keys[0]] = r.clicks;\n\nconst rows = [];\nfor (const r of current) {\n const query = r.keys[0];\n const now = r.clicks;\n const before = prevByQuery[query] || 0;\n rows.push({ query, clicksNow: now, clicksBefore: before, delta: now - before });\n}\nfor (const [query, before] of Object.entries(prevByQuery)) {\n if (!current.find(r => r.keys[0] === query)) {\n rows.push({ query, clicksNow: 0, clicksBefore: before, delta: -before });\n }\n}\n\nrows.sort((a, b) => b.delta - a.delta);\nconst movers = [...rows.slice(0, 10), ...rows.slice(-10)];\nconst runDate = $now.toFormat('yyyy-MM-dd');\n\nreturn movers.map(m => ({ json: { runDate, ...m } }));"
},
"name": "Compare clicks by query",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [940, 300]
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "REPLACE-ME-SHEET-ID",
"mode": "id"
},
"sheetName": {
"__rl": true,
"value": "Movers",
"mode": "name"
},
"columns": {
"mappingMode": "autoMapInputData",
"value": {}
},
"options": {}
},
"name": "Append to Google Sheets",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [1180, 300]
},
{
"parameters": {
"fromEmail": "reports@example.com",
"toEmail": "you@example.com",
"subject": "Search Console: top query movers this week",
"emailFormat": "text",
"text": "={{ $items().map(i => `${i.json.delta >= 0 ? '+' : ''}${i.json.delta} ${i.json.query}`).join('\\n') }}",
"options": {}
},
"name": "Email the top movers",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"executeOnce": true,
"position": [1420, 300]
}
],
"connections": {
"Every Monday": {
"main": [[{ "node": "Last 28 days", "type": "main", "index": 0 }]]
},
"Last 28 days": {
"main": [[{ "node": "Previous 28 days", "type": "main", "index": 0 }]]
},
"Previous 28 days": {
"main": [[{ "node": "Compare clicks by query", "type": "main", "index": 0 }]]
},
"Compare clicks by query": {
"main": [[{ "node": "Append to Google Sheets", "type": "main", "index": 0 }]]
},
"Append to Google Sheets": {
"main": [[{ "node": "Email the top movers", "type": "main", "index": 0 }]]
}
}
}
https%3A%2F%2Fexample.com%2F); a Domain property is sc-domain%3Aexample.com.The email lists the ten biggest risers and the ten biggest fallers by clicks, signed so a plus is a gain. The sheet keeps every week's list, so over a couple of months you can see whether a fall was a blip or a trend. Treat clicks, not position, as the headline here: position averages hide as much as they show, whereas a query that lost clicks lost real visits.
Two limitations worth stating plainly. Search Console data settles over two to three days, so the workflow reads yesterday back, not today, and a very recent week is still firming up. And the API anonymises low-volume queries, so the long tail is under-counted on both sides of the comparison; the movers you see are the ones with enough volume to be reported, which is usually the ones worth acting on anyway.
More Like This
A curated, continuously updated GitHub collection of free n8n templates spanning marketing, sales, content and operations automation.
An importable n8n flow that watches your sitemap, spots new or changed URLs and pings IndexNow so Bing and Yandex hear about a page the moment it ships.
An importable n8n flow that reads Meta account insights daily, compares yesterday against the trailing week and posts to Slack when spend swings beyond your threshold.