DEV Community

Alex Kane
Alex Kane

Posted on

n8n for PropTech & Real Estate Tech: 5 Automations That Scale Property Operations (Free Workflow JSON)

If you work at a PropTech company or manage real estate operations at scale, you already know the problem: listings data flooding in from dozens of sources, tenant applications arriving faster than your team can screen them, maintenance requests going unanswered, and investors expecting monthly reports that take hours to assemble manually.

n8n changes the economics of all of this. It's open-source, self-hostable (your tenant PII and lease data stay on your servers), and has native integrations with the tools PropTech teams actually use — Google Sheets, Gmail, Slack, webhooks from any platform, PostgreSQL, and HTTP calls to MLS or property management APIs.

Here are 5 production-ready workflows with import-ready JSON.


1. New Listing Alert & Prospect Notification Pipeline

What it does: Polls your MLS/listing API every hour, filters new listings by your criteria (price, beds, location), logs them to Sheets, and Slacks your team instantly.

Why it matters: By the time a qualified listing appears and your analyst manually checks the feed, your competitors already have it. This workflow catches listings within 60 minutes of going live.

{
  "nodes": [
    {"id":"1","name":"Every Hour","type":"n8n-nodes-base.scheduleTrigger","parameters":{"rule":{"interval":[{"field":"hours","minutesInterval":1}]}},"position":[200,300]},
    {"id":"2","name":"Fetch New Listings","type":"n8n-nodes-base.httpRequest","parameters":{"url":"https://api.mls-provider.com/listings","qs":{"lastUpdated":"={{$now.minus({hours:1}).toISO()}}","status":"active"}},"position":[400,300]},
    {"id":"3","name":"Filter by Criteria","type":"n8n-nodes-base.code","parameters":{"jsCode":"const listings = $input.all();\nconst qualified = listings.filter(item => {\n  const l = item.json;\n  return l.price <= 500000 && l.bedrooms >= 3 && l.city === 'Austin';\n});\nreturn qualified.map(item => ({json: item.json}));"},"position":[600,300]},
    {"id":"4","name":"Log to Sheets","type":"n8n-nodes-base.googleSheets","parameters":{"operation":"appendOrUpdate","documentId":"YOUR_SHEET_ID","sheetName":"Listings"},"position":[800,300]},
    {"id":"5","name":"Slack Alert","type":"n8n-nodes-base.slack","parameters":{"text":"=New listing: {{$json.address}} | ${{$json.price}} | {{$json.bedrooms}}BR | {{$json.link}}","channel":"#listings-alerts"},"position":[1000,300]}
  ],
  "connections":{"Every Hour":{"main":[["Fetch New Listings"]]},"Fetch New Listings":{"main":[["Filter by Criteria"]]},"Filter by Criteria":{"main":[["Log to Sheets"]]},"Log to Sheets":{"main":[["Slack Alert"]]}}
}
Enter fullscreen mode Exit fullscreen mode

Customise: Change the filter criteria in the Code node for your market. Connect to any listing API or parse an MLS RETS feed via HTTP.


2. Tenant Application Auto-Screening

What it does: Webhook receives application form submissions, runs a scoring algorithm (income-to-rent ratio, employment type, eviction history), routes to leasing team for review or flags for fast-track approval, and emails the applicant immediately.

Why it matters: Leasing teams waste hours manually reviewing applications that clearly don't qualify. This pre-screens instantly and only surfaces the borderline cases.

