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.
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