This workflow follows the Discord → 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 →
{
"name": "wf_maps_serp_backup_data",
"nodes": [
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "wRCvCmRGcILIoLjy",
"mode": "list",
"cachedResultName": "Underfoot \u2014 wf_error_notifications"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"json": "={{ { ...$input.all() } }}",
"callingWorkflow": "={ name: {{$workflow.name}}, id: {{$execution.id}} }"
},
"matchingColumns": [
"json"
],
"schema": [
{
"id": "json",
"displayName": "json",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "object",
"removed": false
},
{
"id": "callingWorkflow",
"displayName": "callingWorkflow",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "object",
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {
"waitForSubWorkflow": false
}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.2,
"position": [
1632,
-448
],
"id": "60dbc180-14f5-42cd-94ef-393f688a4cbe",
"name": "call_error_notifier",
"retryOnFail": false
},
{
"parameters": {
"method": "POST",
"url": "https://api.brightdata.com/request",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "brightdataApi",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"zone\": \"serp_api1\",\n \"url\": \"{{$json.url}}\",\n \"format\": \"raw\",\n \"headers\": {\n \"x-unblock-data-format\": \"parsed_light\"\n }\n}",
"options": {
"timeout": 10000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
736,
-480
],
"id": "2b881369-048e-482f-91c5-c7ef5cbf096e",
"name": "brightdata_serp_api",
"executeOnce": false,
"retryOnFail": true,
"credentials": {
"brightdataApi": {
"name": "<your credential>"
}
},
"onError": "continueErrorOutput"
},
{
"parameters": {
"inputSource": "jsonExample",
"jsonExample": "{\n \"intent\": \"an interest\",\n \"location\": \"a place to look for events\"\n}"
},
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
64,
-400
],
"id": "74f79f8d-6497-4bbe-affc-15fe82493c65",
"name": "workflow_trigger",
"alwaysOutputData": false
},
{
"parameters": {
"jsCode": "// Code node \u2014 Run Once for All Items\n// Build a Google Maps search URL from intent + location, biasing by coordinates if available.\n// Input item shape supported:\n// { query: { intent, location, latitude, longitude } } OR flat { intent, location, latitude, longitude }\n// Output: ARRAY of { url }\n\nconst items = $input.all();\n\nconst norm = (s) => (s ?? \"\").toString().trim().replace(/\\s+/g, \" \");\n\nfunction buildMapsSearchURL({ intent, location, latitude, longitude }) {\n const i = norm(intent);\n const l = norm(location);\n const hasLatLng =\n Number.isFinite(latitude) && Number.isFinite(longitude);\n\n if (!i && !l && !hasLatLng) {\n throw new Error(\"Provide at least an intent, a location, or coordinates\");\n }\n\n // Prefer \"intent near lat,lng\" when coords exist; otherwise combine intent + location.\n let q = \"\";\n if (hasLatLng) q = i ? `${i} near ${latitude},${longitude}` : `${latitude},${longitude}`;\n else if (i && l) q = `${i} ${l}`;\n else q = i || l;\n\n return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(q)}`;\n}\n\nreturn items.map((item) => {\n const j = item.json ?? item;\n const src = j.query ?? j;\n const feature = j.features[0];\n\n const lat = Number(src.latitude ?? src.lat ?? src.coordinates?.lat);\n const lng = Number(src.longitude ?? src.lng ?? src.coordinates?.lng);\n\n return {\n url: buildMapsSearchURL({\n intent: $('workflow_trigger').first().json.intent,\n location: src.text ?? feature.properties.formatted,\n latitude: Number.isFinite(feature.properties.lat) ? feature.properties.lat : undefined,\n longitude: Number.isFinite(feature.properties.lon) ? feature.properties.lon : undefined,\n }),\n };\n});"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
512,
-400
],
"id": "12a68956-31fd-44f9-a28e-5a7ed09e8c93",
"name": "format_search_url",
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "// Inputs:\n// - html body at $input.first().json.body (required)\n// - base_url at $json.base_url OR $input.first().json.url (optional but recommended)\n//\n// Output: [{ json: { structured, scraped_data } }]\n\nconst html = $input.first().json.body || \"\";\nconst baseUrl =\n $input.first().json.url ||\n \"\";\n\n// --- helpers ---\nfunction categorizeLink(url) {\n if (!url) return \"unknown\";\n const u = url.toLowerCase();\n if (u.startsWith(\"mailto:\") || u.startsWith(\"tel:\")) return \"contact\";\n if (u.includes(\"facebook\") || u.includes(\"linkedin\") || u.includes(\"pinterest\") || u.includes(\"youtube\") || u.includes(\"twitter\") || u.includes(\"x.com\") || u.includes(\"instagram\") || u.includes(\"tiktok\")) return \"social\";\n if (u.startsWith(\"/\")) return \"internal\";\n try {\n const abs = new URL(u, baseUrl);\n const base = new URL(baseUrl || abs.href);\n return abs.origin === base.origin ? \"internal\" : \"external\";\n } catch {\n return u.startsWith(\"http\") ? \"external\" : \"unknown\";\n }\n}\n\nfunction toAbs(u) {\n if (!u) return u;\n try { return new URL(u, baseUrl).href; } catch { return u; }\n}\n\n// very light HTML entity decode (good enough for headings/link text)\nfunction decodeHTML(str) {\n if (!str) return str;\n const map = { amp: \"&\", lt: \"<\", gt: \">\", quot: '\"', apos: \"'\" };\n return str.replace(/&(#\\d+|#x[0-9a-fA-F]+|[a-zA-Z]+);/g, (m, ent) => {\n if (ent[0] === \"#\") {\n const code = ent[1].toLowerCase() === \"x\" ? parseInt(ent.slice(2),16) : parseInt(ent.slice(1),10);\n return Number.isFinite(code) ? String.fromCharCode(code) : m;\n }\n return map[ent] ?? m;\n });\n}\n\nfunction textOnly(s) { return decodeHTML(s.replace(/<[^>]+>/g, \" \").replace(/\\s+/g, \" \").trim()); }\n\n// --- headings ---\nconst headings = [...html.matchAll(/<h([1-6])[^>]*>([\\s\\S]*?)<\\/h\\1>/gi)].map(m => {\n let inner = m[2];\n const spanMatch = inner.match(/<span[^>]*class=\"([^\"]*)\"[^>]*data-ux=\"([^\"]*)\"?[^>]*>([\\s\\S]*?)<\\/span>/i);\n if (spanMatch) inner = spanMatch[3];\n return { tag: m[1], text: textOnly(inner) };\n});\n\n// --- links (href + text) ---\nconst links = [...html.matchAll(/<a\\b([^>]*?)href=\"([^\"]+)\"([^>]*)>([\\s\\S]*?)<\\/a>/gi)].map(m => {\n const href = m[2];\n const text = textOnly(m[4]);\n const url = toAbs(href);\n return { url, text, category: categorizeLink(href) };\n});\n\n// --- images (src + alt) ---\nconst images = [...html.matchAll(/<img\\b([^>]*?)src=\"([^\"]+)\"([^>]*?)>/gi)].map(m => {\n const src = toAbs(m[2]);\n const altMatch = m[0].match(/\\balt=\"([^\"]*)\"/i);\n const alt = altMatch ? decodeHTML(altMatch[1]) : \"\";\n return { src, alt };\n});\n\n// --- css + js refs ---\nconst css = [...html.matchAll(/<link[^>]+href=\"([^\"]+\\.css(?:\\?[^\"]*)?)\"[^>]*>/gi)].map(m => toAbs(m[1]));\nconst js = [...html.matchAll(/<script[^>]+src=\"([^\"]+)\"[^>]*>\\s*<\\/script>/gi)].map(m => toAbs(m[1]));\n\n// --- meta tags ---\nconst title =\n html.match(/<title>([\\s\\S]*?)<\\/title>/i)?.[1]?.trim() ||\n html.match(/<meta[^>]+property=\"og:title\"[^>]+content=\"([^\"]*)\"/i)?.[1] ||\n html.match(/<meta[^>]+name=\"twitter:title\"[^>]+content=\"([^\"]*)\"/i)?.[1] ||\n \"\";\n\nconst description =\n html.match(/<meta[^>]+name=\"description\"[^>]+content=\"([^\"]*)\"/i)?.[1] ||\n html.match(/<meta[^>]+property=\"og:description\"[^>]+content=\"([^\"]*)\"/i)?.[1] ||\n html.match(/<meta[^>]+name=\"twitter:description\"[^>]+content=\"([^\"]*)\"/i)?.[1] ||\n \"\";\n\nconst canonical = toAbs(html.match(/<link[^>]+rel=\"canonical\"[^>]+href=\"([^\"]+)\"/i)?.[1] || \"\");\nconst robots = html.match(/<meta[^>]+name=\"robots\"[^>]+content=\"([^\"]*)\"/i)?.[1] || \"\";\n\nconst ogImage = toAbs(html.match(/<meta[^>]+property=\"og:image\"[^>]+content=\"([^\"]+)\"/i)?.[1] || \"\");\nconst twImage = toAbs(html.match(/<meta[^>]+name=\"twitter:image\"[^>]+content=\"([^\"]+)\"/i)?.[1] || \"\");\n\n// --- tables -> rows of cell text ---\nconst tables = [...html.matchAll(/<table[^>]*>([\\s\\S]*?)<\\/table>/gi)].map(t => {\n const tableHTML = t[1];\n const rows = [...tableHTML.matchAll(/<tr[^>]*>([\\s\\S]*?)<\\/tr>/gi)].map(r => {\n const rowHTML = r[1];\n const cells = [...rowHTML.matchAll(/<(td|th)[^>]*>([\\s\\S]*?)<\\/\\1>/gi)].map(c => textOnly(c[2]));\n return cells;\n });\n return { rows };\n});\n\n// --- structured payload ---\nconst structured = {\n base_url: baseUrl || null,\n meta: {\n title: decodeHTML(title),\n description: decodeHTML(description),\n canonical,\n robots,\n og_image: ogImage,\n twitter_image: twImage,\n },\n headings,\n links,\n images,\n css,\n js,\n tables,\n};\n\n// return both raw and stringified\nreturn [\n {\n json: {\n structured,\n scraped_data: JSON.stringify(structured, null, 2),\n },\n },\n];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
960,
-592
],
"id": "4a5cceeb-6d65-4d08-a4ec-430b758e4dc6",
"name": "normalize",
"onError": "continueErrorOutput"
},
{
"parameters": {
"operation": "appendOrUpdate",
"documentId": {
"__rl": true,
"value": "1yA7rRZkgJAGrs6SiGVqHJmwcORunAmM2Mw1KecXcS4A",
"mode": "list",
"cachedResultName": "underfoot_google_maps_serp_cache",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1yA7rRZkgJAGrs6SiGVqHJmwcORunAmM2Mw1KecXcS4A/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": "gid=0",
"mode": "list",
"cachedResultName": "Sheet1",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1yA7rRZkgJAGrs6SiGVqHJmwcORunAmM2Mw1KecXcS4A/edit#gid=0"
},
"columns": {
"mappingMode": "autoMapInputData",
"value": {},
"matchingColumns": [
"url"
],
"schema": [
{
"id": "url",
"displayName": "url",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "title",
"displayName": "title",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "description",
"displayName": "description",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "rank",
"displayName": "rank",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "global_rank",
"displayName": "global_rank",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "query",
"displayName": "query",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "language",
"displayName": "language",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "location",
"displayName": "location",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "latitude",
"displayName": "latitude",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "longitude",
"displayName": "longitude",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "original_url",
"displayName": "original_url",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "cached_at",
"displayName": "cached_at",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "user_intent",
"displayName": "user_intent",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "user_location",
"displayName": "user_location",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {
"handlingExtraData": "ignoreIt",
"useAppend": true
}
},
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.7,
"position": [
1408,
-592
],
"id": "75ba4e97-b3cc-410d-9c2c-b14cdcc48293",
"name": "update_cache",
"retryOnFail": false,
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"onError": "continueErrorOutput"
},
{
"parameters": {
"url": "=https://api.geoapify.com/v1/geocode/search",
"authentication": "genericCredentialType",
"genericAuthType": "httpQueryAuth",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "text",
"value": "={{ $json.location }}"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
288,
-400
],
"id": "fb5f1e60-8b0c-44a1-9e46-aeee3360a1fa",
"name": "normalize_location_with_geoapify",
"credentials": {
"httpCustomAuth": {
"name": "<your credential>"
},
"httpQueryAuth": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"useCustomSchema": true,
"schema": "underfoot",
"tableId": "scrape_results",
"dataToSend": "autoMapInputData",
"inputsToIgnore": "error"
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
1184,
-880
],
"id": "c0ff642c-7cc4-4548-8535-ddafbe2fd0c4",
"name": "create_cache_rows",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
},
"onError": "continueErrorOutput"
},
{
"parameters": {
"useCustomSchema": true,
"schema": "underfoot",
"operation": "update",
"tableId": "scrape_results",
"filters": {
"conditions": [
{
"keyName": "url",
"condition": "eq",
"keyValue": "={{ $json.url }}"
}
]
}
},
"type": "n8n-nodes-base.supabase",
"typeVersion": 1,
"position": [
1408,
-784
],
"id": "626ef149-ed81-4a1d-840f-9556c570638f",
"name": "update_existing_rows",
"credentials": {
"supabaseApi": {
"name": "<your credential>"
}
},
"onError": "continueErrorOutput"
},
{
"parameters": {
"authentication": "webhook",
"content": "I think it passed....",
"options": {}
},
"type": "n8n-nodes-base.discord",
"typeVersion": 2,
"position": [
1632,
-864
],
"id": "dcdbc010-1339-40b8-b544-0327c37f5fe0",
"name": "discord_notifier",
"credentials": {
"discordWebhookApi": {
"name": "<your credential>"
}
}
}
],
"connections": {
"brightdata_serp_api": {
"main": [
[
{
"node": "normalize",
"type": "main",
"index": 0
}
],
[
{
"node": "call_error_notifier",
"type": "main",
"index": 0
}
]
]
},
"workflow_trigger": {
"main": [
[
{
"node": "normalize_location_with_geoapify",
"type": "main",
"index": 0
}
]
]
},
"call_error_notifier": {
"main": [
[]
]
},
"format_search_url": {
"main": [
[
{
"node": "brightdata_serp_api",
"type": "main",
"index": 0
}
],
[
{
"node": "call_error_notifier",
"type": "main",
"index": 0
}
]
]
},
"normalize": {
"main": [
[
{
"node": "update_cache",
"type": "main",
"index": 0
},
{
"node": "create_cache_rows",
"type": "main",
"index": 0
}
],
[
{
"node": "call_error_notifier",
"type": "main",
"index": 0
}
]
]
},
"update_cache": {
"main": [
[
{
"node": "discord_notifier",
"type": "main",
"index": 0
}
],
[
{
"node": "call_error_notifier",
"type": "main",
"index": 0
}
]
]
},
"normalize_location_with_geoapify": {
"main": [
[
{
"node": "format_search_url",
"type": "main",
"index": 0
}
]
]
},
"create_cache_rows": {
"main": [
[
{
"node": "discord_notifier",
"type": "main",
"index": 0
}
],
[
{
"node": "update_existing_rows",
"type": "main",
"index": 0
}
]
]
},
"update_existing_rows": {
"main": [
[
{
"node": "discord_notifier",
"type": "main",
"index": 0
}
],
[
{
"node": "call_error_notifier",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"callerPolicy": "workflowsFromSameOwner",
"errorWorkflow": "g0RA1ILyuSMHbt6q"
},
"versionId": "c2a92acc-d2e6-4e94-8efd-ff44e191a668",
"meta": {
"templateCredsSetupCompleted": true
},
"id": "TjJFvBLuhBlEO3c8",
"tags": []
}
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.
brightdataApidiscordWebhookApigoogleSheetsOAuth2ApihttpCustomAuthhttpQueryAuthsupabaseApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
wf_maps_serp_backup_data. Uses httpRequest, executeWorkflowTrigger, googleSheets, supabase. Event-driven trigger; 10 nodes.
Source: https://gist.github.com/anchildress1/cab1237affe75f0bed6629faeb940f2c — 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 enables webhooks for nearly realtime updates (every 5 seconds) from Notion Databases.
wf_serp_search_tool. Uses executeWorkflowTrigger, supabase, @brightdata/n8n-nodes-brightdata, httpRequest. Event-driven trigger; 10 nodes.
cv-elaborator. Uses chatTrigger, executeWorkflowTrigger, agent, lmChatGoogleGemini. Chat trigger; 18 nodes.
Reagendamiento_v2. Uses executeWorkflowTrigger, redis, httpRequest, n8n-nodes-evolution-api. Event-driven trigger; 89 nodes.
This workflow acts as a junior finance research analyst for a UK boutique M&A or corporate finance team. It listens for Slack messages, classifies the request, gathers company or market data, and prod