{
  "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
          }
        ]
      ]
    }
  }
}