DEV Community

Alex Kane
Alex Kane

Posted on

n8n for WealthTech SaaS: 5 Automations That Scale Portfolio Ops and Keep Investment Data Compliant (Free Workflow JSON)

WealthTech SaaS vendors — RIAs, robo-advisors, trading platforms, portfolio management SaaS — sit at the intersection of high-value client data and dense regulatory requirements. SEC Rule 17a-4, FINRA Rule 4511, MiFID II Article 16(7), CFTC Part 45: every regulation mandates strict control over who can access investment records, how long they're retained, and what audit trail proves compliance.

The problem with cloud automation platforms like Zapier and Make: they're multi-tenant SaaS. Your portfolio event data, advisor PII, and compliance deadlines flow through their shared infrastructure — creating sub-processor relationships your CCO and legal team almost certainly haven't vetted. SEC Reg S-P, FINRA's cybersecurity exam focus areas, and MiFID II Art.28 sub-processor DPA requirements are the specific tripwires.

Self-hosted n8n runs entirely inside your VPC. No data leaves your network. Here are 5 production-ready automations — with import-ready JSON — for WealthTech SaaS teams that need both operational efficiency and defensible compliance posture.


1. New RIA/Advisor Client Onboarding Drip

Automatically classifies new advisor signups by AUM/ARR tier (Enterprise RIA ≥$1B, Institutional AM ≥$100M, Mid-Market RIA ≥$10M, Self-Directed), routes to the correct CSM Slack channel with regulatory flags (SEC Form ADV required, FINRA BD applicable, MiFID II jurisdiction, CFTC CPO/CTA applicable), fires a tier-appropriate Day-0 welcome email, and runs a 7-day nurture sequence — all logged to Postgres for SOC 2 CC7.1 audit trail.

