This workflow corresponds to n8n.io template #9071 — we link there as the canonical source.
This workflow follows the Google Sheets → 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": "3qFkempg015uG0dB",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Find internal linking opportunities using SerAPI and Google Sheets",
"tags": [],
"nodes": [
{
"id": "8c2aa4ad-94d0-4c52-b6bb-bee296a4420b",
"name": "Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
480,
144
],
"parameters": {
"options": {},
"batchSize": 5
},
"typeVersion": 3
},
{
"id": "7fccd9bf-8f71-4adf-97e4-2eff330d757f",
"name": "Get search results using SerpAPI",
"type": "n8n-nodes-base.httpRequest",
"position": [
832,
160
],
"parameters": {
"url": "https://serpapi.com/search",
"options": {},
"sendQuery": true,
"authentication": "predefinedCredentialType",
"queryParameters": {
"parameters": [
{
"name": "q",
"value": "={{ $json.Keyword }} site:{{ $json.Domain }} -inurl:{{ $json.URL }}"
}
]
},
"nodeCredentialType": "serpApi"
},
"credentials": {
"serpApi": {
"name": "<your credential>"
},
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "420bdbbe-a304-4bfe-8cd8-54ab467a6d71",
"name": "Extract links from JSON",
"type": "n8n-nodes-base.set",
"position": [
1184,
160
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "eae05e00-7188-4cca-994f-0cdf091e5618",
"name": "URL",
"type": "string",
"value": "={{ $('Loop Over Items').item.json.URL }}"
},
{
"id": "1e7647fb-a137-4971-8f8c-5f0709d0f92f",
"name": "organic_results",
"type": "array",
"value": "={{ $json.organic_results.map(item => item.link) }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "44ca48a2-fae4-4de4-a5d7-e4d16960b914",
"name": "Add internal URL to Google Sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
1536,
160
],
"parameters": {
"columns": {
"value": {
"URL": "={{ $json.URL }}",
"Internal link 1": "={{ $json.organic_results[0] ? $json.organic_results[0] : \"N/A\"}}",
"Internal link 2": "={{ $json.organic_results[1] ? $json.organic_results[1] : \"N/A\"}}",
"Internal link 3": "={{ $json.organic_results[2] ? $json.organic_results[2] : \"N/A\"}}",
"Internal link 4": "={{ $json.organic_results[3] ? $json.organic_results[3] : \"N/A\"}}",
"Internal link 5": "={{ $json.organic_results[4] ? $json.organic_results[4] : \"N/A\"}}",
"Internal link 6": "={{ $json.organic_results[5] ? $json.organic_results[5] : \"N/A\"}}",
"Internal link 7": "={{ $json.organic_results[6] ? $json.organic_results[6] : \"N/A\"}}",
"Internal link 8": "={{ $json.organic_results[7] ? $json.organic_results[7] : \"N/A\"}}",
"Internal link 9": "={{ $json.organic_results[8] ? $json.organic_results[8] : \"N/A\"}}",
"Internal link 10": "={{ $json.organic_results[9] ? $json.organic_results[9] : \"N/A\"}}"
},
"schema": [
{
"id": "Domain",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Domain",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "URL",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Keyword",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Keyword",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Internal link 1",
"type": "string",
"display": true,
"required": false,
"displayName": "Internal link 1",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Internal link 2",
"type": "string",
"display": true,
"required": false,
"displayName": "Internal link 2",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Internal link 3",
"type": "string",
"display": true,
"required": false,
"displayName": "Internal link 3",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Internal link 4",
"type": "string",
"display": true,
"required": false,
"displayName": "Internal link 4",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Internal link 5",
"type": "string",
"display": true,
"required": false,
"displayName": "Internal link 5",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Internal link 6",
"type": "string",
"display": true,
"required": false,
"displayName": "Internal link 6",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Internal link 7",
"type": "string",
"display": true,
"required": false,
"displayName": "Internal link 7",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Internal link 8",
"type": "string",
"display": true,
"required": false,
"displayName": "Internal link 8",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Internal link 9",
"type": "string",
"display": true,
"required": false,
"displayName": "Internal link 9",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Internal link 10",
"type": "string",
"display": true,
"required": false,
"displayName": "Internal link 10",
"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": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U0whmy1d7wPoWTNTa1u68QrLTE1NqYgyyFXxARWo_1M/edit#gid=0",
"cachedResultName": "Internal links"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1U0whmy1d7wPoWTNTa1u68QrLTE1NqYgyyFXxARWo_1M",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U0whmy1d7wPoWTNTa1u68QrLTE1NqYgyyFXxARWo_1M/edit?usp=drivesdk",
"cachedResultName": "Find internal links using SerpAPI - N8N demo"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "172e75f7-9698-4c87-819e-edf94ea20612",
"name": "Get URLs and keywords",
"type": "n8n-nodes-base.googleSheets",
"position": [
128,
144
],
"parameters": {
"options": {},
"filtersUI": {
"values": [
{
"lookupColumn": "Internal link 1"
}
]
},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U0whmy1d7wPoWTNTa1u68QrLTE1NqYgyyFXxARWo_1M/edit#gid=0",
"cachedResultName": "Internal links"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1U0whmy1d7wPoWTNTa1u68QrLTE1NqYgyyFXxARWo_1M",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U0whmy1d7wPoWTNTa1u68QrLTE1NqYgyyFXxARWo_1M/edit?usp=drivesdk",
"cachedResultName": "Find internal links using SerpAPI - N8N demo"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "1d1a37d8-8b2b-4399-9abe-13dff9cc1ad2",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
16,
-320
],
"parameters": {
"color": 7,
"width": 320,
"height": 704,
"content": "## Get URLs and keywords\nIn this node, we're getting the different URLs that we want suggestions for, and the keywords/topics those pages are targeting.\n\nWe're filtering out everything that already has a value in the `internal link 1` column so we don't override rows we already processed in a previous run\n"
},
"typeVersion": 1
},
{
"id": "2f549918-a4ca-4a99-b60a-94b903c7f994",
"name": "Manual trigger",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-112,
144
],
"parameters": {},
"typeVersion": 1
},
{
"id": "36b666a5-613b-43bc-a940-b5bb546bdbfa",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
368,
-320
],
"parameters": {
"color": 7,
"width": 320,
"height": 704,
"content": "## Loop over URLs\nLoop over each URL to get the results. Here we're doing it in batches of 5\n\n:warning: Note :warning:\nDon't set the batch size too high or you'll start hitting rate limits for some of the APIs, especially Google Sheets. \n\nIf you have a small number of URLs to process you can leave it at five. If you have a larger set of URLs consider setting the batch size lower, or adding a wait node at the end.\n\n"
},
"typeVersion": 1
},
{
"id": "3dc9bb5d-03c4-423b-96d0-120badad6810",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
720,
-320
],
"parameters": {
"color": 7,
"width": 320,
"height": 704,
"content": "## Get search results using SerpAPI\nGet the search results from SerpAPI.\n\nWe're searching for the target keyword, while doing a `site:<domain>` search so we only get results from our site.\n\nWe use `-inurl:<url>` to exclude the URL we are currently working with. This gives us results similar to the URL we have that we can building internal links from.\n\n:warning: Note :warning:\nIf you have to process a large set of URLs it's worth [setting up a programmable search engine](https://drlee.io/build-your-own-google-create-a-custom-search-engine-with-trusted-sources-c1c113e845cc) instead of using SerAPI\n\n"
},
"typeVersion": 1
},
{
"id": "75af678f-5af2-4aa7-8f00-d6fc84ee18e6",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
1072,
-320
],
"parameters": {
"color": 7,
"width": 320,
"height": 704,
"content": "## Extract organic links from JSON\nSerpAPI gives us a bunch of data we don't need for this use case so we just filter for the organic results. specifically, we filter the URLs it gives us\n\n"
},
"typeVersion": 1
},
{
"id": "72d62473-fab0-4db5-92f8-4cac1ab9acb0",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1424,
-320
],
"parameters": {
"color": 7,
"width": 320,
"height": 704,
"content": "## Update Google Sheet\nHere we're adding the URLs to the different column in our sheet.\n\nNote that we're using an inline `if` statement so, if there's no URL present, it just adds 'N/A' to our Google Sheet. Not all URLs will have 10 internal link opportunities\n\n"
},
"typeVersion": 1
},
{
"id": "e229c5a9-44c9-44c5-af81-ca2859283831",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-592,
-320
],
"parameters": {
"color": 7,
"width": 400,
"height": 704,
"content": "## Overview\n\nUse this workflow to spot internal linking ideas on your site and improve your search performance. It takes your target URLs and keywords, finds related pages, and suggests where to add links. Strong internal linking helps search engines understand your site.\n\n## How it works\n- You provide a list of target URLs and the keywords you want to rank for\n- The workflow uses the SERP API to search your site for related pages, skipping the target URL\n- It filters the results and pulls relevant URLs\n- It writes the suggestions to a Google Sheet, and adds \u201cN/A\u201d if no good matches are found\n\n## Setup steps\n1. Turn on the Google Sheets API and create a sheet with your domain, target URLs, and keywords\n2. Create a SERP API account and get an API key\n3. Optional: Set up a Google Programmable Search Engine if you prefer not to use the SERP API\n4. Add your SERP API key and Google Sheets credentials to n8n.\n5. Run the workflow to generate internal link suggestions in your Google Sheet"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "5d62c2bc-3ff0-4b0c-8af5-76d8fe285226",
"connections": {
"Manual trigger": {
"main": [
[
{
"node": "Get URLs and keywords",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[],
[
{
"node": "Get search results using SerpAPI",
"type": "main",
"index": 0
}
]
]
},
"Get URLs and keywords": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Extract links from JSON": {
"main": [
[
{
"node": "Add internal URL to Google Sheet",
"type": "main",
"index": 0
}
]
]
},
"Add internal URL to Google Sheet": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Get search results using SerpAPI": {
"main": [
[
{
"node": "Extract links from JSON",
"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.
googleSheetsOAuth2ApihttpHeaderAuthserpApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Use this workflow to spot internal linking ideas on your site and improve your search performance. It takes your target URLs and keywords, finds related pages, and suggests where to add links. Strong internal linking helps search engines understand your site. You provide a list…
Source: https://n8n.io/workflows/9071/ — 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 template is ideal for solo store owners, eCommerce marketers, automation beginners, or anyone using Shopify and Gmail who wants to recover lost revenue without coding.
PCN. Uses googleSheets, httpRequest, @n-octo-n/n8n-nodes-json-database, itemLists. Event-driven trigger; 60 nodes.
The workflow automates the process of gathering extensive keyword data for a "Main Keyword." It starts by reading initial parameters from a Google Sheets template, creates a new dedicated Google Sheet
🔥 March Sale – n8n Community Members Get ideoGener8r for Just $27! (Reg. $47) Use Coupon Code: (Valid until 3/31/2025 for n8n community members)
📄 Documentation: Notion Guide