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 →
{
"name": "Im\u00f3veis SP - Coleta + Geocode + Score (Manual)",
"nodes": [
{
"parameters": {},
"id": "ManualTrigger",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-600,
-80
]
},
{
"parameters": {
"values": {
"string": [
{
"name": "APIFY_TOKEN",
"value": "<<<COLOQUE_SEU_APIFY_TOKEN_AQUI>>>"
},
{
"name": "APIFY_ACTOR_ID",
"value": "<<<ex.: apify~web-scraper OU o ID do Actor do QuintoAndar>>>"
},
{
"name": "START_URLS",
"value": "[{\"url\":\"<<<URL_DE_BUSCA_FILTRADA_DO_PORTAL_1>>>\"},{\"url\":\"<<<URL_DE_BUSCA_DO_PORTAL_2>>>\"}]"
},
{
"name": "DIST_MAX_METRO_KM",
"value": "1.2"
}
]
}
},
"id": "SetConfig",
"name": "Set Config",
"type": "n8n-nodes-base.set",
"typeVersion": 2,
"position": [
-400,
-80
]
},
{
"parameters": {
"url": "={{\"https://api.apify.com/v2/acts/\" + $json[\"APIFY_ACTOR_ID\"] + \"/runs?token=\" + $json[\"APIFY_TOKEN\"] + \"&waitForFinish=120\"}}",
"options": {
"headerParametersUi": {
"parameter": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Accept",
"value": "application/json"
}
]
}
},
"jsonParameters": true,
"optionsUi": {},
"sendBody": true,
"bodyParametersJson": "={\n \"input\": {\n \"startUrls\": {{ $json[\"START_URLS\"] }},\n \"maxConcurrency\": 2,\n \"maxRequestsPerCrawl\": 100,\n \"maxDepth\": 0\n }\n}"
},
"id": "ApifyRun",
"name": "Apify - Run Actor",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
-160,
-80
]
},
{
"parameters": {
"url": "={{\"https://api.apify.com/v2/datasets/\" + $json[\"data\"][\"defaultDatasetId\"] + \"/items?clean=true\"}}",
"options": {
"headerParametersUi": {
"parameter": [
{
"name": "Accept",
"value": "application/json"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
}
},
"id": "ApifyDataset",
"name": "Apify - Get Dataset Items",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
60,
-80
]
},
{
"parameters": {
"functionCode": "return items.flatMap(i => (Array.isArray(i.json) ? i.json : [i]));"
},
"id": "FlattenItems",
"name": "Flatten Items",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
260,
-80
]
},
{
"parameters": {
"url": "={{\"https://nominatim.openstreetmap.org/search?format=json&limit=1&q=\" + encodeURIComponent(($json.address || $json.endereco || $json.endereco_bairro || \"\") + \", S\u00e3o Paulo, SP\")}}",
"options": {
"headerParametersUi": {
"parameter": [
{
"name": "User-Agent",
"value": "n8n-imoveis/1.0 (contato: seuemail@exemplo.com)"
}
]
}
}
},
"id": "Nominatim",
"name": "Geocode (Nominatim)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
460,
-80
]
},
{
"parameters": {
"functionCode": "const addr = $json.address || $json.endereco || $json.endereco_bairro || \"\";\nconst geo = Array.isArray(items[0].json) ? items[0].json[0] : items[0].json;\nlet lat = null, lon = null, geocode_note = \"\";\nif (Array.isArray(geo) && geo.length>0) {\n lat = parseFloat(geo[0].lat);\n lon = parseFloat(geo[0].lon);\n} else if (geo && geo.length>0) {\n lat = parseFloat(geo[0].lat);\n lon = parseFloat(geo[0].lon);\n}\nif (!lat || !lon) {\n // fallback: bairro/SP\n geocode_note = \"geocode aproximado (bairro)\";\n}\nreturn items.map((it) => {\n it.json.lat = lat || it.json.lat || null;\n it.json.lon = lon || it.json.lon || null;\n it.json.geocode_note = geocode_note;\n return it;\n});"
},
"id": "SetLatLon",
"name": "Set Lat/Lon",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
660,
-80
]
},
{
"parameters": {
"url": "https://overpass-api.de/api/interpreter",
"options": {
"headerParametersUi": {
"parameter": [
{
"name": "Accept",
"value": "application/json"
}
]
}
},
"method": "POST",
"sendBody": true,
"jsonParameters": false,
"optionsUi": {},
"body": "={{`[out:json];(node(around:2000,${$json.lat},${$json.lon})[railway=station][station=subway];node(around:2000,${$json.lat},${$json.lon})[public_transport=station];);out;`}}"
},
"id": "Overpass",
"name": "Metr\u00f4 (Overpass)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
860,
-80
]
},
{
"parameters": {
"functionCode": "// Haversine\nfunction distKm(a,b){const R=6371;const dLat=(b.lat-a.lat)*Math.PI/180;const dLon=(b.lon-a.lon)*Math.PI/180;const la1=a.lat*Math.PI/180;const la2=b.lat*Math.PI/180;const x=Math.sin(dLat/2)**2+Math.sin(dLon/2)**2*Math.cos(la1)*Math.cos(la2);return 2*R*Math.asin(Math.sqrt(x));}\n\nreturn items.map(it=>{\n const lat = parseFloat(it.json.lat||0), lon = parseFloat(it.json.lon||0);\n let minKm = null;\n try{\n const elements = it.json.elements || it.json.data || it.json;\n const arr = (elements && elements.elements) ? elements.elements : [];\n for (const e of arr){\n if (e.type==='node' && e.lat && e.lon){\n const d = distKm({lat,lon},{lat:e.lat,lon:e.lon});\n if (minKm===null || d<minKm) minKm = d;\n }\n }\n }catch{}\n it.json.distancia_metro_km = minKm!=null ? Number(minKm.toFixed(3)) : null;\n return it;\n});"
},
"id": "CalcDistMetro",
"name": "Calcular dist\u00e2ncia ao metr\u00f4",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
1060,
-80
]
},
{
"parameters": {
"functionCode": "// C\u00e1lculos principais do seu JSON\nreturn items.map(it=>{\n const j = it.json;\n const preco = Number(j.preco_pedido||j.price||0);\n const cond = Number(j.condominio_mensal||j.condo||0);\n const iptuA = Number(j.iptu_anual||j.iptu||0);\n const area = Number(j.area_m2||j.area||0);\n const aluguel = Number(j.aluguel_estimado||j.rent_est||0); // Sug.: calcular antes por compar\u00e1veis\n const iptuM = iptuA/12;\n const yieldB = preco? (aluguel/preco):0;\n const yieldL = preco? ((aluguel - cond - iptuM)/preco):0;\n const precoM2 = area? (preco/area):0;\n const condoRent = aluguel? (cond/aluguel):1;\n\n j.iptu_mensalizado = Number(iptuM.toFixed(2));\n j.yield_bruto_mensal = Number(yieldB.toFixed(5));\n j.yield_liquido_mensal = Number(yieldL.toFixed(5));\n j.preco_m2 = Math.round(precoM2||0);\n j.condo_rent_ratio = Number(condoRent.toFixed(3));\n\n // Cortes\n if (cond>1000) { j._descartado = \"condominio>1000\"; return it; }\n if (aluguel <= (cond + iptuM)) { j._descartado = \"aluguel<=despesas\"; return it; }\n if (yieldB < 0.008) { j._descartado = \"yield_bruto<0.008\"; return it; }\n if (condoRent > 0.35) { j._descartado = \"condo_rent>0.35\"; return it; }\n\n // Score simples (sem compar\u00e1veis de m\u00b2)\n let score = 0;\n score += 0.40 * Math.min(Math.max(j.yield_liquido_mensal,0), 0.05);\n const dist = Number(j.distancia_metro_km||9);\n score += 0.10 * (1 - Math.min(dist/1.2,1));\n score += 0.10 * (1 - Math.min(condoRent,1));\n if (j.yield_liquido_mensal >= 0.01) score += 0.05;\n if (dist>1.2) score -= 0.05;\n j.score_total = Number(score.toFixed(2));\n\n return it;\n});"
},
"id": "Score",
"name": "Calcular yields + score",
"type": "n8n-nodes-base.function",
"typeVersion": 2,
"position": [
1260,
-80
]
},
{
"parameters": {
"operation": "append",
"sheetId": "<<<COLOQUE_O_ID_DA_SUA_PLANILHA_GOOGLE_AQUI>>>",
"range": "Resultados!A:Z",
"options": {
"valueInputMode": "RAW"
},
"fields": "url_anuncio,endereco_bairro,preco_pedido,condominio_mensal,iptu_anual,area_m2,quartos,banheiros,vaga,distancia_metro_km,aluguel_estimado,yield_bruto_mensal,yield_liquido_mensal,condo_rent_ratio,preco_m2,score_total,geocode_note"
},
"id": "Sheets",
"name": "Google Sheets - Append",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [
1460,
-80
],
"credentials": {
"googleApi": "<your credential>"
}
}
],
"connections": {
"Manual Trigger": {
"main": [
[
{
"node": "Set Config",
"type": "main",
"index": 0
}
]
]
},
"Set Config": {
"main": [
[
{
"node": "Apify - Run Actor",
"type": "main",
"index": 0
}
]
]
},
"Apify - Run Actor": {
"main": [
[
{
"node": "Apify - Get Dataset Items",
"type": "main",
"index": 0
}
]
]
},
"Apify - Get Dataset Items": {
"main": [
[
{
"node": "Flatten Items",
"type": "main",
"index": 0
}
]
]
},
"Flatten Items": {
"main": [
[
{
"node": "Geocode (Nominatim)",
"type": "main",
"index": 0
}
]
]
},
"Geocode (Nominatim)": {
"main": [
[
{
"node": "Set Lat/Lon",
"type": "main",
"index": 0
}
]
]
},
"Set Lat/Lon": {
"main": [
[
{
"node": "Metr\u00f4 (Overpass)",
"type": "main",
"index": 0
}
]
]
},
"Metr\u00f4 (Overpass)": {
"main": [
[
{
"node": "Calcular dist\u00e2ncia ao metr\u00f4",
"type": "main",
"index": 0
}
]
]
},
"Calcular dist\u00e2ncia ao metr\u00f4": {
"main": [
[
{
"node": "Calcular yields + score",
"type": "main",
"index": 0
}
]
]
},
"Calcular yields + score": {
"main": [
[
{
"node": "Google Sheets - Append",
"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.
googleApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Imóveis SP - Coleta + Geocode + Score (Manual). Uses httpRequest, googleSheets. Event-driven trigger; 11 nodes.
Source: https://gist.github.com/andrespector/5623dc6a862689966a7104b87c053d73 — 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.
TrackCollect_deeper. Uses googleSheets, httpRequest, @n-octo-n/n8n-nodes-json-database, itemLists. Event-driven trigger; 80 nodes.
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.
Product Ad Machine. Uses googleDrive, googleSheets, httpRequest, @ffmpeg-micro/n8n-nodes-ffmpeg-micro. Event-driven trigger; 60 nodes.
Basic AI SEO KW Research n8n DataForSEO Gumroad 030125. Uses googleSheets, googleDrive, httpRequest. Event-driven trigger; 56 nodes.