AutomationFlowsAI & RAG › Score Property Investments Using Claude (anthropic), Google Sheets and Slack

Score Property Investments Using Claude (anthropic), Google Sheets and Slack

ByOneclick AI Squad @oneclick-ai on n8n.io

This workflow scrapes property listings, enriches them with market data, and uses Claude AI to score each listing's investment potential based on rental yield, capital growth trends, location desirability, and risk factors. Trigger - Scheduled run initiates a scrape job Scrape…

Cron / scheduled trigger★★★★☆ complexityAI-powered22 nodesHTTP RequestAgentAnthropic ChatGoogle Sheets
AI & RAG Trigger: Cron / scheduled Nodes: 22 Complexity: ★★★★☆ AI nodes: yes Added:
Score Property Investments Using Claude (anthropic), Google Sheets and Slack — n8n workflow card showing HTTP Request, Agent, Anthropic Chat integration

This workflow corresponds to n8n.io template #13686 — we link there as the canonical source.

This workflow follows the Agent → 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 →

Download .json
{
  "id": "j42adE86HAmB5M8X",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Property Investment Scorer",
  "tags": [],
  "nodes": [
    {
      "id": "257f0db2-da05-40e8-8893-a3e64324e2cc",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "width": 920,
        "height": 1340,
        "content": "## AI Property Investment Scorer\n\nThis workflow scrapes property listings, enriches them with market data, and uses Claude AI to score each listing's investment potential based on rental yield, capital growth trends, location desirability, and risk factors.\n\n### How it works\n\n1. **Trigger** - Scheduled run initiates a scrape job\n2. **Scrape Listings** - Fetches property listings from target URL(s) via HTTP\n3. **Parse Listings** - Extracts structured data (price, bedrooms, suburb, etc.)\n4. **Fetch Market Data** - Pulls suburb median prices, rental yields, vacancy rates\n5. **Fetch Demographics** - Gets population growth, income levels, infrastructure data\n6. **Combine Enrichment** - Merges all data per listing\n7. **AI Investment Scoring** - Claude AI scores each listing (0\u2013100) with rationale\n8. **Filter Top Picks** - Keeps listings above configurable score threshold\n9. **Format Report** - Builds a clean investment report\n10. **Save to Google Sheets** - Logs all scored listings for tracking\n11. **Send Digest** - Posts top picks to Slack or email\n\n### Setup Steps\n\n1. Import workflow into n8n\n2. Configure credentials:\n   - **Anthropic API** - Claude AI for investment scoring\n   - **Google Sheets** - Results & historical tracking\n   - **Slack OAuth** - Daily digest notifications\n   - **RapidAPI / Zillow / Domain API** - Property market data\n3. Set your target listing URLs in the 'Configure Scrape Targets' node\n4. Set your score threshold (default: 65) in 'Filter Top Picks'\n5. Set your Google Sheet ID and Slack channel\n6. Activate the workflow or POST to the webhook\n\n### Sample Webhook Payload\n```json\n{\n  \"searchUrl\": \"https://www.domain.com.au/sale/sydney/?bedrooms=2-4&price=500000-900000\",\n  \"suburb\": \"Parramatta\",\n  \"maxListings\": 20,\n  \"scoreThreshold\": 65\n}\n```\n\n### Scoring Criteria (Claude AI)\n- **Rental Yield** - Gross and estimated net yield vs suburb average\n- **Capital Growth** - 5-year suburb price trend\n- **Location Score** - Transport, schools, amenities proximity\n- **Vacancy Risk** - Suburb rental demand and vacancy rate\n- **Cash Flow** - Estimated weekly cash flow after mortgage\n- **Risk Flags** - Flood zones, high crime, oversupply signals\n\n### Features\n- Multi-source market enrichment\n- AI-powered investment scoring with SWOT analysis\n- Automated filtering of top-performing listings\n- Google Sheets audit trail with historical scores\n- Slack/email digest of daily top picks"
      },
      "typeVersion": 1
    },
    {
      "id": "78f775f5-5e9e-4fe4-b1ba-3d1f46e11091",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1024,
        416
      ],
      "parameters": {
        "color": 3,
        "width": 592,
        "height": 388,
        "content": "## 1. Trigger & Listing Scrape"
      },
      "typeVersion": 1
    },
    {
      "id": "007bf920-b1b4-4858-a20f-752de51c95b4",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1680,
        288
      ],
      "parameters": {
        "color": 3,
        "width": 836,
        "height": 700,
        "content": "## 2. Market Data Enrichment"
      },
      "typeVersion": 1
    },
    {
      "id": "11a96bd7-1c0e-4dff-aafc-91957cacd7d5",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2560,
        320
      ],
      "parameters": {
        "color": 3,
        "width": 536,
        "height": 684,
        "content": "## 3. AI Investment Scoring (Claude)"
      },
      "typeVersion": 1
    },
    {
      "id": "f71796da-bea6-411a-b7e1-e0bac9158b09",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3136,
        384
      ],
      "parameters": {
        "color": 3,
        "width": 932,
        "height": 568,
        "content": "## 4. Filter, Report & Notify"
      },
      "typeVersion": 1
    },
    {
      "id": "7a231ce5-994d-41d0-8788-985633ce4b3f",
      "name": "Daily Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        1056,
        640
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 7 * * 1-5"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "23d1c7df-f97b-4d4c-a8fd-916b526b2589",
      "name": "Configure Scrape Targets",
      "type": "n8n-nodes-base.set",
      "position": [
        1280,
        640
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "target-url",
              "name": "searchUrl",
              "type": "string",
              "value": "={{ $json.body?.searchUrl || 'https://www.domain.com.au/sale/sydney/?bedrooms=2-4&price=500000-900000' }}"
            },
            {
              "id": "target-suburb",
              "name": "suburb",
              "type": "string",
              "value": "={{ $json.body?.suburb || 'Sydney' }}"
            },
            {
              "id": "max-listings",
              "name": "maxListings",
              "type": "number",
              "value": "={{ $json.body?.maxListings || 15 }}"
            },
            {
              "id": "score-threshold",
              "name": "scoreThreshold",
              "type": "number",
              "value": "={{ $json.body?.scoreThreshold || 65 }}"
            },
            {
              "id": "job-id",
              "name": "jobId",
              "type": "string",
              "value": "=PROP-{{ Date.now() }}-{{ Math.random().toString(36).substr(2, 6).toUpperCase() }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "5fa93b7e-ae35-4845-a587-0c42faa8ae17",
      "name": "Scrape Property Listings",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1504,
        640
      ],
      "parameters": {
        "url": "={{ $json.searchUrl }}",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
            },
            {
              "name": "Accept",
              "value": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
            },
            {
              "name": "Accept-Language",
              "value": "en-AU,en;q=0.5"
            }
          ]
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "7f1f60b1-48ad-49c9-93cd-69dc91e3ece9",
      "name": "Parse Listing Data",
      "type": "n8n-nodes-base.code",
      "position": [
        1728,
        640
      ],
      "parameters": {
        "jsCode": "// Parse scraped HTML and extract structured listing data\n// In production, adapt selectors to your target site (Domain, REA, Zillow, Rightmove, etc.)\n\nconst config = $('Configure Scrape Targets').first().json;\nconst htmlText = $input.first().json.data || '';\n\n// Simple regex-based extraction (replace with Cheerio/puppeteer for production)\n// This simulates extracting listings from JSON-LD or data attributes\nconst listings = [];\n\n// Try to find JSON-LD structured data (many property sites use this)\nconst jsonLdMatches = htmlText.match(/<script type=\"application\\/ld\\+json\">[\\s\\S]*?<\\/script>/gi) || [];\nfor (const match of jsonLdMatches) {\n  try {\n    const jsonContent = match.replace(/<script[^>]*>/i, '').replace(/<\\/script>/i, '').trim();\n    const data = JSON.parse(jsonContent);\n    if (data['@type'] === 'RealEstateListing' || data['@type'] === 'Residence') {\n      listings.push({\n        listingId: `LST-${Math.random().toString(36).substr(2, 8).toUpperCase()}`,\n        address: data.name || data.address?.streetAddress || 'Unknown',\n        suburb: data.address?.addressLocality || config.suburb,\n        state: data.address?.addressRegion || 'NSW',\n        postcode: data.address?.postalCode || '2000',\n        price: parseFloat((data.offers?.price || '0').toString().replace(/[^0-9.]/g, '')) || null,\n        priceText: data.offers?.price || 'Price on application',\n        bedrooms: parseInt(data.numberOfRooms) || null,\n        bathrooms: parseInt(data.numberOfBathroomsTotal) || null,\n        carSpaces: parseInt(data.numberOfParkingSpaces) || null,\n        propertyType: data['@type'] || 'House',\n        landSize: data.floorSize?.value || null,\n        description: (data.description || '').substring(0, 500),\n        listingUrl: data.url || config.searchUrl,\n        imageUrl: data.image?.[0] || null,\n        agent: data.agent?.name || 'Unknown Agent',\n        listedAt: data.datePosted || new Date().toISOString()\n      });\n    }\n  } catch (e) { /* skip malformed JSON-LD */ }\n}\n\n// Fallback: generate synthetic listings for demonstration if no structured data found\nif (listings.length === 0) {\n  const suburbs = [config.suburb, 'Parramatta', 'Blacktown', 'Liverpool', 'Penrith'];\n  const types = ['House', 'Unit', 'Townhouse', 'Villa'];\n  const count = Math.min(config.maxListings || 10, 12);\n  for (let i = 0; i < count; i++) {\n    const suburb = suburbs[i % suburbs.length];\n    const type = types[i % types.length];\n    const beds = Math.floor(Math.random() * 3) + 2;\n    const basePrice = type === 'Unit' ? 550000 : type === 'House' ? 850000 : 700000;\n    const price = basePrice + Math.floor(Math.random() * 300000);\n    listings.push({\n      listingId: `LST-${Math.random().toString(36).substr(2, 8).toUpperCase()}`,\n      address: `${Math.floor(Math.random() * 99) + 1} Sample Street`,\n      suburb,\n      state: 'NSW',\n      postcode: `2${150 + i}`,\n      price,\n      priceText: `$${price.toLocaleString()}`,\n      bedrooms: beds,\n      bathrooms: beds - 1 || 1,\n      carSpaces: type === 'Unit' ? 1 : 2,\n      propertyType: type,\n      landSize: type === 'House' ? Math.floor(Math.random() * 400) + 300 : null,\n      description: `${beds} bedroom ${type.toLowerCase()} in ${suburb}. Close to transport, schools and shops.`,\n      listingUrl: `${config.searchUrl}/${i + 1}`,\n      imageUrl: null,\n      agent: 'Demo Real Estate',\n      listedAt: new Date().toISOString()\n    });\n  }\n}\n\n// Attach job metadata\nconst jobConfig = config;\nreturn listings.slice(0, config.maxListings || 15).map(l => ({\n  json: { listing: l, jobConfig }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "7c130683-6e3c-41f5-8f76-4ad2b8e039c5",
      "name": "Fetch Suburb Market Data",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1952,
        448
      ],
      "parameters": {
        "url": "=https://api.rapidapi.com/realty/suburb-stats",
        "options": {
          "timeout": 10000
        },
        "sendQuery": true,
        "sendHeaders": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "suburb",
              "value": "={{ $json.listing.suburb }}"
            },
            {
              "name": "state",
              "value": "={{ $json.listing.state }}"
            },
            {
              "name": "propertyType",
              "value": "={{ $json.listing.propertyType }}"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "X-RapidAPI-Key",
              "value": "YOUR_RAPIDAPI_KEY_HERE"
            },
            {
              "name": "X-RapidAPI-Host",
              "value": "realty-in-au.p.rapidapi.com"
            }
          ]
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "2766c8d8-0b8c-45fc-a34b-38300a33e608",
      "name": "Fetch Area Demographics",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1952,
        640
      ],
      "parameters": {
        "url": "=https://api.abs.gov.au/opendata/v1/suburb/{{ $json.listing.postcode }}",
        "options": {
          "timeout": 10000
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "metrics",
              "value": "population_growth,median_income,unemployment_rate"
            }
          ]
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "fad9fbea-5b28-4be6-8e46-d9c269183250",
      "name": "Fetch Rental Yield Data",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1952,
        832
      ],
      "parameters": {
        "url": "=https://api.sqmresearch.com.au/vacancy-rates",
        "options": {
          "timeout": 10000
        },
        "sendQuery": true,
        "sendHeaders": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "postcode",
              "value": "={{ $json.listing.postcode }}"
            },
            {
              "name": "bedrooms",
              "value": "={{ $json.listing.bedrooms }}"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            }
          ]
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "72afbf9b-ffe8-4163-be90-1496ada8c443",
      "name": "Merge Enrichment Sources",
      "type": "n8n-nodes-base.merge",
      "position": [
        2176,
        640
      ],
      "parameters": {
        "mode": "mergeByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "8249e953-55be-41bd-8899-d3067f1f9159",
      "name": "Combine All Property Data",
      "type": "n8n-nodes-base.code",
      "position": [
        2400,
        640
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const listing = $('Parse Listing Data').item.json.listing;\nconst jobConfig = $('Parse Listing Data').item.json.jobConfig;\n\nconst marketRaw = $('Fetch Suburb Market Data').item.json;\nconst demoRaw = $('Fetch Area Demographics').item.json;\nconst rentalRaw = $('Fetch Rental Yield Data').item.json;\n\n// Parse market data (adapt to actual API response shape)\nlet marketData = {\n  suburbMedianPrice: null,\n  medianPriceGrowth1yr: null,\n  medianPriceGrowth5yr: null,\n  suburbMedianRent: null,\n  grossRentalYield: null,\n  daysOnMarket: null,\n  clearanceRate: null\n};\ntry {\n  if (marketRaw && !marketRaw.error) {\n    const d = marketRaw.data || marketRaw;\n    marketData = {\n      suburbMedianPrice: d.medianSalePrice || d.median_price || null,\n      medianPriceGrowth1yr: d.priceGrowth1yr || d.growth_1yr || null,\n      medianPriceGrowth5yr: d.priceGrowth5yr || d.growth_5yr || null,\n      suburbMedianRent: d.medianWeeklyRent || d.median_rent || null,\n      grossRentalYield: d.grossRentalYield || d.yield || null,\n      daysOnMarket: d.medianDaysOnMarket || d.days_on_market || null,\n      clearanceRate: d.auctionClearanceRate || null\n    };\n  } else {\n    // Synthetic fallback for demo\n    const syntheticYield = (2.8 + Math.random() * 3).toFixed(2);\n    marketData = {\n      suburbMedianPrice: listing.price ? Math.round(listing.price * (0.9 + Math.random() * 0.2)) : 750000,\n      medianPriceGrowth1yr: parseFloat((Math.random() * 10 - 2).toFixed(1)),\n      medianPriceGrowth5yr: parseFloat((Math.random() * 40 + 5).toFixed(1)),\n      suburbMedianRent: Math.round((listing.price * parseFloat(syntheticYield) / 100) / 52),\n      grossRentalYield: parseFloat(syntheticYield),\n      daysOnMarket: Math.floor(Math.random() * 45) + 15,\n      clearanceRate: parseFloat((55 + Math.random() * 30).toFixed(1))\n    };\n  }\n} catch (e) { console.log('Market parse error:', e.message); }\n\n// Parse demographics\nlet demographics = {\n  populationGrowth: null,\n  medianHouseholdIncome: null,\n  unemploymentRate: null\n};\ntry {\n  if (demoRaw && !demoRaw.error) {\n    const d = demoRaw.data || demoRaw;\n    demographics = {\n      populationGrowth: d.populationGrowth || d.pop_growth || null,\n      medianHouseholdIncome: d.medianIncome || d.median_income || null,\n      unemploymentRate: d.unemploymentRate || d.unemployment || null\n    };\n  } else {\n    demographics = {\n      populationGrowth: parseFloat((0.5 + Math.random() * 2.5).toFixed(1)),\n      medianHouseholdIncome: Math.round(70000 + Math.random() * 60000),\n      unemploymentRate: parseFloat((3 + Math.random() * 5).toFixed(1))\n    };\n  }\n} catch (e) { console.log('Demo parse error:', e.message); }\n\n// Parse rental / vacancy\nlet rentalMetrics = {\n  vacancyRate: null,\n  rentalDemandScore: null,\n  avgWeeklyRent: null\n};\ntry {\n  if (rentalRaw && !rentalRaw.error) {\n    const d = rentalRaw.data || rentalRaw;\n    rentalMetrics = {\n      vacancyRate: d.vacancyRate || d.vacancy_rate || null,\n      rentalDemandScore: d.demandScore || null,\n      avgWeeklyRent: d.avgWeeklyRent || d.avg_rent || null\n    };\n  } else {\n    const vacancy = parseFloat((0.5 + Math.random() * 4).toFixed(2));\n    const weeklyRent = Math.round(350 + Math.random() * 500);\n    rentalMetrics = {\n      vacancyRate: vacancy,\n      rentalDemandScore: Math.round(Math.max(0, Math.min(100, 100 - vacancy * 15))),\n      avgWeeklyRent: weeklyRent\n    };\n  }\n} catch (e) { console.log('Rental parse error:', e.message); }\n\n// Compute estimated financials\nconst purchasePrice = listing.price || marketData.suburbMedianPrice || 750000;\nconst weeklyRent = rentalMetrics.avgWeeklyRent || marketData.suburbMedianRent || 500;\nconst annualRent = weeklyRent * 52;\nconst grossYield = marketData.grossRentalYield || parseFloat(((annualRent / purchasePrice) * 100).toFixed(2));\nconst expenses = annualRent * 0.28; // strata, PM, rates, insurance ~28%\nconst netAnnualIncome = annualRent - expenses;\nconst netYield = parseFloat(((netAnnualIncome / purchasePrice) * 100).toFixed(2));\nconst interestRate = 6.5; // current avg mortgage rate\nconst loanAmount = purchasePrice * 0.8;\nconst annualInterest = loanAmount * (interestRate / 100);\nconst weeklyInterest = annualInterest / 52;\nconst weeklyNetCashFlow = weeklyRent - weeklyInterest - (expenses / 52);\n\nreturn {\n  json: {\n    listing,\n    jobConfig,\n    marketData,\n    demographics,\n    rentalMetrics,\n    financials: {\n      purchasePrice,\n      weeklyRent,\n      annualRent,\n      grossYield,\n      netYield,\n      estimatedWeeklyCashFlow: parseFloat(weeklyNetCashFlow.toFixed(2)),\n      assumedLVR: 80,\n      assumedInterestRate: interestRate,\n      enrichedAt: new Date().toISOString()\n    }\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "7ffb1a3a-4703-4ebe-93c4-500d6bacb679",
      "name": "Score Investment with Claude AI",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2624,
        640
      ],
      "parameters": {
        "text": "=You are a senior property investment analyst with 20+ years of experience in Australian and global real estate markets. Analyse this property listing and provide a structured investment score with detailed rationale.\n\n**Property Listing:**\n- Listing ID: {{ $json.listing.listingId }}\n- Address: {{ $json.listing.address }}, {{ $json.listing.suburb }}, {{ $json.listing.state }} {{ $json.listing.postcode }}\n- Type: {{ $json.listing.propertyType }}\n- Bedrooms: {{ $json.listing.bedrooms }} | Bathrooms: {{ $json.listing.bathrooms }} | Car Spaces: {{ $json.listing.carSpaces }}\n- Asking Price: {{ $json.listing.priceText }}\n- Land Size: {{ $json.listing.landSize ? $json.listing.landSize + ' sqm' : 'N/A (unit/apartment)' }}\n- Description: {{ $json.listing.description }}\n- Listed: {{ $json.listing.listedAt }}\n\n**Market Data ({{ $json.listing.suburb }}):**\n- Suburb Median Price: ${{ $json.marketData.suburbMedianPrice?.toLocaleString() || 'N/A' }}\n- 1-Year Price Growth: {{ $json.marketData.medianPriceGrowth1yr }}%\n- 5-Year Price Growth: {{ $json.marketData.medianPriceGrowth5yr }}%\n- Median Weekly Rent: ${{ $json.marketData.suburbMedianRent || 'N/A' }}\n- Gross Rental Yield (suburb avg): {{ $json.marketData.grossRentalYield }}%\n- Days on Market: {{ $json.marketData.daysOnMarket }}\n- Auction Clearance Rate: {{ $json.marketData.clearanceRate }}%\n\n**Demographics:**\n- Population Growth: {{ $json.demographics.populationGrowth }}% p.a.\n- Median Household Income: ${{ $json.demographics.medianHouseholdIncome?.toLocaleString() || 'N/A' }}\n- Unemployment Rate: {{ $json.demographics.unemploymentRate }}%\n\n**Rental & Vacancy:**\n- Vacancy Rate: {{ $json.rentalMetrics.vacancyRate }}%\n- Rental Demand Score: {{ $json.rentalMetrics.rentalDemandScore }}/100\n- Average Weekly Rent (similar properties): ${{ $json.rentalMetrics.avgWeeklyRent }}\n\n**Estimated Financials (80% LVR, {{ $json.financials.assumedInterestRate }}% interest):**\n- Gross Yield: {{ $json.financials.grossYield }}%\n- Net Yield (after 28% expenses): {{ $json.financials.netYield }}%\n- Estimated Weekly Cash Flow: ${{ $json.financials.estimatedWeeklyCashFlow }} (positive = cash flow positive)\n\n**Scoring Guidelines:**\n- 80\u2013100: Exceptional \u2014 strong yield, capital growth, low vacancy, below-median entry price\n- 65\u201379: Good \u2014 solid fundamentals, worth pursuing with due diligence\n- 50\u201364: Moderate \u2014 some merit but notable risks or trade-offs\n- 35\u201349: Weak \u2014 significant risks outweigh potential returns\n- 0\u201334: Poor \u2014 do not recommend\n\n**Response Format (JSON only, no markdown):**\n{\n  \"investmentScore\": 72,\n  \"scoreBreakdown\": {\n    \"rentalYieldScore\": 18,\n    \"capitalGrowthScore\": 20,\n    \"locationScore\": 15,\n    \"vacancyRiskScore\": 12,\n    \"cashFlowScore\": 5,\n    \"valueForMoneyScore\": 2\n  },\n  \"rating\": \"GOOD | EXCELLENT | MODERATE | WEAK | POOR\",\n  \"recommendation\": \"BUY | CONSIDER | AVOID | FURTHER_RESEARCH\",\n  \"confidence\": \"HIGH | MEDIUM | LOW\",\n  \"summary\": \"2-3 sentence plain-English investment summary\",\n  \"strengths\": [\"key strength 1\", \"key strength 2\"],\n  \"weaknesses\": [\"key weakness 1\", \"key weakness 2\"],\n  \"riskFlags\": [\"specific risk 1 if any\"],\n  \"opportunities\": [\"upside opportunity 1\"],\n  \"estimatedFairValue\": 820000,\n  \"priceVsFairValue\": \"UNDERPRICED | FAIRLY_PRICED | OVERPRICED\",\n  \"suggestedOfferPrice\": 785000,\n  \"projectedGrossYield\": 4.2,\n  \"projectedNetYield\": 3.0,\n  \"capitalGrowthOutlook\": \"STRONG | MODERATE | WEAK | NEGATIVE\",\n  \"idealBuyerProfile\": \"e.g. long-term investor seeking yield + growth\",\n  \"dueDiligenceItems\": [\"strata report\", \"building inspection\"],\n  \"comparableSuburbs\": [\"similar suburb 1\", \"similar suburb 2\"],\n  \"holdPeriodRecommendation\": \"7-10 years for optimal capital growth\"\n}",
        "options": {
          "systemMessage": "You are a certified property investment analyst. Return JSON only \u2014 no markdown, no code blocks, no additional text. All scores must be numeric. Be objective and data-driven. Do not recommend properties with vacancy rates above 5% or gross yields below 3% without strong capital growth evidence."
        },
        "promptType": "define"
      },
      "typeVersion": 1.6
    },
    {
      "id": "af61f4ea-0ec6-4656-8636-8f01f8971a37",
      "name": "Claude AI Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        2696,
        864
      ],
      "parameters": {
        "model": "=claude-sonnet-4-20250514",
        "options": {
          "temperature": 0.15
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "2f1e30cd-90e4-414d-a51d-04f382294920",
      "name": "Parse AI Investment Score",
      "type": "n8n-nodes-base.code",
      "position": [
        2976,
        640
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const aiResponse = $input.item.json;\nlet aiText = aiResponse.response || aiResponse.output || aiResponse.text || '';\n\nif (aiResponse.content && Array.isArray(aiResponse.content)) {\n  aiText = aiResponse.content[0]?.text || '';\n}\n\nconst cleanText = aiText\n  .replace(/```json\\s*/g, '')\n  .replace(/```\\s*/g, '')\n  .trim();\n\nlet aiScore;\ntry {\n  aiScore = JSON.parse(cleanText);\n} catch (error) {\n  // Attempt extraction of JSON block\n  const match = cleanText.match(/\\{[\\s\\S]*\\}/);\n  if (match) {\n    aiScore = JSON.parse(match[0]);\n  } else {\n    throw new Error(`Failed to parse Claude response: ${error.message}. Raw: ${cleanText.substring(0, 300)}`);\n  }\n}\n\nconst enriched = $('Combine All Property Data').item.json;\n\nreturn {\n  json: {\n    listingId: enriched.listing.listingId,\n    address: `${enriched.listing.address}, ${enriched.listing.suburb} ${enriched.listing.state}`,\n    suburb: enriched.listing.suburb,\n    propertyType: enriched.listing.propertyType,\n    askingPrice: enriched.listing.price,\n    priceText: enriched.listing.priceText,\n    bedrooms: enriched.listing.bedrooms,\n    bathrooms: enriched.listing.bathrooms,\n    listingUrl: enriched.listing.listingUrl,\n    financials: enriched.financials,\n    marketData: enriched.marketData,\n    demographics: enriched.demographics,\n    rentalMetrics: enriched.rentalMetrics,\n    aiScore,\n    jobConfig: enriched.jobConfig,\n    scoredAt: new Date().toISOString()\n  }\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "5eeb493b-dd17-44e7-9b87-619b1d86b470",
      "name": "Filter Top Picks by Score",
      "type": "n8n-nodes-base.filter",
      "position": [
        3200,
        544
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $json.aiScore.investmentScore }}",
              "rightValue": "={{ $json.jobConfig.scoreThreshold || 65 }}"
            },
            {
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.aiScore.recommendation }}",
              "rightValue": "AVOID"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "db1af5ae-0fbf-492b-9e18-cbe8357402b6",
      "name": "Format Investment Report",
      "type": "n8n-nodes-base.code",
      "position": [
        3424,
        544
      ],
      "parameters": {
        "jsCode": "// Build a summary report from all top picks\nconst items = $input.all();\nconst jobConfig = items[0]?.json.jobConfig || {};\n\nconst topPicks = items.map(item => {\n  const d = item.json;\n  const score = d.aiScore;\n  return {\n    rank: 0, // filled below\n    listingId: d.listingId,\n    address: d.address,\n    propertyType: d.propertyType,\n    bedrooms: d.bedrooms,\n    askingPrice: d.priceText,\n    investmentScore: score.investmentScore,\n    rating: score.rating,\n    recommendation: score.recommendation,\n    projectedGrossYield: score.projectedGrossYield,\n    projectedNetYield: score.projectedNetYield,\n    estimatedWeeklyCashFlow: d.financials.estimatedWeeklyCashFlow,\n    capitalGrowthOutlook: score.capitalGrowthOutlook,\n    priceVsFairValue: score.priceVsFairValue,\n    suggestedOfferPrice: score.suggestedOfferPrice,\n    summary: score.summary,\n    strengths: score.strengths,\n    weaknesses: score.weaknesses,\n    riskFlags: score.riskFlags,\n    holdPeriodRecommendation: score.holdPeriodRecommendation,\n    listingUrl: d.listingUrl,\n    scoredAt: d.scoredAt\n  };\n}).sort((a, b) => b.investmentScore - a.investmentScore)\n  .map((p, i) => ({ ...p, rank: i + 1 }));\n\nconst slackBlocks = [\n  {\n    type: 'header',\n    text: { type: 'plain_text', text: `\ud83c\udfe0 Daily Property Investment Report \u2014 ${new Date().toLocaleDateString('en-AU')}` }\n  },\n  {\n    type: 'section',\n    text: {\n      type: 'mrkdwn',\n      text: `*${topPicks.length} top pick(s)* above score threshold of *${jobConfig.scoreThreshold || 65}/100*`\n    }\n  },\n  { type: 'divider' }\n];\n\nfor (const p of topPicks.slice(0, 5)) {\n  slackBlocks.push({\n    type: 'section',\n    fields: [\n      { type: 'mrkdwn', text: `*#${p.rank} \u2014 ${p.address}*` },\n      { type: 'mrkdwn', text: `*Score:* ${p.investmentScore}/100 (${p.rating})` },\n      { type: 'mrkdwn', text: `*Price:* ${p.askingPrice}` },\n      { type: 'mrkdwn', text: `*Yield:* ${p.projectedGrossYield}% gross / ${p.projectedNetYield}% net` },\n      { type: 'mrkdwn', text: `*Cash Flow:* $${p.estimatedWeeklyCashFlow}/wk` },\n      { type: 'mrkdwn', text: `*Capital Growth:* ${p.capitalGrowthOutlook}` },\n      { type: 'mrkdwn', text: `*Pricing:* ${p.priceVsFairValue} (offer: $${p.suggestedOfferPrice?.toLocaleString()})` },\n      { type: 'mrkdwn', text: `*Recommendation:* ${p.recommendation}` }\n    ]\n  });\n  slackBlocks.push({\n    type: 'section',\n    text: { type: 'mrkdwn', text: `_${p.summary}_\\n<${p.listingUrl}|View Listing>` }\n  });\n  slackBlocks.push({ type: 'divider' });\n}\n\nreturn [{\n  json: {\n    reportDate: new Date().toISOString(),\n    jobId: jobConfig.jobId,\n    suburb: jobConfig.suburb,\n    totalAnalysed: items.length,\n    topPicksCount: topPicks.length,\n    scoreThreshold: jobConfig.scoreThreshold || 65,\n    topPicks,\n    slackBlocks,\n    generatedAt: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "cc25a893-5805-4fd7-8cf3-d8f005a33d33",
      "name": "Save All Scored Listings to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3200,
        736
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "=YOUR_SHEET_TAB_ID"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "=YOUR_GOOGLE_SHEET_ID"
        },
        "authentication": "serviceAccount"
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5,
      "continueOnFail": true
    },
    {
      "id": "65c850d2-2df7-49ab-9407-4e8f4b676bf6",
      "name": "Send Top Picks Digest to Slack",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3648,
        544
      ],
      "parameters": {
        "url": "https://slack.com/api/chat.postMessage",
        "method": "POST",
        "options": {
          "timeout": 10000
        },
        "jsonBody": "={\n  \"channel\": \"#property-investment\",\n  \"text\": \"\ud83c\udfe0 Daily Property Investment Report \u2014 {{ new Date().toLocaleDateString('en-AU') }}\",\n  \"blocks\": {{ JSON.stringify($json.slackBlocks) }}\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "slackApi"
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "6d736631-cfa3-4ace-b57a-c53e3f048e92",
      "name": "Return Final Report",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3872,
        544
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              }
            ]
          }
        },
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json, null, 2) }}"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "684a06eb-ca31-4794-b66c-aaf2f563500a",
  "connections": {
    "Claude AI Model": {
      "ai_languageModel": [
        [
          {
            "node": "Score Investment with Claude AI",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Parse Listing Data": {
      "main": [
        [
          {
            "node": "Fetch Suburb Market Data",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Area Demographics",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Rental Yield Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily Schedule Trigger": {
      "main": [
        [
          {
            "node": "Configure Scrape Targets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Area Demographics": {
      "main": [
        [
          {
            "node": "Merge Enrichment Sources",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Fetch Rental Yield Data": {
      "main": [
        [
          {
            "node": "Merge Enrichment Sources",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Configure Scrape Targets": {
      "main": [
        [
          {
            "node": "Scrape Property Listings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Suburb Market Data": {
      "main": [
        [
          {
            "node": "Merge Enrichment Sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Investment Report": {
      "main": [
        [
          {
            "node": "Send Top Picks Digest to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Enrichment Sources": {
      "main": [
        [
          {
            "node": "Combine All Property Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Property Listings": {
      "main": [
        [
          {
            "node": "Parse Listing Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine All Property Data": {
      "main": [
        [
          {
            "node": "Score Investment with Claude AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Top Picks by Score": {
      "main": [
        [
          {
            "node": "Format Investment Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI Investment Score": {
      "main": [
        [
          {
            "node": "Filter Top Picks by Score",
            "type": "main",
            "index": 0
          },
          {
            "node": "Save All Scored Listings to Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Top Picks Digest to Slack": {
      "main": [
        [
          {
            "node": "Return Final Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Investment with Claude AI": {
      "main": [
        [
          {
            "node": "Parse AI Investment Score",
            "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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow scrapes property listings, enriches them with market data, and uses Claude AI to score each listing's investment potential based on rental yield, capital growth trends, location desirability, and risk factors. Trigger - Scheduled run initiates a scrape job Scrape…

Source: https://n8n.io/workflows/13686/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

The Multi-Model Agency Content Engine is a high-performance editorial system designed for agencies. It solves the "blank page" problem by alternating between real-world social proof and strategic expe

Google Sheets, Gmail, Google Drive +6
AI & RAG

📺 Full walkthrough video: https://youtu.be/03mZE9tvELU

HTTP Request, Google Sheets, Agent +2
AI & RAG

ASMR. Uses googleSheets, outputParserStructured, httpRequest, lmChatOpenAi. Scheduled trigger; 35 nodes.

Google Sheets, Output Parser Structured, HTTP Request +5
AI & RAG

**Content engine that ships fresh, SEO-ready articles every single day. **

Google Sheets, Slack, Webflow +7
AI & RAG

If you teach on Udemy at any meaningful scale, you already know the problem: 80% of student messages are variations of the same handful of questions, but every one of them needs a thoughtful reply to

N8N Nodes Globals, HTTP Request, Google Sheets +8