{
  "name": "WealthTech \u2014 New RIA/Advisor Client Onboarding Drip",
  "nodes": [
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerOn": "rowAdded",
        "documentId": "YOUR_SHEET_ID",
        "sheetName": "advisors"
      },
      "id": "t1",
      "name": "New Advisor Signup",
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "typeVersion": 4,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const r = $input.first().json;\nconst aum = parseFloat(r.aum_usd || 0);\nconst arr = parseFloat(r.arr_usd || 0);\nlet tier, ch;\nif (aum >= 1e9 || arr >= 1e6) { tier = 'ENTERPRISE_RIA'; ch = '#wealthtech-enterprise'; }\nelse if (aum >= 1e8 || arr >= 250000) { tier = 'INSTITUTIONAL_AM'; ch = '#wealthtech-institutional'; }\nelse if (aum >= 1e7 || arr >= 100000) { tier = 'MID_MARKET_RIA'; ch = '#wealthtech-midmarket'; }\nelse { tier = 'SELF_DIRECTED'; ch = '#wealthtech-onboarding'; }\nreturn [{ json: { ...r, tier, csmChannel: ch,\n  formAdvRequired: aum >= 1e7,\n  finraBdApplicable: r.bd_registered === 'yes',\n  mifid2Applicable: r.jurisdiction === 'EU',\n  cftcCpoApplicable: r.trades_derivatives === 'yes'\n}}];"
      },
      "id": "c1",
      "name": "Classify RIA Tier",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "toEmail": "={{ $json.advisor_email }}",
        "subject": "Welcome to {{ $json.platform_name }} \u2014 Your Setup Checklist",
        "emailType": "html",
        "message": "<p>Hi {{ $json.advisor_name }},</p><p>Account tier: <strong>{{ $json.tier }}</strong>. Next: connect custodian feeds \u2192 complete compliance profile \u2192 schedule CSM walkthrough.</p>{{ $json.formAdvRequired ? '<p><strong>Required:</strong> SEC Form ADV registration confirmation before live trading.</p>' : '' }}"
      },
      "id": "e1",
      "name": "Day 0 \u2014 Welcome Email",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        680,
        200
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "channel": "={{ $json.csmChannel }}",
        "text": ":chart_with_upwards_trend: New *{{ $json.tier }}* advisor: *{{ $json.advisor_name }}* ({{ $json.firm_name }})\nAUM: ${{ $json.aum_usd }} | ARR: ${{ $json.arr_usd }}\nFlags: FormADV={{ $json.formAdvRequired }}, FINRA-BD={{ $json.finraBdApplicable }}, MiFID-II={{ $json.mifid2Applicable }}, CFTC-CPO={{ $json.cftcCpoApplicable }}"
      },
      "id": "s1",
      "name": "Slack CSM Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        680,
        440
      ]
    },
    {
      "parameters": {
        "operation": "insert",
        "table": "advisor_onboarding_log",
        "columns": "advisor_id,tier,form_adv_required,finra_bd,mifid2,cftc_cpo,created_at"
      },
      "id": "db1",
      "name": "SOC2 Audit Log",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        680,
        620
      ]
    },
    {
      "parameters": {
        "amount": 3,
        "unit": "days"
      },
      "id": "w1",
      "name": "Wait 3 Days",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        900,
        200
      ]
    },
    {
      "parameters": {
        "toEmail": "={{ $json.advisor_email }}",
        "subject": "Day 3 \u2014 {{ $json.platform_name }} Setup Check-In",
        "message": "<p>Hi {{ $json.advisor_name }},</p><p>Have you connected your custodian feeds? Reply with any questions or grab sandbox API docs in your dashboard.</p>"
      },
      "id": "e2",
      "name": "Day 3 \u2014 Check-In Email",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1100,
        200
      ]
    },
    {
      "parameters": {
        "amount": 4,
        "unit": "days"
      },
      "id": "w2",
      "name": "Wait 4 Days",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1300,
        200
      ]
    },
    {
      "parameters": {
        "toEmail": "={{ $json.advisor_email }}",
        "subject": "Day 7 \u2014 Schedule Your Platform Walkthrough",
        "message": "<p>Hi {{ $json.advisor_name }},</p><p>You're one week in. Book a 30-min CSM walkthrough: [CALENDLY_LINK]. We'll cover rebalancing config, compliance reports, and API webhook testing.</p>"
      },
      "id": "e3",
      "name": "Day 7 \u2014 Walkthrough Invite",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1500,
        200
      ]
    }
  ],
  "connections": {
    "New Advisor Signup": {
      "main": [
        [
          {
            "node": "Classify RIA Tier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify RIA Tier": {
      "main": [
        [
          {
            "node": "Day 0 \u2014 Welcome Email",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack CSM Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "SOC2 Audit Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Day 0 \u2014 Welcome Email": {
      "main": [
        [
          {
            "node": "Wait 3 Days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 3 Days": {
      "main": [
        [
          {
            "node": "Day 3 \u2014 Check-In Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Day 3 \u2014 Check-In Email": {
      "main": [
        [
          {
            "node": "Wait 4 Days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 4 Days": {
      "main": [
        [
          {
            "node": "Day 7 \u2014 Walkthrough Invite",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  }
}
Enter fullscreen mode Exit fullscreen mode

2. Portfolio Drift & Rebalancing Alert

Polls your portfolio API every 30 minutes, calculates current vs. target asset class weights, fires a Slack alert when drift exceeds thresholds (5% equities, 4% bonds, 3% alternatives). Uses $getWorkflowStaticData for 2-hour dedup to prevent alert fatigue. Logs every drift event to Postgres — giving you the audit trail that SEC fiduciary duty reviews and FINRA exam requests require.

{
  "name": "WealthTech \u2014 Portfolio Drift & Rebalancing Alert",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 30
            }
          ]
        }
      },
      "id": "t2",
      "name": "Every 30 Min",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "url": "https://api.yourplatform.com/v1/portfolios/positions",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "httpBearerAuth",
        "options": {}
      },
      "id": "h2",
      "name": "Fetch Portfolio Positions",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const positions = $input.first().json.positions || [];\nconst targetWeights = { EQUITIES: 0.60, BONDS: 0.30, ALTERNATIVES: 0.05, CASH: 0.05 };\nconst driftThreshold = { EQUITIES: 0.05, BONDS: 0.04, ALTERNATIVES: 0.03, CASH: 0.02 };\nconst totalValue = positions.reduce((s, p) => s + p.market_value, 0);\nconst alerts = [];\nfor (const [assetClass, target] of Object.entries(targetWeights)) {\n  const current = positions.filter(p => p.asset_class === assetClass).reduce((s, p) => s + p.market_value, 0) / totalValue;\n  const drift = Math.abs(current - target);\n  if (drift > driftThreshold[assetClass]) {\n    alerts.push({ assetClass, current: (current*100).toFixed(2), target: (target*100).toFixed(2), drift: (drift*100).toFixed(2), severity: drift > driftThreshold[assetClass] * 2 ? 'REBALANCE_REQUIRED' : 'DRIFT_ALERT' });\n  }\n}\nconst state = $getWorkflowStaticData('global');\nconst now = Date.now();\nif (alerts.length === 0 || (state.lastAlert && now - state.lastAlert < 2 * 3600 * 1000)) return [];\nstate.lastAlert = now;\n$setWorkflowStaticData('global', state);\nreturn alerts.map(a => ({ json: { ...a, totalPortfolioValue: totalValue, ts: new Date().toISOString() } }));"
      },
      "id": "c2",
      "name": "Calculate Drift",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "channel": "#portfolio-ops",
        "text": ":warning: *{{ $json.severity }}* \u2014 {{ $json.assetClass }}\nCurrent: {{ $json.current }}% | Target: {{ $json.target }}% | Drift: {{ $json.drift }}%\nPortfolio Value: ${{ $json.totalPortfolioValue }}"
      },
      "id": "s2",
      "name": "Slack Portfolio Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        900,
        200
      ]
    },
    {
      "parameters": {
        "operation": "insert",
        "table": "rebalancing_log",
        "columns": "asset_class,current_weight,target_weight,drift_pct,severity,portfolio_value,ts",
        "additionalFields": {
          "onConflict": "DO NOTHING"
        }
      },
      "id": "db2",
      "name": "Rebalancing Audit Log",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        900,
        440
      ]
    }
  ],
  "connections": {
    "Every 30 Min": {
      "main": [
        [
          {
            "node": "Fetch Portfolio Positions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Portfolio Positions": {
      "main": [
        [
          {
            "node": "Calculate Drift",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Drift": {
      "main": [
        [
          {
            "node": "Slack Portfolio Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Rebalancing Audit Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  }
}
Enter fullscreen mode Exit fullscreen mode

3. SEC / FINRA / MiFID II Compliance Deadline Tracker

Runs every weekday at 8AM, reads your compliance calendar from Google Sheets, and fires tiered alerts (OVERDUE / CRITICAL ≤3d / URGENT ≤7d / WARNING ≤14d / NOTICE ≤30d) with dedup to avoid repeat notifications. Covers the full WealthTech regulatory stack: SEC Form ADV annual/amendment, Reg S-P privacy, Rule 17a-4 WORM audit, FINRA 4511/4512/2010, CFTC Part 17/45, MiFID II Art.16/26, GDPR Art.30 RoPA, CCPA annual privacy update, SOC 2 Type II window.

{
  "name": "WealthTech \u2014 SEC/FINRA/MiFID II Compliance Deadline Tracker",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1-5"
            }
          ]
        }
      },
      "id": "t3",
      "name": "Weekdays 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": "YOUR_SHEET_ID",
        "sheetName": "compliance_deadlines",
        "options": {}
      },
      "id": "sh3",
      "name": "Read Compliance Calendar",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const actionMap = {\n  SEC_ADV_ANNUAL: 'File annual Form ADV amendment with SEC EDGAR',\n  SEC_ADV_AMENDMENT: 'File material Form ADV amendment within 30 days',\n  SEC_REGSP_PRIVACY: 'Deliver Reg S-P annual privacy notice to clients',\n  SEC_204_2_RECORDS: 'Complete annual books-and-records retention audit (Rule 204-2)',\n  SEC_RULE17A4_WORM: 'Verify WORM storage compliance for Rule 17a-4 electronic records',\n  FINRA_4511_BOOKS: 'Complete FINRA Rule 4511 books-and-records annual review',\n  FINRA_4512_ACCOUNTS: 'Update FINRA 4512 customer account information review',\n  FINRA_2010_CONDUCT: 'Annual FINRA Rule 2010 standards-of-commercial-honor training',\n  CFTC_PART17: 'CFTC Part 17 large trader position report due',\n  CFTC_PART45_SWAP: 'CFTC Part 45 swap data repository reporting deadline',\n  MIFID2_ART16: 'MiFID II Art.16(7) transaction records 5-year retention audit',\n  MIFID2_ART26: 'MiFID II Art.26 transaction reporting to NCA',\n  GDPR_ART30_ROPA: 'Update GDPR Art.30 records of processing activities',\n  GDPR_DSR_30DAY: 'GDPR data subject request 30-day response deadline',\n  CCPA_ANNUAL_PRIVACY: 'Publish updated CCPA annual privacy policy',\n  SOC2_TYPE2: 'SOC 2 Type II annual audit window opens'\n};\nconst now = Date.now();\nconst results = [];\nfor (const row of $input.all()) {\n  const d = row.json;\n  if (!d.deadline_date || !d.regulation_code) continue;\n  const deadline = new Date(d.deadline_date).getTime();\n  const daysUntil = Math.floor((deadline - now) / 86400000);\n  let severity;\n  if (daysUntil < 0) severity = 'OVERDUE';\n  else if (daysUntil <= 3) severity = 'CRITICAL';\n  else if (daysUntil <= 7) severity = 'URGENT';\n  else if (daysUntil <= 14) severity = 'WARNING';\n  else if (daysUntil <= 30) severity = 'NOTICE';\n  else continue;\n  const lastNotified = d.last_notified_at ? new Date(d.last_notified_at).getTime() : 0;\n  if (now - lastNotified < 4 * 3600 * 1000) continue;\n  results.push({ json: { ...d, daysUntil, severity, action: actionMap[d.regulation_code] || d.regulation_code }});\n}\nreturn results;"
      },
      "id": "c3",
      "name": "Calculate Urgency",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true
          },
          "conditions": [
            {
              "leftValue": "={{ $json.severity }}",
              "rightValue": "NOTICE",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ]
        }
      },
      "id": "if3",
      "name": "Skip NOTICE to Slack",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        900,
        300
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "channel": "#compliance-regulatory",
        "text": "{{ $json.severity === 'OVERDUE' ? ':rotating_light:' : $json.severity === 'CRITICAL' ? ':red_circle:' : ':warning:' }} *{{ $json.severity }}* \u2014 {{ $json.regulation_code }} ({{ $json.daysUntil }}d)\n*Action:* {{ $json.action }}\n*Owner:* {{ $json.owner_email }}"
      },
      "id": "s3",
      "name": "Slack @here Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1100,
        200
      ]
    },
    {
      "parameters": {
        "toEmail": "={{ $json.owner_email }}",
        "subject": "[{{ $json.severity }}] {{ $json.regulation_code }} \u2014 {{ $json.daysUntil }} days remaining",
        "message": "<p><strong>{{ $json.severity }}</strong>: {{ $json.regulation_code }}</p><p>Deadline: {{ $json.deadline_date }}</p><p>Required action: {{ $json.action }}</p>"
      },
      "id": "e3b",
      "name": "Gmail Owner Email",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1100,
        440
      ]
    }
  ],
  "connections": {
    "Weekdays 8AM": {
      "main": [
        [
          {
            "node": "Read Compliance Calendar",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Compliance Calendar": {
      "main": [
        [
          {
            "node": "Calculate Urgency",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Urgency": {
      "main": [
        [
          {
            "node": "Skip NOTICE to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip NOTICE to Slack": {
      "main": [
        [
          {
            "node": "Slack @here Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Gmail Owner Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Investment Performance Anomaly & Drawdown Alert

Queries portfolio_performance hourly, flags accounts with drawdowns ≥5% (MEDIUM), ≥8% (HIGH), or ≥15% (CRITICAL), or daily vol spikes ≥3%. Per-account $getWorkflowStaticData dedup with 4-hour TTL prevents flooding. Logs every alert to performance_alerts — the documented-review paper trail that satisfies SEC Investment Advisers Act Rule 206(4)-7 (compliance program) and fiduciary duty under Section 206.

{
  "name": "WealthTech \u2014 Investment Performance Anomaly & Drawdown Alert",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 1
            }
          ]
        }
      },
      "id": "t4",
      "name": "Hourly",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT account_id, account_name, strategy, current_value, peak_value, ROUND((current_value - peak_value) / NULLIF(peak_value, 0) * 100, 2) AS drawdown_pct, daily_vol_pct, benchmark_return_pct, portfolio_return_pct FROM portfolio_performance WHERE updated_at > NOW() - INTERVAL '2 hours'"
      },
      "id": "db4",
      "name": "Query Portfolio Performance",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const state = $getWorkflowStaticData('global');\nconst now = Date.now();\nconst dedupTtl = 4 * 3600 * 1000;\nconst alerts = [];\nfor (const item of $input.all()) {\n  const p = item.json;\n  const drawdown = parseFloat(p.drawdown_pct || 0);\n  const vol = parseFloat(p.daily_vol_pct || 0);\n  let severity;\n  if (drawdown <= -15 || vol >= 5) severity = 'CRITICAL';\n  else if (drawdown <= -8 || vol >= 3) severity = 'HIGH';\n  else if (drawdown <= -5) severity = 'MEDIUM';\n  else continue;\n  const key = `${p.account_id}_${severity}`;\n  if (state[key] && now - state[key] < dedupTtl) continue;\n  state[key] = now;\n  alerts.push({ json: { ...p, severity, drawdown_pct: drawdown, ts: new Date().toISOString() } });\n}\n$setWorkflowStaticData('global', state);\nreturn alerts;"
      },
      "id": "c4",
      "name": "Classify Severity + Dedup",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "channel": "#risk-management",
        "text": "{{ $json.severity === 'CRITICAL' ? ':rotating_light:' : ':warning:' }} *{{ $json.severity }}* drawdown \u2014 *{{ $json.account_name }}* ({{ $json.strategy }})\nDrawdown: {{ $json.drawdown_pct }}% | Daily Vol: {{ $json.daily_vol_pct }}%\nPortfolio: {{ $json.portfolio_return_pct }}% vs Benchmark: {{ $json.benchmark_return_pct }}%\n_SEC fiduciary duty requires documented review for CRITICAL drawdowns_"
      },
      "id": "s4",
      "name": "Slack Risk Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        900,
        200
      ]
    },
    {
      "parameters": {
        "operation": "insert",
        "table": "performance_alerts",
        "columns": "account_id,severity,drawdown_pct,daily_vol_pct,ts",
        "additionalFields": {
          "onConflict": "DO NOTHING"
        }
      },
      "id": "db4b",
      "name": "Fiduciary Duty Audit Log",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        900,
        440
      ]
    }
  ],
  "connections": {
    "Hourly": {
      "main": [
        [
          {
            "node": "Query Portfolio Performance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Portfolio Performance": {
      "main": [
        [
          {
            "node": "Classify Severity + Dedup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Severity + Dedup": {
      "main": [
        [
          {
            "node": "Slack Risk Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fiduciary Duty Audit Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  }
}
Enter fullscreen mode Exit fullscreen mode

5. Weekly WealthTech Platform KPI Dashboard

Every Monday at 8AM, queries platform metrics (AUM, ARR, advisors, clients, new signups) and account health (at-risk count, churn, avg health score) in parallel, merges them, computes WoW% changes with color-coded HTML, and sends the report to CEO/CCO/CFO via Gmail with a one-liner to Slack #exec-kpis. $getWorkflowStaticData stores prior-week values so WoW comparisons work without a separate database table.

{
  "name": "WealthTech \u2014 Weekly Platform KPI Dashboard",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "id": "t5",
      "name": "Monday 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT SUM(aum_usd) AS total_aum, SUM(arr_usd) AS total_arr, COUNT(DISTINCT advisor_id) AS total_advisors, COUNT(DISTINCT client_id) AS total_clients, COUNT(CASE WHEN created_at > NOW() - INTERVAL '7 days' THEN 1 END) AS new_advisors_7d FROM platform_metrics"
      },
      "id": "db5a",
      "name": "Platform Metrics",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        460,
        200
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(CASE WHEN health_score < 40 THEN 1 END) AS at_risk_accounts, COUNT(CASE WHEN status = 'churned' AND updated_at > NOW() - INTERVAL '7 days' THEN 1 END) AS churned_7d, AVG(health_score) AS avg_health_score FROM account_health"
      },
      "id": "db5b",
      "name": "Account Health",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        460,
        500
      ]
    },
    {
      "parameters": {
        "mode": "combine",
        "combinationMode": "mergeByPosition",
        "options": {}
      },
      "id": "m5",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        680,
        350
      ]
    },
    {
      "parameters": {
        "jsCode": "const d = $input.first().json;\nconst state = $getWorkflowStaticData('global');\nconst fmt = (n, d=0) => n ? Number(n).toLocaleString('en-US', {maximumFractionDigits: d}) : '0';\nconst fmtB = n => n >= 1e9 ? `$${(n/1e9).toFixed(2)}B` : n >= 1e6 ? `$${(n/1e6).toFixed(1)}M` : `$${fmt(n)}`;\nconst wow = (cur, prev) => prev ? (((cur - prev) / prev) * 100).toFixed(1) + '%' : 'N/A';\nconst color = (pct) => parseFloat(pct) >= 2 ? '#27ae60' : parseFloat(pct) >= 0 ? '#f39c12' : '#e74c3c';\nconst aumWow = wow(d.total_aum, state.prev_aum);\nconst arrWow = wow(d.total_arr, state.prev_arr);\nstate.prev_aum = d.total_aum;\nstate.prev_arr = d.total_arr;\n$setWorkflowStaticData('global', state);\nconst html = `<html><body style='font-family:Arial;max-width:600px'><h2 style='color:#1a1a2e'>Weekly WealthTech KPI Report</h2><table width='100%' cellpadding='8' style='border-collapse:collapse'><tr style='background:#f0f4f8'><th align='left'>Metric</th><th align='right'>This Week</th><th align='right'>WoW</th></tr><tr><td>Total AUM</td><td align='right'>${fmtB(d.total_aum)}</td><td align='right' style='color:${color(aumWow)}'>${aumWow}</td></tr><tr style='background:#f9f9f9'><td>Annual Recurring Revenue</td><td align='right'>${fmtB(d.total_arr)}</td><td align='right' style='color:${color(arrWow)}'>${arrWow}</td></tr><tr><td>Active Advisors</td><td align='right'>${fmt(d.total_advisors)}</td><td></td></tr><tr style='background:#f9f9f9'><td>New Advisors (7d)</td><td align='right'>${fmt(d.new_advisors_7d)}</td><td></td></tr><tr><td>At-Risk Accounts</td><td align='right' style='color:${d.at_risk_accounts > 10 ? '#e74c3c' : '#27ae60'}'>${fmt(d.at_risk_accounts)}</td><td></td></tr><tr style='background:#f9f9f9'><td>Avg Health Score</td><td align='right'>${parseFloat(d.avg_health_score || 0).toFixed(1)}/100</td><td></td></tr></table><p style='color:#666;font-size:12px'>Generated by n8n self-hosted \u2014 data never leaves your VPC</p></body></html>`;\nreturn [{ json: { html, aumWow, arrWow, ...d } }];"
      },
      "id": "c5",
      "name": "Build KPI Report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        350
      ]
    },
    {
      "parameters": {
        "toEmail": "ceo@yourwealthtech.com",
        "additionalFields": {
          "bcc": "cto@yourwealthtech.com,cco@yourwealthtech.com,cf\u043e@yourwealthtech.com"
        },
        "subject": "Weekly WealthTech KPI \u2014 AUM {{ $json.aumWow }} | ARR {{ $json.arrWow }}",
        "emailType": "html",
        "message": "={{ $json.html }}"
      },
      "id": "e5",
      "name": "Gmail CEO Report",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1100,
        250
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "channel": "#exec-kpis",
        "text": ":bar_chart: Weekly KPIs \u2014 AUM: {{ $json.total_aum | $json.aumWow > 0 ? ':arrow_up:' : ':arrow_down:' }} {{ $json.aumWow }} | ARR {{ $json.arrWow }} | At-Risk: {{ $json.at_risk_accounts }}"
      },
      "id": "s5",
      "name": "Slack Exec Summary",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.3,
      "position": [
        1100,
        450
      ]
    }
  ],
  "connections": {
    "Monday 8AM": {
      "main": [
        [
          {
            "node": "Platform Metrics",
            "type": "main",
            "index": 0
          },
          {
            "node": "Account Health",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Platform Metrics": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Account Health": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Build KPI Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build KPI Report": {
      "main": [
        [
          {
            "node": "Gmail CEO Report",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack Exec Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  }
}
Enter fullscreen mode Exit fullscreen mode

Why Self-Hosted n8n vs. Zapier / Make for WealthTech

Requirement n8n (self-hosted) Zapier Make.com
SEC Rule 17a-4 WORM records ✅ Your WORM storage — full control ❌ 30-day task log, not Rule 17a-4 compliant ❌ Same — task logs not compliant
FINRA 4511/4512 books & records ✅ Postgres audit log, audit-ready ❌ Shared task log, not FINRA-exam defensible ❌ Shared log, same gap
MiFID II Art.16(7) 5-year records ✅ EU data stays in your EU VPC ❌ US Zapier servers = sub-processor DPA gap ❌ EU-hosted option but still third-party sub-processor
SEC Reg S-P privacy ✅ No third-party data sharing ❌ Client portfolio data flows through Zapier ❌ Same
CFTC Part 45 swap records ✅ Your DB — permanent retention ❌ 30-day log — CFTC requires 5-year retention ❌ Same
Cost (10M events/mo) ~$300/mo VPS ~$50,000+/yr ~$20,000+/yr
Complex branching logic ✅ Native Code nodes + IF branching ⚠️ Limited, expensive ⚠️ Module-based, slower
SOC 2 Type II vendor review ✅ No additional vendor needed ❌ Annual Zapier vendor assessment required ❌ Same

Ready-Made n8n Templates for WealthTech Teams

These 5 workflows are available as import-ready templates in the FlowKit n8n Template Store:

Or grab the Complete FlowKit Bundle (15 templates, $97) — all the workflows above plus invoice generation, price monitoring, social cross-posting, and more.

All templates are import-ready JSON — drop into your n8n instance, add credentials, and run.

Top comments (0)