This workflow corresponds to n8n.io template #4310 — we link there as the canonical source.
This workflow follows the Gmail → 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": "65c5da22-3579-48d7-a37d-899c956a4b3a",
"name": "When clicking \u2018Test workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-1600,
575
],
"parameters": {},
"typeVersion": 1
},
{
"id": "8279787b-d056-4a46-aca3-2888416f46cf",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-1600,
240
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 24
}
]
}
},
"typeVersion": 1.2
},
{
"id": "5d17fa71-520a-45bf-82b4-ebb5025085c5",
"name": "Making Email Template",
"type": "n8n-nodes-base.code",
"position": [
-720,
40
],
"parameters": {
"jsCode": "const simplifiedResults = items.map(item => {\n return {\n json: {\n Keyword: item.json.Keyword,\n rank: item.json.rank\n }\n };\n});\n\n// Start the HTML table\nlet emailContent = \"<table border='1' cellpadding='5' cellspacing='0' style='border-collapse: collapse;'>\";\n\n// Add table header\nemailContent += \"<tr><th>Keyword</th><th>Rank</th></tr>\";\n\n// Add table rows for each result\nsimplifiedResults.forEach(result => {\n emailContent += `<tr><td>${result.json.Keyword}</td><td>${result.json.rank}</td></tr>`;\n});\n\n// Close the table tag\nemailContent += \"</table>\";\n\n// Return the email content with the table\nreturn [\n {\n json: {\n emailBody: emailContent\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "f689b1b7-553f-4a88-ae8f-94de4bd6701a",
"name": "Reading Keywords",
"type": "n8n-nodes-base.googleSheets",
"position": [
-1380,
240
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1441108147,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1fvWyTep-YOUR_AWS_SECRET_KEY_HERE#gid=1441108147",
"cachedResultName": "Keyword"
},
"documentId": "={{ $credentials.googleSheetDocId }}"
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.5
},
{
"id": "3fb77ecb-4f3c-4e87-9ffb-70d5e38bb3ff",
"name": "Transforming Keywords",
"type": "n8n-nodes-base.code",
"position": [
-1160,
240
],
"parameters": {
"jsCode": "// Get all rows from input\nconst items = $input.all();\n\n// Transform each row's 'Keyword' field\nreturn items.map(item => {\n const keyword = item.json.Keyword || '';\n const transformedKeyword = keyword.replaceAll(' ', '+');\n\n return {\n json: {\n ...item.json, // keep original data\n transformedKeyword\n }\n };\n});\n"
},
"typeVersion": 2
},
{
"id": "fe63d679-a349-4ce2-8d0b-9122da534aa8",
"name": "Loop over Keywords",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-940,
240
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "2ad0d400-d52b-4401-8f65-c79d7b329c32",
"name": "Getting Ranks",
"type": "n8n-nodes-base.httpRequest",
"position": [
-720,
240
],
"parameters": {
"url": "https://api.brightdata.com/request",
"method": "POST",
"options": {},
"jsonBody": "={\"zone\": \"serp_n8n\",\"url\": \"https://www.google.com/search?q={{ $json.transformedKeyword }}&gl=US\", \n \"format\": \"raw\"} ",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer {{$credentials.brightDataApiKey}}"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "99acbc30-b1d8-4c16-819a-20b4a186a56d",
"name": "Rank Finder",
"type": "n8n-nodes-base.code",
"position": [
-500,
240
],
"parameters": {
"jsCode": "const html = $input.first().json.data;\nconst targetDomain = $('Reading Keywords').first().json.Domain; \n\n\nconst regex = /<a[^>]+href=\"(http[^\"]+)\"[^>]*>/g;\nlet match;\nlet links = [];\nwhile ((match = regex.exec(html)) !== null) {\n const url = match[1];\n if (\n url.startsWith(\"http\") &&\n !url.includes(\"google.com\") &&\n !url.includes(\"/search?\") &&\n !url.includes(\"webcache\")\n ) {\n links.push(url);\n }\n}\n\n// Deduplicate and trim links\nlinks = [...new Set(links.map(link => link.trim()))];\n\n// Try to find the rank (position) of your target domain\nlet rank = null;\nlet foundUrl = null;\nfor (let i = 0; i < links.length; i++) {\n if (links[i].includes(targetDomain)) {\n rank = i + 1; // 1-based position\n foundUrl = links[i];\n break;\n }\n}\n\n// Get current date and time\nconst now = new Date();\nconst dateTime = now.toLocaleString(); // e.g., \"5/19/2025, 2:30:00 PM\"\n\n// Output result\nreturn [\n {\n json: {\n row: $('Loop over Keywords').first().json.row_number,\n rank: rank || \"Not Ranked\",\n url: foundUrl || \"N/A\",\n totalResultsChecked: links.length,\n found: !!foundUrl,\n checkedAt: dateTime\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "eec8fa1e-c0c7-48a5-b7e4-9e5d9be3a52f",
"name": "Post Rank Results",
"type": "n8n-nodes-base.googleSheets",
"position": [
-280,
315
],
"parameters": {
"columns": {
"value": {
"url": "={{ $json.url }}",
"rank": "={{ $json.rank }}",
"found": "={{ $json.found }}",
"Domain": "={{ $('Reading Keywords').item.json.Domain }}",
"Keyword": "={{ $('Reading Keywords').item.json.Keyword }}",
"checkedAt": "={{ $json.checkedAt }}",
"totalResultsChecked": "={{ $json.totalResultsChecked }}"
},
"schema": [
{
"id": "Keyword",
"type": "string",
"display": true,
"required": false,
"displayName": "Keyword",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Domain",
"type": "string",
"display": true,
"required": false,
"displayName": "Domain",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "rank",
"type": "string",
"display": true,
"required": false,
"displayName": "rank",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "url",
"type": "string",
"display": true,
"required": false,
"displayName": "url",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "totalResultsChecked",
"type": "string",
"display": true,
"required": false,
"displayName": "totalResultsChecked",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "found",
"type": "string",
"display": true,
"required": false,
"displayName": "found",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "checkedAt",
"type": "string",
"display": true,
"required": false,
"displayName": "checkedAt",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "Results"
},
"documentId": "={{ $credentials.googleSheetDocId }}"
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.5,
"alwaysOutputData": true
},
{
"id": "86b5345d-6a6c-4c5b-9ca2-312ab627f69e",
"name": "Sending Email Message",
"type": "n8n-nodes-base.gmail",
"position": [
-500,
40
],
"parameters": {
"sendTo": "={{ $credentials.recipientEmail }}",
"message": "={{ $json.emailBody }}",
"options": {},
"subject": "Ranked"
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 2.1
},
{
"id": "2aa430ee-b967-461e-9b20-97d34a1e5775",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2480,
-360
],
"parameters": {
"width": 780,
"height": 1740,
"content": "# n8n Workflow Explanation & Setup Guide\n\n## Workflow Overview\n\nThis n8n workflow automates keyword rank tracking on Google Search and emails the results. It works as follows:\n\n1. **Trigger** \n - Manually by clicking \u2018Test workflow\u2019 or automatically every 24 hours.\n\n2. **Read Keywords from Google Sheets** \n - Fetch keywords and target domains from a specified Google Sheets document.\n\n3. **Transform Keywords** \n - Format keywords for URL encoding (replace spaces with `+`).\n\n4. **Batch Processing** \n - Process each keyword individually.\n\n5. **Get Google Search Results** \n - Use BrightData API to fetch raw Google search HTML for each keyword.\n\n6. **Parse and Find Rank** \n - Extract URLs from search results and identify the rank of the target domain.\n\n7. **Save Results to Google Sheets** \n - Append rank results to a \"Results\" sheet.\n\n8. **Prepare Email Template** \n - Format results into an HTML table.\n\n9. **Send Email** \n - Email the results table to a specified recipient.\n\n---\n\n## Setup Instructions\n\n### 1. Google Sheets Setup\n\n- Create a Google Sheet with:\n - A sheet containing `Keyword` and `Domain` columns.\n - A sheet named \"Results\" for output.\n- Update the **document ID** and **sheet names** in:\n - **\"Reading Keywords\"** node\n - **\"Post Rank Results\"** node\n\n### 2. BrightData API Setup\n\n- Sign up for BrightData or a similar data provider.\n- Obtain your API token.\n- Replace the **Authorization Bearer YOUR_TOKEN_HERE** in the **\"Getting Ranks\"** HTTP Request node.\n\n### 3. Gmail Email Setup\n\n- Set up Gmail OAuth2 credentials in n8n.\n- Ensure Gmail API access is enabled.\n- Change the recipient email address in the **\"Sending Email Message\"** node's `sendTo` field.\n\n### 4. Change Google Search Location\n\n- In the **\"Getting Ranks\"** node, the location is set via the URL parameter `gl=US`.\n- To change location, replace `US` with the desired country code:\n\n| Country | Code |\n| ------------- | -----|\n| United States | US |\n| United Kingdom | GB |\n| Canada | CA |\n| Australia | AU |\n\nExample (for UK):\n\n```json\n\"url\": \"https://www.google.com/search?q={{ $json.transformedKeyword }}&gl=GB\"\n"
},
"typeVersion": 1
}
],
"connections": {
"Rank Finder": {
"main": [
[
{
"node": "Post Rank Results",
"type": "main",
"index": 0
}
]
]
},
"Getting Ranks": {
"main": [
[
{
"node": "Rank Finder",
"type": "main",
"index": 0
}
]
]
},
"Reading Keywords": {
"main": [
[
{
"node": "Transforming Keywords",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Reading Keywords",
"type": "main",
"index": 0
}
]
]
},
"Post Rank Results": {
"main": [
[
{
"node": "Loop over Keywords",
"type": "main",
"index": 0
}
]
]
},
"Loop over Keywords": {
"main": [
[
{
"node": "Making Email Template",
"type": "main",
"index": 0
}
],
[
{
"node": "Getting Ranks",
"type": "main",
"index": 0
}
]
]
},
"Making Email Template": {
"main": [
[
{
"node": "Sending Email Message",
"type": "main",
"index": 0
}
]
]
},
"Sending Email Message": {
"main": [
[]
]
},
"Transforming Keywords": {
"main": [
[
{
"node": "Loop over Keywords",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Test workflow\u2019": {
"main": [
[
{
"node": "Reading Keywords",
"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.
gmailOAuth2googleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automates daily or manual keyword rank tracking on Google Search for your target domain. Results are logged in Google Sheets and sent via email using Bright Data's SERP API.
Source: https://n8n.io/workflows/4310/ — 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.
Splitout Code. Uses manualTrigger, httpRequest, stickyNote, splitOut. Event-driven trigger; 46 nodes.
Automate CSV imports into HubSpot without the mess. Powered by n8n. Supercharged by Pollup AI.
AICARE Email Blast System. Uses googleDrive, httpRequest, googleSheets, gmail. Event-driven trigger; 39 nodes.
Automatically processes new orders added to Google Sheets. Small orders are approved instantly; large orders trigger an HTML email with one-click Approve / Reject links — each handled by an independen
Submit any YouTube, Vimeo, or Zoom webinar URL using a simple form and the workflow handles everything from there. It runs a two-phase pipeline: first identifying the top viral moments in your video w