{
  "nodes": [
    {"id":"1","name":"New Application","type":"n8n-nodes-base.webhook","parameters":{"path":"tenant-application","responseMode":"responseNode"},"position":[200,300]},
    {"id":"2","name":"Acknowledge","type":"n8n-nodes-base.respondToWebhook","parameters":{"respondWith":"json","responseBody":"{"status":"received","message":"Application received. We'll review within 2 business days."}"},"position":[400,180]},
    {"id":"3","name":"Score Applicant","type":"n8n-nodes-base.code","parameters":{"jsCode":"const app = $input.first().json;\nconst income_ok = app.annual_income >= app.monthly_rent * 12 * 3;\nconst score = {\n  income: income_ok ? 40 : 0,\n  employment: app.employment_type === 'full-time' ? 30 : 15,\n  history: app.prior_evictions === 0 ? 30 : -20\n};\nconst total = Object.values(score).reduce((a, b) => a + b, 0);\nconst tier = total >= 80 ? 'APPROVED' : total >= 60 ? 'REVIEW' : 'DECLINED';\nreturn [{json: {...app, score: total, tier}}];"},"position":[400,420]},
    {"id":"4","name":"Route by Tier","type":"n8n-nodes-base.switch","parameters":{"dataType":"string","value1":"={{$json.tier}}","rules":{"rules":[{"value1":"REVIEW"},{"value1":"APPROVED"}]}},"position":[600,420]},
    {"id":"5","name":"Slack Leasing Team","type":"n8n-nodes-base.slack","parameters":{"text":"=Tenant review needed: {{$json.applicant_name}} | Score: {{$json.score}}/100 | Property: {{$json.property_address}} | Income ratio: {{($json.annual_income / ($json.monthly_rent * 12)).toFixed(1)}}x","channel":"#leasing-team"},"position":[800,300]},
    {"id":"6","name":"Email Applicant","type":"n8n-nodes-base.gmail","parameters":{"toList":"={{$json.applicant_email}}","subject":"Your rental application has been received","message":"=Hi {{$json.applicant_name}},\n\nThank you for applying for {{$json.property_address}}. We've received your application and will be in touch within 2 business days."},"position":[800,540]}
  ],
  "connections":{"New Application":{"main":[["Acknowledge","Score Applicant"]]},"Score Applicant":{"main":[["Route by Tier"]]},"Route by Tier":{"main":[["Slack Leasing Team"],["Email Applicant"]]}}
}
Enter fullscreen mode Exit fullscreen mode

Customise: Adjust the scoring weights and thresholds in the Code node to match your qualification criteria. Add a Postgres upsert to log every application regardless of outcome.


3. Maintenance Request Triage & Vendor Assignment

What it does: Webhook receives maintenance requests from tenants, classifies severity (EMERGENCY through LOW) using keyword matching, alerts the maintenance Slack channel, acknowledges the tenant via email, and logs to Sheets for tracking.

Why it matters: The difference between an emergency leak and a cosmetic paint touch-up is response time. This ensures EMERGENCY tickets surface immediately while LOW-priority requests don't create noise.

{
  "nodes": [
    {"id":"1","name":"New Request","type":"n8n-nodes-base.webhook","parameters":{"path":"maintenance-request","responseMode":"responseNode"},"position":[200,300]},
    {"id":"2","name":"Acknowledge","type":"n8n-nodes-base.respondToWebhook","parameters":{"respondWith":"json","responseBody":"{"status":"received"}"},"position":[400,180]},
    {"id":"3","name":"Classify Severity","type":"n8n-nodes-base.code","parameters":{"jsCode":"const req = $input.first().json;\nconst desc = (req.description || '').toLowerCase();\nlet severity, sla_hours;\nif (/leak|flood|fire|no heat|no power|gas|emergency/.test(desc)) {\n  severity = 'EMERGENCY'; sla_hours = 2;\n} else if (/broken|not working|hvac|plumbing|electrical/.test(desc)) {\n  severity = 'HIGH'; sla_hours = 24;\n} else if (/cosmetic|paint|minor|squeaky/.test(desc)) {\n  severity = 'LOW'; sla_hours = 168;\n} else {\n  severity = 'MEDIUM'; sla_hours = 72;\n}\nreturn [{json: {...req, severity, sla_hours, received_at: new Date().toISOString()}}];"},"position":[400,420]},
    {"id":"4","name":"Slack Maintenance","type":"n8n-nodes-base.slack","parameters":{"text":"=[{{$json.severity}}] Maintenance request | Unit: {{$json.unit}} | {{$json.description}} | Tenant: {{$json.tenant_name}} | SLA: {{$json.sla_hours}}h","channel":"#maintenance"},"position":[600,300]},
    {"id":"5","name":"Email Tenant","type":"n8n-nodes-base.gmail","parameters":{"toList":"={{$json.tenant_email}}","subject":"Maintenance request received — {{$json.property_address}}","message":"=Hi {{$json.tenant_name}},\n\nWe've received your maintenance request (Priority: {{$json.severity}}).\nExpected resolution: within {{$json.sla_hours}} hours.\n\nWe'll keep you updated."},"position":[600,480]},
    {"id":"6","name":"Log to Sheets","type":"n8n-nodes-base.googleSheets","parameters":{"operation":"append","documentId":"YOUR_SHEET_ID","sheetName":"Maintenance"},"position":[600,660]}
  ],
  "connections":{"New Request":{"main":[["Acknowledge","Classify Severity"]]},"Classify Severity":{"main":[["Slack Maintenance","Email Tenant","Log to Sheets"]]}}
}
Enter fullscreen mode Exit fullscreen mode

Customise: Add a second Switch node to route different severity levels to different vendor emails or on-call phone numbers via Twilio.


4. Monthly Investor Portfolio Performance Report

What it does: Runs on the 1st of every month at 8AM, pulls portfolio data from Sheets, calculates NOI, occupancy rate, and maintenance cost ratio, then emails an HTML report to investors.

Why it matters: Investors expect monthly reporting. Building the report manually takes 2-3 hours. This runs while you sleep.

{
  "nodes": [
    {"id":"1","name":"1st of Month 8AM","type":"n8n-nodes-base.scheduleTrigger","parameters":{"rule":{"interval":[{"field":"cronExpression","expression":"0 8 1 * *"}]}},"position":[200,300]},
    {"id":"2","name":"Get Portfolio","type":"n8n-nodes-base.googleSheets","parameters":{"operation":"getAll","documentId":"YOUR_SHEET_ID","sheetName":"Properties"},"position":[400,300]},
    {"id":"3","name":"Build Report","type":"n8n-nodes-base.code","parameters":{"jsCode":"const props = $input.all();\nconst totalRevenue = props.reduce((s, p) => s + Number(p.json.monthly_revenue || 0), 0);\nconst totalExpenses = props.reduce((s, p) => s + Number(p.json.monthly_expenses || 0), 0);\nconst noi = totalRevenue - totalExpenses;\nconst occupied = props.filter(p => p.json.status === 'occupied').length;\nconst occupancyRate = ((occupied / props.length) * 100).toFixed(1);\nconst maintCost = props.reduce((s, p) => s + Number(p.json.maintenance_cost || 0), 0);\nconst month = new Date().toLocaleDateString('en-US', {month: 'long', year: 'numeric'});\nconst rows = props.map(p => `<tr><td>${p.json.address}</td><td>$${Number(p.json.monthly_revenue||0).toLocaleString()}</td><td>${p.json.status}</td></tr>`).join('');\nconst html = `<h2>Portfolio Report — ${month}</h2><table border=1 cellpadding=6><tr><td><b>Total Revenue</b></td><td>$${totalRevenue.toLocaleString()}</td></tr><tr><td><b>Total Expenses</b></td><td>$${totalExpenses.toLocaleString()}</td></tr><tr><td><b>NOI</b></td><td>$${noi.toLocaleString()}</td></tr><tr><td><b>Occupancy Rate</b></td><td>${occupancyRate}%</td></tr><tr><td><b>Maintenance Cost</b></td><td>$${maintCost.toLocaleString()}</td></tr></table><br><h3>Properties</h3><table border=1 cellpadding=6><tr><th>Address</th><th>Revenue</th><th>Status</th></tr>${rows}</table>`;\nreturn [{json: {html, totalRevenue, noi, occupancyRate, month}}];"},"position":[600,300]},
    {"id":"4","name":"Email Investors","type":"n8n-nodes-base.gmail","parameters":{"toList":"investors@yourcompany.com","subject":"=Portfolio Report — {{$json.month}} | NOI: ${{$json.noi.toLocaleString()}} | Occupancy: {{$json.occupancyRate}}%","message":"={{$json.html}}","emailType":"html"},"position":[800,300]}
  ],
  "connections":{"1st of Month 8AM":{"main":[["Get Portfolio"]]},"Get Portfolio":{"main":[["Build Report"]]},"Build Report":{"main":[["Email Investors"]]}}
}
Enter fullscreen mode Exit fullscreen mode

Customise: BCC specific investors per property by filtering the Sheets rows and looping. Add a Postgres query if your property data lives in a database rather than Sheets.


5. Lease Renewal Alert Pipeline

What it does: Runs daily at 8AM, reads all active leases from Sheets, identifies leases expiring within 90/60/30/14 days, emails tenants a renewal offer at each urgency level, and Slacks your leasing team.

Why it matters: Vacant units cost money. Proactive renewal outreach at 90 days out dramatically increases renewal rates. Manual tracking falls through the cracks — this doesn't.

{
  "nodes": [
    {"id":"1","name":"Daily 8AM","type":"n8n-nodes-base.scheduleTrigger","parameters":{"rule":{"interval":[{"field":"cronExpression","expression":"0 8 * * *"}]}},"position":[200,300]},
    {"id":"2","name":"Get Leases","type":"n8n-nodes-base.googleSheets","parameters":{"operation":"getAll","documentId":"YOUR_SHEET_ID","sheetName":"Leases"},"position":[400,300]},
    {"id":"3","name":"Flag Expiring","type":"n8n-nodes-base.code","parameters":{"jsCode":"const leases = $input.all();\nconst today = new Date();\nconst alerts = [];\nfor (const item of leases) {\n  const l = item.json;\n  if (!l.lease_end_date || l.status !== 'active') continue;\n  const expiry = new Date(l.lease_end_date);\n  const daysLeft = Math.round((expiry - today) / 86400000);\n  l.days_left = daysLeft;\n  if (daysLeft <= 14) l.urgency = 'CRITICAL';\n  else if (daysLeft <= 30) l.urgency = 'URGENT';\n  else if (daysLeft <= 60) l.urgency = 'WARNING';\n  else if (daysLeft <= 90) l.urgency = 'NOTICE';\n  else continue;\n  alerts.push({json: l});\n}\nreturn alerts;"},"position":[600,300]},
    {"id":"4","name":"Email Tenant","type":"n8n-nodes-base.gmail","parameters":{"toList":"={{$json.tenant_email}}","subject":"=Your lease expires in {{$json.days_left}} days — renewal options inside","message":"=Hi {{$json.tenant_name}},\n\nYour lease at {{$json.property_address}} expires on {{$json.lease_end_date}} ({{$json.days_left}} days from now).\n\nWe'd love to keep you as a resident. Reply to this email or call us to discuss renewal options. Early renewers can lock in their current rate."},"position":[800,180]},
    {"id":"5","name":"Slack Leasing","type":"n8n-nodes-base.slack","parameters":{"text":"=[{{$json.urgency}}] Lease expires in {{$json.days_left}} days | {{$json.property_address}} | Tenant: {{$json.tenant_name}} | Email: {{$json.tenant_email}}","channel":"#leasing"},"position":[800,420]}
  ],
  "connections":{"Daily 8AM":{"main":[["Get Leases"]]},"Get Leases":{"main":[["Flag Expiring"]]},"Flag Expiring":{"main":[["Email Tenant","Slack Leasing"]]}}
}
Enter fullscreen mode Exit fullscreen mode

Customise: Add a Sheets update to mark renewal_contacted_at so tenants aren't emailed on every shift. Add a second email at 7 days with a stronger offer.


Why self-host n8n for PropTech?

Tenant PII, lease financial terms, investor returns, and applicant credit information are either legally regulated (Fair Housing, GDPR) or commercially sensitive. Routing this data through Zapier's or Make's cloud servers creates unnecessary exposure.

Self-hosted n8n keeps all of this inside your infrastructure. The JSON workflow files live in your git repo — auditable, version-controlled, reproducible. And at 500+ users, it's far cheaper than Zapier Enterprise.

n8n (self-hosted) Zapier Make.com
Data location Your servers Zapier cloud Make cloud
Cost at 50K ops/mo ~$20/mo server $799+/mo $299+/mo
Tenant PII exposure None Yes Yes
Workflow versioning Git No No
Custom code Full Node.js Limited Limited

Ready-to-use templates

These 5 workflows are available as pre-built, documented n8n templates at stripeai.gumroad.com. Drop them into your n8n instance and customize for your property data sources.

The full automation bundle (15 templates covering leads, operations, finance, and reporting) is available for $97 — or grab individual templates starting at $12.

Questions about adapting these for your stack? Drop them in the comments.

Top comments (0)