This workflow corresponds to n8n.io template #15008 — 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 →
{
"nodes": [
{
"id": "c1b2c3d4-0003-4000-8000-000000000001",
"name": "Every Monday 8am",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
112,
560
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 8 * * 1"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "c1b2c3d4-0003-4000-8000-000000000002",
"name": "Scrape Madrid",
"type": "n8n-nodes-idealista-scraper.idealistaScraper",
"position": [
368,
464
],
"parameters": {
"numPages": 3,
"operation": "sale",
"sizeFilters": {},
"priceFilters": {},
"rentalFilters": {},
"featureFilters": {},
"advancedFilters": {},
"conditionFilters": {},
"floorTimeFilters": {},
"propertyTypeFilters": {},
"bedroomBathroomFilters": {}
},
"typeVersion": 1
},
{
"id": "c1b2c3d4-0003-4000-8000-000000000003",
"name": "Scrape Barcelona",
"type": "n8n-nodes-idealista-scraper.idealistaScraper",
"position": [
368,
688
],
"parameters": {
"numPages": 3,
"operation": "sale",
"locationId": "0-EU-ES-08-19-001-013",
"sizeFilters": {},
"locationName": "Barcelona",
"priceFilters": {},
"rentalFilters": {},
"featureFilters": {},
"advancedFilters": {},
"conditionFilters": {},
"floorTimeFilters": {},
"propertyTypeFilters": {},
"bedroomBathroomFilters": {}
},
"typeVersion": 1
},
{
"id": "c1b2c3d4-0003-4000-8000-000000000004",
"name": "Analyze Madrid",
"type": "n8n-nodes-base.code",
"position": [
624,
464
],
"parameters": {
"jsCode": "const items = $input.all();\nconst prices = items.map(i => i.json.price).filter(p => typeof p === 'number' && p > 0);\nconst sizes = items.map(i => i.json.size).filter(s => typeof s === 'number' && s > 0);\nconst pricesPerM2 = items.map(i => i.json.priceByArea || i.json.unitPrice).filter(p => typeof p === 'number' && p > 0);\nconst rooms = items.map(i => i.json.rooms).filter(r => typeof r === 'number' && r > 0);\n\nconst avg = arr => arr.length ? Math.round(arr.reduce((a, b) => a + b, 0) / arr.length) : 0;\nconst median = arr => {\n if (!arr.length) return 0;\n const sorted = [...arr].sort((a, b) => a - b);\n const mid = Math.floor(sorted.length / 2);\n return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);\n};\n\nreturn [{\n json: {\n market: 'Madrid',\n totalListings: items.length,\n avgPrice: avg(prices),\n medianPrice: median(prices),\n minPrice: prices.length ? Math.min(...prices) : 0,\n maxPrice: prices.length ? Math.max(...prices) : 0,\n avgSize: avg(sizes),\n medianSize: median(sizes),\n avgPricePerM2: avg(pricesPerM2),\n medianPricePerM2: median(pricesPerM2),\n avgRooms: (rooms.length ? (rooms.reduce((a, b) => a + b, 0) / rooms.length).toFixed(1) : '0'),\n reportDate: new Date().toISOString().split('T')[0]\n }\n}];"
},
"typeVersion": 2
},
{
"id": "c1b2c3d4-0003-4000-8000-000000000005",
"name": "Analyze Barcelona",
"type": "n8n-nodes-base.code",
"position": [
624,
688
],
"parameters": {
"jsCode": "const items = $input.all();\nconst prices = items.map(i => i.json.price).filter(p => typeof p === 'number' && p > 0);\nconst sizes = items.map(i => i.json.size).filter(s => typeof s === 'number' && s > 0);\nconst pricesPerM2 = items.map(i => i.json.priceByArea || i.json.unitPrice).filter(p => typeof p === 'number' && p > 0);\nconst rooms = items.map(i => i.json.rooms).filter(r => typeof r === 'number' && r > 0);\n\nconst avg = arr => arr.length ? Math.round(arr.reduce((a, b) => a + b, 0) / arr.length) : 0;\nconst median = arr => {\n if (!arr.length) return 0;\n const sorted = [...arr].sort((a, b) => a - b);\n const mid = Math.floor(sorted.length / 2);\n return sorted.length % 2 ? sorted[mid] : Math.round((sorted[mid - 1] + sorted[mid]) / 2);\n};\n\nreturn [{\n json: {\n market: 'Barcelona',\n totalListings: items.length,\n avgPrice: avg(prices),\n medianPrice: median(prices),\n minPrice: prices.length ? Math.min(...prices) : 0,\n maxPrice: prices.length ? Math.max(...prices) : 0,\n avgSize: avg(sizes),\n medianSize: median(sizes),\n avgPricePerM2: avg(pricesPerM2),\n medianPricePerM2: median(pricesPerM2),\n avgRooms: (rooms.length ? (rooms.reduce((a, b) => a + b, 0) / rooms.length).toFixed(1) : '0'),\n reportDate: new Date().toISOString().split('T')[0]\n }\n}];"
},
"typeVersion": 2
},
{
"id": "c1b2c3d4-0003-4000-8000-000000000006",
"name": "Merge Market Data",
"type": "n8n-nodes-base.merge",
"position": [
880,
560
],
"parameters": {},
"typeVersion": 3.1
},
{
"id": "c1b2c3d4-0003-4000-8000-000000000007",
"name": "Build HTML Report",
"type": "n8n-nodes-base.code",
"position": [
1152,
560
],
"parameters": {
"jsCode": "const markets = $input.all().map(i => i.json);\nconst date = new Date().toLocaleDateString('en-US', {\n weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'\n});\n\nconst fmt = n => typeof n === 'number' ? n.toLocaleString('en-US') : n;\n\nconst rows = markets.map(m => `\n <tr>\n <td style=\"padding:12px 16px;border-bottom:1px solid #e2e8f0;font-weight:600;color:#1a202c\">${m.market}</td>\n <td style=\"padding:12px 16px;border-bottom:1px solid #e2e8f0;text-align:right\">${m.totalListings}</td>\n <td style=\"padding:12px 16px;border-bottom:1px solid #e2e8f0;text-align:right\">${fmt(m.avgPrice)} EUR</td>\n <td style=\"padding:12px 16px;border-bottom:1px solid #e2e8f0;text-align:right\">${fmt(m.medianPrice)} EUR</td>\n <td style=\"padding:12px 16px;border-bottom:1px solid #e2e8f0;text-align:right\">${fmt(m.minPrice)} - ${fmt(m.maxPrice)} EUR</td>\n <td style=\"padding:12px 16px;border-bottom:1px solid #e2e8f0;text-align:right\">${m.avgSize} m\\u00b2</td>\n <td style=\"padding:12px 16px;border-bottom:1px solid #e2e8f0;text-align:right\">${fmt(m.avgPricePerM2)} EUR/m\\u00b2</td>\n </tr>\n`).join('');\n\nconst html = `\n<div style=\"font-family:'Segoe UI',Arial,sans-serif;max-width:900px;margin:0 auto;padding:20px\">\n <div style=\"background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);padding:30px;border-radius:12px 12px 0 0\">\n <h1 style=\"color:white;margin:0;font-size:24px\">Weekly Real Estate Market Report</h1>\n <p style=\"color:rgba(255,255,255,0.85);margin:8px 0 0 0;font-size:14px\">${date} | Source: Idealista.com</p>\n </div>\n <div style=\"background:white;padding:24px;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px\">\n <h2 style=\"color:#2d3748;font-size:18px;margin-top:0\">Market Comparison: ${markets.map(m => m.market).join(' vs ')}</h2>\n <table style=\"width:100%;border-collapse:collapse;margin:16px 0\">\n <thead>\n <tr style=\"background:#f7fafc\">\n <th style=\"padding:12px 16px;text-align:left;font-size:13px;color:#718096;text-transform:uppercase;letter-spacing:0.5px\">Market</th>\n <th style=\"padding:12px 16px;text-align:right;font-size:13px;color:#718096;text-transform:uppercase;letter-spacing:0.5px\">Listings</th>\n <th style=\"padding:12px 16px;text-align:right;font-size:13px;color:#718096;text-transform:uppercase;letter-spacing:0.5px\">Avg Price</th>\n <th style=\"padding:12px 16px;text-align:right;font-size:13px;color:#718096;text-transform:uppercase;letter-spacing:0.5px\">Median</th>\n <th style=\"padding:12px 16px;text-align:right;font-size:13px;color:#718096;text-transform:uppercase;letter-spacing:0.5px\">Range</th>\n <th style=\"padding:12px 16px;text-align:right;font-size:13px;color:#718096;text-transform:uppercase;letter-spacing:0.5px\">Avg Size</th>\n <th style=\"padding:12px 16px;text-align:right;font-size:13px;color:#718096;text-transform:uppercase;letter-spacing:0.5px\">EUR/m\\u00b2</th>\n </tr>\n </thead>\n <tbody>${rows}</tbody>\n </table>\n <hr style=\"border:none;border-top:1px solid #e2e8f0;margin:20px 0\">\n <p style=\"color:#a0aec0;font-size:12px;margin:0\">Generated automatically by n8n using the Idealista Scraper community node. API-based extraction with 64+ filters across Spain, Italy, and Portugal.</p>\n </div>\n</div>`;\n\nconst subject = `Weekly Market Report: ${markets.map(m => m.market).join(' vs ')} - ${markets[0]?.reportDate || ''}`;\n\nreturn [{ json: { subject, htmlBody: html } }];"
},
"typeVersion": 2
},
{
"id": "c1b2c3d4-0003-4000-8000-000000000008",
"name": "Email Report",
"type": "n8n-nodes-base.gmail",
"position": [
1408,
480
],
"parameters": {
"sendTo": "user@example.com",
"message": "={{ $json.htmlBody }}",
"options": {},
"subject": "={{ $json.subject }}"
},
"typeVersion": 2.2
},
{
"id": "c1b2c3d4-0003-4000-8000-000000000009",
"name": "Log to Market History",
"type": "n8n-nodes-base.googleSheets",
"position": [
1408,
672
],
"parameters": {
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": ""
},
"documentId": {
"__rl": true,
"mode": "list",
"value": ""
}
},
"typeVersion": 4.5
},
{
"id": "c1b2c3d4-0003-4000-8000-000000000020",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
144,
-128
],
"parameters": {
"color": 4,
"width": 1380,
"height": 520,
"content": "## Analyze Idealista Real Estate Market Trends and Email Weekly Reports\n\nEvery Monday at 8am, this workflow scrapes property listings from multiple Idealista markets, calculates key statistics, builds an HTML comparison report, emails it to you, and logs data to Google Sheets for trend tracking. Idealista has no official API -- this workflow bridges that gap.\n\n**This workflow uses the `n8n-nodes-idealista-scraper` community node and requires a self-hosted n8n instance.**\n\n### How it works\n1. The Schedule Trigger fires every Monday at 8am\n2. Two Idealista Scraper nodes fetch Madrid and Barcelona listings in parallel via API-based extraction (never breaks)\n3. Code nodes calculate per-market statistics: avg/median price, price range, price per m\u00b2, avg size, avg rooms\n4. The Merge node combines both market analyses into one dataset\n5. A Code node builds a professionally formatted HTML comparison table\n6. The report is emailed via Gmail and weekly stats are logged to Google Sheets\n\n### Setup\n1. Install **n8n-nodes-idealista-scraper** via Settings > Community Nodes\n2. Add your **Apify API** credential ([get token](https://console.apify.com/account/integrations))\n3. Add your **Gmail** credential (OAuth2)\n4. Create a Google Sheet with a tab named \"MarketHistory\"\n5. Update the email recipient in the Gmail node\n6. **Activate the workflow!**\n\n### Customization\n- Add more cities by duplicating a Scraper + Analysis pair (Valencia, Rome, Lisbon, Milan)\n- Switch `operation` from `sale` to `rent` to analyze rental markets\n- Add price filters to focus on specific segments (luxury >1M EUR, budget <200K EUR)\n- Calculate rental yield by scraping both sale and rent for the same area\n- Cost: ~$0.50/week (2 markets x 3 pages x ~40 properties each)"
},
"typeVersion": 1
},
{
"id": "c1b2c3d4-0003-4000-8000-000000000022",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
224,
880
],
"parameters": {
"width": 260,
"height": 140,
"content": "## 1. Scrape Markets\nFetches listings from Madrid and Barcelona in parallel. 3 pages each (~120 properties per city)."
},
"typeVersion": 1
},
{
"id": "c1b2c3d4-0003-4000-8000-000000000023",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
544,
880
],
"parameters": {
"width": 260,
"height": 140,
"content": "## 2. Analyze\nCalculates per-market statistics: avg/median price, price per m\u00b2, inventory count, avg size."
},
"typeVersion": 1
},
{
"id": "c1b2c3d4-0003-4000-8000-000000000024",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
864,
880
],
"parameters": {
"width": 340,
"height": 140,
"content": "## 3. Merge & Report\nCombines market data, builds formatted HTML comparison table, emails report via Gmail."
},
"typeVersion": 1
},
{
"id": "c1b2c3d4-0003-4000-8000-000000000025",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1264,
880
],
"parameters": {
"width": 260,
"height": 140,
"content": "## 4. Deliver\nEmails the HTML report and logs weekly stats to Google Sheets for trend tracking."
},
"typeVersion": 1
}
],
"connections": {
"Scrape Madrid": {
"main": [
[
{
"node": "Analyze Madrid",
"type": "main",
"index": 0
}
]
]
},
"Analyze Madrid": {
"main": [
[
{
"node": "Merge Market Data",
"type": "main",
"index": 0
}
]
]
},
"Every Monday 8am": {
"main": [
[
{
"node": "Scrape Madrid",
"type": "main",
"index": 0
},
{
"node": "Scrape Barcelona",
"type": "main",
"index": 0
}
]
]
},
"Scrape Barcelona": {
"main": [
[
{
"node": "Analyze Barcelona",
"type": "main",
"index": 0
}
]
]
},
"Analyze Barcelona": {
"main": [
[
{
"node": "Merge Market Data",
"type": "main",
"index": 1
}
]
]
},
"Build HTML Report": {
"main": [
[
{
"node": "Email Report",
"type": "main",
"index": 0
}
]
]
},
"Merge Market Data": {
"main": [
[
{
"node": "Build HTML Report",
"type": "main",
"index": 0
},
{
"node": "Log to Market History",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Real estate investors comparing markets across cities, agencies generating market reports for clients, property consultants doing due diligence, or analysts tracking price trends in Southern European property markets.
Source: https://n8n.io/workflows/15008/ — 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.
YOUR_ID 4. Uses gmail, googleDrive, googleSheets, httpRequest. Scheduled trigger; 53 nodes.
Looking for a way to track GitHub bounty issues automatically and get notified in real time? This GitHub Bounty Tracker workflow monitors repositories for issues labeled 💎 Bounty, logs them in Google
This workflow automatically sends a beautifully designed HTML newsletter every Sunday at 8 AM, featuring products currently on sale from your Algolia-powered e-commerce store.
This n8n template demonstrates how to build a Auto Lead Gen & Outreach System for Local Businesses specifically designed to help businesses that don’t have a website yet.
The workflow is triggered automatically every day at 12:00 PM using a Cron node.