DEV Community

Alex Kane
Alex Kane

Posted on

n8n for PropertyTech SaaS: 5 Automations for RESPA, Fair Housing, and TRID Compliance (Free JSON)

If your SaaS platform serves real estate brokerages, mortgage lenders, title companies, or MLS operators, you already know the regulatory surface is brutal: RESPA Section 8 kickback rules, TRID 3-day disclosure windows, Fair Housing Act algorithmic bias risk, HMDA reporting obligations, and GSE seller/servicer certification requirements.

Most PropertyTech teams manage these obligations through a maze of calendar reminders, spreadsheets, and manual email threads. That breaks at scale — and a single compliance miss can mean CFPB enforcement, HUD complaints, or civil Fair Housing litigation.

Here are five production-ready n8n workflows that automate the compliance and ops work so your team can focus on building, not firefighting.

Why self-hosted n8n? Your platform processes buyer search history, mortgage pre-approval amounts, property offer prices, and agent commission structures. Routing that through Zapier or Make.com cloud means your customers' PII travels through a third-party CSP — creating CCPA, GLBA, and state privacy law exposure. Self-hosted n8n keeps all data in your VPC. Git-versioned workflow JSON gives auditors a complete automation change log.


Workflow 1: New Customer Onboarding Drip (Tier-Segmented + Compliance-Flag Aware)

Real estate buyers come in radically different shapes: enterprise brokerages with hundreds of agents, regional firms, independent agents, mortgage lenders, and title companies. Each segment has different RESPA exposure, different compliance training needs, and different integration requirements.

This workflow triggers on a new row in your customer sheet and routes a three-touch onboarding email sequence based on customer tier and compliance flags.

{
  "name": "PropertyTech Customer Onboarding Drip",
  "nodes": [
    {
      "id": "1",
      "name": "Customer Sheet Trigger",
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "parameters": {
        "sheetId": "YOUR_SHEET_ID",
        "range": "Customers!A:K",
        "pollTime": {
          "mode": "everyMinute"
        }
      },
      "position": [
        240,
        300
      ]
    },
    {
      "id": "2",
      "name": "Classify Tier & Compliance",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const row = $input.first().json;\nconst tier = row.customer_type || 'INDEPENDENT_AGENT';\nconst flags = {\n  RESPA_LICENSED: row.respa_licensed === 'TRUE',\n  FAIR_HOUSING_CERTIFIED: row.fair_housing_cert === 'TRUE',\n  HMDA_FILER: row.hmda_filer === 'TRUE',\n  GSE_APPROVED: row.gse_approved === 'TRUE',\n  STATE_RE_LICENSE: row.state_re_license === 'TRUE'\n};\nconst tierConfig = {\n  ENTERPRISE_BROKERAGE: { label: 'Enterprise Brokerage', csm: 'enterprise@flowkit.io', sla: '2-hour response' },\n  REGIONAL_BROKERAGE: { label: 'Regional Brokerage', csm: 'success@flowkit.io', sla: '4-hour response' },\n  MORTGAGE_LENDER: { label: 'Mortgage Lender', csm: 'success@flowkit.io', sla: '4-hour response' },\n  TITLE_COMPANY: { label: 'Title Company', csm: 'success@flowkit.io', sla: '8-hour response' },\n  INDEPENDENT_AGENT: { label: 'Independent Agent', csm: 'onboarding@flowkit.io', sla: '24-hour response' }\n};\nconst cfg = tierConfig[tier] || tierConfig.INDEPENDENT_AGENT;\nconst complianceNote = flags.RESPA_LICENSED\n  ? 'As a RESPA-licensed entity, your account includes our Section 8 referral fee audit log and kickback prevention controls.'\n  : 'Please confirm your RESPA licensing status in your account settings to unlock compliance features.';\nreturn [{ json: { ...row, tier, flags, cfg, complianceNote } }];"
      },
      "position": [
        460,
        300
      ]
    },
    {
      "id": "3",
      "name": "Day 0 Welcome Email",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "={{ $json.email }}",
        "subject": "Welcome to FlowKit \u2014 Your {{ $json.cfg.label }} account is ready",
        "message": "={{ 'Hi ' + $json.contact_name + ',\\n\\nWelcome to FlowKit. Your ' + $json.cfg.label + ' account is active.\\n\\nSupport SLA: ' + $json.cfg.sla + '.\\n\\n' + $json.complianceNote + '\\n\\nYour onboarding checklist: [link]\\n\\nFlowKit Team' }}"
      },
      "position": [
        680,
        300
      ]
    },
    {
      "id": "4",
      "name": "Log to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "appendOrUpdate",
        "sheetId": "YOUR_SHEET_ID",
        "range": "OnboardingLog!A:F",
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "customer_id": "={{ $json.customer_id }}",
            "tier": "={{ $json.tier }}",
            "day0_sent": "={{ new Date().toISOString() }}",
            "csm": "={{ $json.cfg.csm }}",
            "status": "ONBOARDING"
          }
        }
      },
      "position": [
        680,
        480
      ]
    },
    {
      "id": "5",
      "name": "Wait 3 Days",
      "type": "n8n-nodes-base.wait",
      "parameters": {
        "amount": 3,
        "unit": "days"
      },
      "position": [
        900,
        300
      ]
    },
    {
      "id": "6",
      "name": "Day 3 Integration Tips",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "={{ $json.email }}",
        "subject": "Day 3: Connect your MLS feed and CRM in 10 minutes",
        "message": "={{ 'Hi ' + $json.contact_name + ',\\n\\nDay 3 tip: Connect your MLS RETS/RESO Web API feed and your CRM in under 10 minutes.\\n\\nTop 3 integrations for ' + $json.cfg.label + ' accounts: [MLS sync] [CRM webhook] [E-signature]\\n\\nQuestions? Reply to this email \u2014 your CSM (' + $json.cfg.csm + ') responds within ' + $json.cfg.sla + '.\\n\\nFlowKit Team' }}"
      },
      "position": [
        1120,
        300
      ]
    },
    {
      "id": "7",
      "name": "Wait 4 More Days",
      "type": "n8n-nodes-base.wait",
      "parameters": {
        "amount": 4,
        "unit": "days"
      },
      "position": [
        1340,
        300
      ]
    },
    {
      "id": "8",
      "name": "Day 7 Value Check-In",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "={{ $json.email }}",
        "subject": "Week 1 complete \u2014 how is FlowKit working for your team?",
        "message": "={{ 'Hi ' + $json.contact_name + ',\\n\\nOne week in. We want to make sure FlowKit is delivering value for your ' + $json.cfg.label + ' operation.\\n\\nSchedule a 20-min call with your CSM: [calendar link]\\n\\nOr reply with your biggest challenge right now and we will help.\\n\\nFlowKit Team' }}"
      },
      "position": [
        1560,
        300
      ]
    }
  ],
  "connections": {
    "Customer Sheet Trigger": {
      "main": [
        [
          "Classify Tier & Compliance"
        ]
      ]
    },
    "Classify Tier & Compliance": {
      "main": [
        [
          "Day 0 Welcome Email",
          "Log to Sheets"
        ]
      ]
    },
    "Day 0 Welcome Email": {
      "main": [
        [
          "Wait 3 Days"
        ]
      ]
    },
    "Wait 3 Days": {
      "main": [
        [
          "Day 3 Integration Tips"
        ]
      ]
    },
    "Day 3 Integration Tips": {
      "main": [
        [
          "Wait 4 More Days"
        ]
      ]
    },
    "Wait 4 More Days": {
      "main": [
        [
          "Day 7 Value Check-In"
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why this matters: Enterprise brokerages with RESPA licensing need different compliance messaging than independent agents. Sending a HMDA reporting reminder to someone who isn't a HMDA filer wastes their time and signals your platform doesn't understand their business.


Workflow 2: MLS / Property Data Feed Health Monitor

Your platform's core value is accurate, timely listing data. When your MLS RETS or RESO Web API connection degrades, agents list properties with stale data — and that creates Fair Housing exposure if protected-class neighborhoods show outdated prices or availability.

{
  "name": "MLS Data Feed Health Monitor",
  "nodes": [
    {
      "id": "1",
      "name": "Every 5 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 5
            }
          ]
        }
      },
      "position": [
        240,
        300
      ]
    },
    {
      "id": "2",
      "name": "Load MLS Feeds",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "getAll",
        "sheetId": "YOUR_SHEET_ID",
        "range": "MLSFeeds!A:F"
      },
      "position": [
        460,
        300
      ]
    },
    {
      "id": "3",
      "name": "Check Each Feed",
      "type": "n8n-nodes-base.splitInBatches",
      "parameters": {
        "batchSize": 1
      },
      "position": [
        680,
        300
      ]
    },
    {
      "id": "4",
      "name": "HTTP Health Check",
      "type": "n8n-nodes-base.httpRequest",
      "parameters": {
        "method": "GET",
        "url": "={{ $json.health_endpoint }}",
        "timeout": 10000,
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      },
      "position": [
        900,
        300
      ]
    },
    {
      "id": "5",
      "name": "Evaluate Status",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const feed = $('Load MLS Feeds').first().json;\nconst resp = $input.first().json;\nconst now = new Date();\nconst lastSync = new Date(feed.last_sync_ts);\nconst minutesSinceSync = (now - lastSync) / 60000;\nconst statusCode = resp.$response?.statusCode || 0;\nlet status = 'OK';\nlet severity = 'none';\nlet message = '';\nconst freshnessLimit = feed.mls_type === 'RETS' ? 60 : 30;\nif (statusCode !== 200) {\n  status = 'DOWN';\n  severity = 'CRITICAL';\n  message = `MLS feed ${feed.feed_name} returned HTTP ${statusCode}. Listing data may be stale \u2014 Fair Housing risk if protected-class neighborhoods show outdated availability.`;\n} else if (minutesSinceSync > freshnessLimit) {\n  status = 'STALE';\n  severity = 'WARNING';\n  message = `MLS feed ${feed.feed_name} last synced ${Math.round(minutesSinceSync)} min ago (limit: ${freshnessLimit} min). RESO Web API data freshness SLA may be violated.`;\n} else if (resp.response_time_ms > 5000) {\n  status = 'DEGRADED';\n  severity = 'WARNING';\n  message = `MLS feed ${feed.feed_name} responding slowly (${resp.response_time_ms}ms). Agent search performance impacted.`;\n}\nreturn [{ json: { ...feed, status, severity, message, checked_at: now.toISOString() } }];"
      },
      "position": [
        1120,
        300
      ]
    },
    {
      "id": "6",
      "name": "Alert if Not OK",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.severity }}",
              "operation": "isNotEqual",
              "value2": "none"
            }
          ]
        }
      },
      "position": [
        1340,
        300
      ]
    },
    {
      "id": "7",
      "name": "Slack #mls-ops Alert",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "resource": "message",
        "operation": "post",
        "channel": "#mls-ops",
        "text": "={{ ':rotating_light: MLS FEED ' + $json.severity + ': ' + $json.message + ' | Feed: ' + $json.feed_name + ' | MLS: ' + $json.mls_id }}"
      },
      "position": [
        1560,
        200
      ]
    }
  ],
  "connections": {
    "Every 5 Minutes": {
      "main": [
        [
          "Load MLS Feeds"
        ]
      ]
    },
    "Load MLS Feeds": {
      "main": [
        [
          "Check Each Feed"
        ]
      ]
    },
    "Check Each Feed": {
      "main": [
        [
          "HTTP Health Check"
        ]
      ]
    },
    "HTTP Health Check": {
      "main": [
        [
          "Evaluate Status"
        ]
      ]
    },
    "Evaluate Status": {
      "main": [
        [
          "Alert if Not OK"
        ]
      ]
    },
    "Alert if Not OK": {
      "main": [
        [
          "Slack #mls-ops Alert"
        ],
        []
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Key angle: Stale MLS data in protected-class neighborhoods creates Fair Housing exposure — buyers seeing outdated listings could bring disparate impact claims. Catching freshness failures in 5 minutes keeps your data integrity SLA and your Fair Housing posture clean.


Workflow 3: RESPA / TRID / HMDA / Fair Housing Compliance Deadline Tracker

PropertyTech SaaS vendors have their own compliance calendar — annual audits, quarterly HMDA submissions, TRID disclosure reviews, GSE certification renewals. Most teams manage this in a shared calendar that nobody actually owns.

{
  "name": "PropertyTech Compliance Deadline Tracker",
  "nodes": [
    {
      "id": "1",
      "name": "Weekdays 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1-5"
            }
          ]
        }
      },
      "position": [
        240,
        300
      ]
    },
    {
      "id": "2",
      "name": "Load Deadlines",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "getAll",
        "sheetId": "YOUR_SHEET_ID",
        "range": "ComplianceDeadlines!A:H"
      },
      "position": [
        460,
        300
      ]
    },
    {
      "id": "3",
      "name": "Classify Urgency",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const rows = $input.all();\nconst now = new Date();\nconst alerts = [];\nconst deadlineLabels = {\n  RESPA_SECTION8_AUDIT: 'RESPA Section 8 Kickback Compliance Audit',\n  TRID_DISCLOSURE_REVIEW: 'TRID Integrated Disclosure Compliance Review',\n  FAIR_HOUSING_ANNUAL_TRAINING: 'Fair Housing Act Annual Training Certification',\n  HMDA_Q1_LAR_SUBMISSION: 'HMDA Q1 Loan Application Register Submission',\n  HMDA_Q2_LAR_SUBMISSION: 'HMDA Q2 Loan Application Register Submission',\n  HMDA_Q3_LAR_SUBMISSION: 'HMDA Q3 Loan Application Register Submission',\n  HMDA_ANNUAL_LAR_SUBMISSION: 'HMDA Annual LAR Submission (CFPB HMDA Platform, by Mar 1)',\n  CFPB_REG_X_REVIEW: 'CFPB Regulation X (RESPA Implementation) Annual Review',\n  STATE_RE_LICENSE_RENEWAL: 'State Real Estate License Renewal',\n  GSE_SELLER_SERVICER_CERT: 'Fannie Mae/Freddie Mac Seller-Servicer Annual Certification',\n  FANNIE_MAE_SELLING_GUIDE: 'Fannie Mae Selling Guide Compliance Audit',\n  FREDDIE_MAC_SELLER_AUDIT: 'Freddie Mac Seller/Servicer Assessment'\n};\nfor (const row of rows) {\n  const due = new Date(row.json.due_date);\n  const daysLeft = Math.ceil((due - now) / 86400000);\n  const sent = row.json.alert_sent_date;\n  const today = now.toISOString().slice(0,10);\n  if (sent === today) continue;\n  let severity = null;\n  if (daysLeft < 0) severity = 'OVERDUE';\n  else if (daysLeft <= 7) severity = 'CRITICAL';\n  else if (daysLeft <= 21) severity = 'URGENT';\n  else if (daysLeft <= 45) severity = 'WARNING';\n  else if (daysLeft <= 90) severity = 'NOTICE';\n  if (severity) alerts.push({ ...row.json, daysLeft, severity, label: deadlineLabels[row.json.deadline_type] || row.json.deadline_type });\n}\nif (!alerts.length) return [{ json: { skip: true } }];\nreturn alerts.map(a => ({ json: a }));"
      },
      "position": [
        680,
        300
      ]
    },
    {
      "id": "4",
      "name": "Skip if None",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.skip }}",
              "operation": "equal",
              "value2": true
            }
          ]
        }
      },
      "position": [
        900,
        300
      ]
    },
    {
      "id": "5",
      "name": "Slack #compliance",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "resource": "message",
        "operation": "post",
        "channel": "#compliance",
        "text": "={{ $json.severity + ': ' + $json.label + ' \u2014 ' + ($json.daysLeft < 0 ? Math.abs($json.daysLeft) + ' days OVERDUE' : $json.daysLeft + ' days remaining') + ' | Owner: ' + $json.owner + ' | Due: ' + $json.due_date }}"
      },
      "position": [
        1120,
        200
      ]
    },
    {
      "id": "6",
      "name": "Email Owner",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "={{ $json.owner_email }}",
        "subject": "={{ $json.severity + ': ' + $json.label + ' due ' + ($json.daysLeft < 0 ? 'OVERDUE by ' + Math.abs($json.daysLeft) + ' days' : 'in ' + $json.daysLeft + ' days') }}",
        "message": "={{ 'Compliance deadline alert:\\n\\n' + $json.label + '\\nStatus: ' + $json.severity + '\\nDue: ' + $json.due_date + '\\nDays remaining: ' + $json.daysLeft + '\\nOwner: ' + $json.owner + '\\n\\nRegulatory reference: ' + $json.regulatory_ref }}"
      },
      "position": [
        1120,
        400
      ]
    }
  ],
  "connections": {
    "Weekdays 8AM": {
      "main": [
        [
          "Load Deadlines"
        ]
      ]
    },
    "Load Deadlines": {
      "main": [
        [
          "Classify Urgency"
        ]
      ]
    },
    "Classify Urgency": {
      "main": [
        [
          "Skip if None"
        ]
      ]
    },
    "Skip if None": {
      "main": [
        [],
        [
          "Slack #compliance",
          "Email Owner"
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

12 deadline types covered: RESPA Section 8 audits, TRID disclosure reviews, Fair Housing annual training, HMDA Q1/Q2/Q3/annual LAR submissions (CFPB HMDA Platform deadline: March 1), CFPB Reg X review, state RE license renewal, and GSE seller/servicer certification.


Workflow 4: Fair Housing Compliance Incident & CFPB Inquiry Pipeline

When your platform's AI recommendation engine surfaces a Fair Housing complaint, or when a CFPB inquiry lands, you need a documented, auditable response chain — not a frantic Slack thread.

This workflow catches compliance incidents at ingestion, classifies them, routes to the right team, and creates a Postgres audit trail that survives a regulatory review.

{
  "name": "Fair Housing Incident & CFPB Inquiry Pipeline",
  "nodes": [
    {
      "id": "1",
      "name": "Incident Webhook",
      "type": "n8n-nodes-base.webhook",
      "parameters": {
        "path": "propertytech-incident",
        "responseMode": "responseNode"
      },
      "position": [
        240,
        300
      ]
    },
    {
      "id": "2",
      "name": "Immediate 200 OK",
      "type": "n8n-nodes-base.respondToWebhook",
      "parameters": {
        "responseCode": 200,
        "responseBody": "{\"received\":true}"
      },
      "position": [
        460,
        200
      ]
    },
    {
      "id": "3",
      "name": "Classify Incident",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const body = $input.first().json.body || $input.first().json;\nconst incidentType = body.incident_type;\nconst incidentConfig = {\n  DISPARATE_IMPACT_ALGORITHM_FLAG: {\n    severity: 'CRITICAL',\n    channel: '#legal-compliance',\n    clock: '72 hours to document remediation plan',\n    regRef: 'Fair Housing Act 42 U.S.C. \u00a73604 \u2014 disparate impact standard (HUD v. Inclusive Communities)',\n    escalate: true\n  },\n  REDLINING_COMPLAINT: {\n    severity: 'CRITICAL',\n    channel: '#legal-compliance',\n    clock: '48 hours for initial legal review',\n    regRef: 'Fair Housing Act \u00a73604(a) + ECOA \u2014 geographic lending pattern scrutiny',\n    escalate: true\n  },\n  PROTECTED_CLASS_EXCLUSION: {\n    severity: 'HIGH',\n    channel: '#legal-compliance',\n    clock: '24 hours for compliance review',\n    regRef: 'Fair Housing Act \u00a73604 \u2014 race/color/national origin/religion/sex/familial status/disability',\n    escalate: true\n  },\n  HUD_INQUIRY: {\n    severity: 'HIGH',\n    channel: '#legal-compliance',\n    clock: '10 business days to respond (HUD standard)',\n    regRef: '24 CFR Part 100 \u2014 HUD Fair Housing regulations',\n    escalate: true\n  },\n  CFPB_COMPLAINT: {\n    severity: 'HIGH',\n    channel: '#legal-compliance',\n    clock: '15 calendar days to respond (CFPB complaint portal)',\n    regRef: 'CFPB Regulation X (RESPA) / Regulation Z (TILA) jurisdiction',\n    escalate: true\n  },\n  STATE_AG_INVESTIGATION: {\n    severity: 'CRITICAL',\n    channel: '#legal-compliance',\n    clock: 'Respond per state AG notice terms',\n    regRef: 'State Fair Housing Act equivalents + UDAP statutes',\n    escalate: true\n  },\n  AI_BIAS_AUDIT_TRIGGER: {\n    severity: 'HIGH',\n    channel: '#engineering-compliance',\n    clock: '5 business days for bias audit initiation',\n    regRef: 'Fair Housing Act disparate impact \u2014 algorithmic underwriting/search ranking scrutiny (CFPB 2023 guidance)',\n    escalate: false\n  },\n  FEDERAL_FAIR_HOUSING_LAWSUIT: {\n    severity: 'CRITICAL',\n    channel: '#legal-compliance',\n    clock: 'Preserve all evidence immediately \u2014 litigation hold',\n    regRef: '42 U.S.C. \u00a73613 \u2014 private Fair Housing enforcement',\n    escalate: true\n  }\n};\nconst cfg = incidentConfig[incidentType] || { severity: 'HIGH', channel: '#legal-compliance', clock: 'Review within 24 hours', regRef: 'General compliance review', escalate: false };\nconst deadlineTs = new Date(Date.now() + 86400000).toISOString();\nreturn [{ json: { ...body, ...cfg, incident_id: `INC-${Date.now()}`, received_at: new Date().toISOString(), deadline_ts: deadlineTs } }];"
      },
      "position": [
        680,
        300
      ]
    },
    {
      "id": "4",
      "name": "Slack Alert",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "resource": "message",
        "operation": "post",
        "channel": "={{ $json.channel }}",
        "text": "={{ ':rotating_light: ' + $json.severity + ' \u2014 ' + $json.incident_type.replace(/_/g, ' ') + ' | ID: ' + $json.incident_id + ' | Clock: ' + $json.clock + ' | Ref: ' + $json.regRef }}"
      },
      "position": [
        900,
        200
      ]
    },
    {
      "id": "5",
      "name": "Log to Postgres",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO compliance_incidents (incident_id, incident_type, severity, received_at, deadline_ts, regulatory_ref, raw_payload, status) VALUES ('{{ $json.incident_id }}', '{{ $json.incident_type }}', '{{ $json.severity }}', '{{ $json.received_at }}', '{{ $json.deadline_ts }}', '{{ $json.regRef }}', '{{ JSON.stringify($json) }}', 'OPEN') ON CONFLICT (incident_id) DO NOTHING;"
      },
      "position": [
        900,
        400
      ]
    },
    {
      "id": "6",
      "name": "Email Compliance Lead",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "compliance-lead@yourcompany.com",
        "subject": "={{ $json.severity + ' Compliance Incident: ' + $json.incident_type }}",
        "message": "={{ 'Incident ID: ' + $json.incident_id + '\\nType: ' + $json.incident_type + '\\nSeverity: ' + $json.severity + '\\nRegulatory reference: ' + $json.regRef + '\\nResponse clock: ' + $json.clock + '\\nReceived at: ' + $json.received_at + '\\n\\nRaw payload:\\n' + JSON.stringify($json, null, 2) }}"
      },
      "position": [
        1120,
        300
      ]
    }
  ],
  "connections": {
    "Incident Webhook": {
      "main": [
        [
          "Immediate 200 OK",
          "Classify Incident"
        ]
      ]
    },
    "Classify Incident": {
      "main": [
        [
          "Slack Alert",
          "Log to Postgres",
          "Email Compliance Lead"
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

8 incident types covered: Disparate impact algorithm flags, redlining complaints, protected class exclusions, HUD inquiries, CFPB complaints, state AG investigations, AI bias audit triggers, and federal Fair Housing lawsuits.

CFPB AI Guidance note: The CFPB's 2023 guidance on algorithmic decision-making in mortgage underwriting and property search explicitly calls out disparate impact risk from AI ranking systems. If your search ranks properties by "neighborhood score" that correlates with race or national origin, that's a Fair Housing violation — regardless of intent. This workflow catches the flag; your engineers fix the model.


Workflow 5: Weekly PropertyTech Platform KPI Dashboard

CEOs need MRR by customer segment. Compliance officers need to know how many RESPA incidents and Fair Housing flags fired this week. This workflow delivers both in one Monday morning briefing.

{
  "name": "Weekly PropertyTech KPI Dashboard",
  "nodes": [
    {
      "id": "1",
      "name": "Monday 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "position": [
        240,
        300
      ]
    },
    {
      "id": "2",
      "name": "Query Platform Metrics",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT customer_tier, COUNT(*) as active_customers, SUM(mrr_usd) as tier_mrr, COUNT(CASE WHEN created_at >= NOW() - INTERVAL '7 days' THEN 1 END) as new_this_week, COUNT(CASE WHEN status = 'CHURNED' AND churned_at >= NOW() - INTERVAL '7 days' THEN 1 END) as churned_this_week FROM customers WHERE status != 'TRIAL' GROUP BY customer_tier;"
      },
      "position": [
        460,
        300
      ]
    },
    {
      "id": "3",
      "name": "Query Compliance Events",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT incident_type, severity, COUNT(*) as count FROM compliance_incidents WHERE received_at >= NOW() - INTERVAL '7 days' GROUP BY incident_type, severity ORDER BY severity, count DESC;"
      },
      "position": [
        460,
        500
      ]
    },
    {
      "id": "4",
      "name": "Build Report",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const metrics = $('Query Platform Metrics').all().map(r => r.json);\nconst compliance = $('Query Compliance Events').all().map(r => r.json);\nconst totalMRR = metrics.reduce((s, r) => s + parseFloat(r.tier_mrr || 0), 0);\nconst totalCustomers = metrics.reduce((s, r) => s + parseInt(r.active_customers || 0), 0);\nconst newThisWeek = metrics.reduce((s, r) => s + parseInt(r.new_this_week || 0), 0);\nconst churnedThisWeek = metrics.reduce((s, r) => s + parseInt(r.churned_this_week || 0), 0);\nconst tierRows = metrics.map(r => `<tr><td>${r.customer_tier}</td><td>${r.active_customers}</td><td>$${parseFloat(r.tier_mrr).toLocaleString()}</td><td>+${r.new_this_week}</td><td>-${r.churned_this_week}</td></tr>`).join('');\nconst criticalFlags = compliance.filter(c => ['CRITICAL','HIGH'].includes(c.severity));\nconst complianceRows = compliance.map(c => `<tr><td>${c.incident_type.replace(/_/g,' ')}</td><td>${c.severity}</td><td>${c.count}</td></tr>`).join('');\nconst criticalAlert = criticalFlags.length > 0 ? `<p style='color:red;font-weight:bold'>\u26a0 ${criticalFlags.length} CRITICAL/HIGH compliance incident(s) this week \u2014 review required.</p>` : '<p style=\"color:green\">\u2713 No critical compliance incidents this week.</p>';\nconst html = `<h2>PropertyTech Platform \u2014 Weekly KPI Report</h2><p>Week ending: ${new Date().toISOString().slice(0,10)}</p><h3>Revenue Summary</h3><p>Total MRR: <b>$${totalMRR.toLocaleString()}</b> | Active Customers: <b>${totalCustomers}</b> | New: <b>+${newThisWeek}</b> | Churned: <b>-${churnedThisWeek}</b></p><table border='1' cellpadding='6'><tr><th>Tier</th><th>Customers</th><th>MRR</th><th>New</th><th>Churned</th></tr>${tierRows}</table><h3>Compliance Events (Last 7 Days)</h3>${criticalAlert}<table border='1' cellpadding='6'><tr><th>Incident Type</th><th>Severity</th><th>Count</th></tr>${complianceRows || '<tr><td colspan=3>None</td></tr>'}</table>`;\nreturn [{ json: { html, totalMRR, totalCustomers, criticalFlags: criticalFlags.length } }];"
      },
      "position": [
        680,
        400
      ]
    },
    {
      "id": "5",
      "name": "Email CEO + BCC Compliance",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "operation": "send",
        "to": "ceo@yourcompany.com",
        "bcc": "compliance@yourcompany.com",
        "subject": "={{ 'PropertyTech Weekly KPI \u2014 MRR $' + $json.totalMRR.toLocaleString() + ' | ' + ($json.criticalFlags > 0 ? $json.criticalFlags + ' COMPLIANCE FLAGS' : 'No compliance flags') }}",
        "message": "={{ $json.html }}",
        "options": {
          "appendAttribution": false
        }
      },
      "position": [
        900,
        400
      ]
    }
  ],
  "connections": {
    "Monday 8AM": {
      "main": [
        [
          "Query Platform Metrics",
          "Query Compliance Events"
        ]
      ]
    },
    "Query Platform Metrics": {
      "main": [
        [
          "Build Report"
        ]
      ]
    },
    "Query Compliance Events": {
      "main": [
        [
          "Build Report"
        ]
      ]
    },
    "Build Report": {
      "main": [
        [
          "Email CEO + BCC Compliance"
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Compliance Officer BCC is intentional. When a CRITICAL Fair Housing flag fires in the weekly report, the compliance officer sees it alongside the CEO. No more 'I didn't know about this' in a regulator deposition.


Why n8n instead of Zapier or Make.com?

Requirement Zapier / Make.com Self-Hosted n8n
Buyer PII in automation Leaves your VPC Stays in your VPC
RESPA/HMDA audit trail No built-in version control Git-versioned JSON
Fair Housing incident log External SaaS storage Postgres in your enclave
CCPA/GLBA data sovereignty Third-party cloud Self-controlled infrastructure
GSE seller/servicer compliance Vendor risk finding No external dependency
Custom compliance routing logic Limited branching Full JavaScript/Python

The core problem: Routing mortgage pre-approval amounts, buyer search histories, property offer prices, and agent commission structures through Zapier or Make.com means your customers' non-public financial information (NPFI under GLBA) transits a third-party cloud. That's a GLBA Safeguards Rule §314.4(f) third-party oversight finding waiting to happen.


Get the complete FlowKit n8n template bundle

These five workflows are part of the FlowKit n8n Automation Template Bundle — 13 production-ready workflows for SaaS teams, available at stripeai.gumroad.com.

Templates include: Email Auto-Responder, Social Media Cross-Poster, Invoice Generator, Customer Feedback Analyzer, Appointment Reminder, Price Monitor, Content Repurposer, Webhook-to-Database, Lead Capture CRM, and AI Customer Support Bot.

Individual templates: $12–$29. Full bundle: $97.


Built with n8n self-hosted. All workflow JSON is import-ready — paste into your n8n canvas and adapt credentials.

Top comments (0)