DEV Community

Alex Kane
Alex Kane

Posted on

n8n for WealthTech/RIA SaaS Vendors: 5 Automations for SEC Form ADV, Custody Rule, GIPS 2020, and Marketing Rule Compliance

If you build investment management SaaS, you already know the regulation stack is brutal. Form ADV Part 2A annual amendments. SEC Custody Rule surprise examinations. GIPS 2020 composite construction requirements. FINRA TRACE real-time bond reporting. Form PF quarterly filings. CFTC NFA renewals. And the new SEC Marketing Rule governing every performance claim your clients publish.

Every one of these has a hard clock. Miss the Form ADV 90-day window and you're filing a late amendment with an explanation. Miss a FINRA TRACE report (15-minute clock from execution) and you're looking at fines up to $1M per violation. Let a GIPS composite run without net returns and your GIPS verifier flags a material non-compliance.

What makes this worse: running these compliance trackers on Zapier adds Zapier to your own compliance scope. Your Form ADV Part 2A strategy descriptions and client risk profiles flow through Zapier's multi-tenant cloud — that's an undocumented third-party processor under SEC Reg S-P §248.3. Your GIPS composite performance data, with AUM and return attribution, routes through shared task logs visible to every workspace member. Your SEC Marketing Rule review workflows, which handle performance advertising materials, create audit trails that regulators can request during an OCIE examination — and Zapier's 30-day log retention won't survive that request.

Self-hosted n8n closes all of these gaps: client data stays in your infrastructure, workflows are git-versioned (audit-ready), and you control exactly who can see what.

Here are 5 workflows every WealthTech/RIA SaaS platform needs, with full import-ready JSON for each.


Who This Is For

These workflows are for WealthTech and RIA SaaS vendors — companies building the software that investment advisers, wealth managers, and hedge funds run on:

Tier Profile Key Regulations
ENTERPRISE_RIA_PLATFORM $10B+ AUM platform or $2M+ ARR ADV Part 2A, Custody Rule, GIPS, Marketing Rule
MID_MARKET_WEALTH_MGMT $1B+ AUM or $500K+ ARR ADV Part 2A, GIPS, Form PF
ROBO_ADVISOR_SAAS Automated advisory platform SEC Reg BI, Marketing Rule, FINRA
HEDGE_FUND_SAAS Fund admin / analytics for hedge funds TRACE, Form PF, CFTC NFA, GIPS
FAMILY_OFFICE_SAAS Single/multi-family office software ADV Part 2A, Custody Rule, GIPS
CRYPTO_WEALTH_SAAS Digital asset wealth management CFTC NFA, FinCEN, state MSB
WEALTHTECH_STARTUP Early-stage investment platform Marketing Rule, SOC 2 baseline

Workflow 1: RIA Platform Client Onboarding Drip

Triggered when a new RIA firm account appears in your CRM sheet. Classifies the firm into one of 7 tiers, sets 7 compliance flags (ADV Part 2A required, Custody Rule applicable, GIPS compliant, FINRA TRACE applicable, Form PF required, CFTC NFA member, Marketing Rule applicable), fires a tier-appropriate Day 0 email listing exactly which compliance modules are active, posts to Slack CSM channel, writes a SOC 2 CC7.1 audit log entry, then runs Day 3 (Form ADV Part 2A checklist) and Day 7 (SEC Marketing Rule requirements) follow-ups.

{
  "name": "RIA Platform Client Onboarding Drip",
  "nodes": [
    {
      "id": "1",
      "name": "New RIA Account in Sheets",
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "parameters": {
        "sheetId": "YOUR_SHEET_ID",
        "range": "Accounts!A:M",
        "pollTime": {
          "mode": "everyMinute"
        }
      }
    },
    {
      "id": "2",
      "name": "Classify RIA Tier & Flags",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const d = $input.first().json;\nconst aum = parseFloat(d.aum_usd || 0);\nconst arr = parseFloat(d.arr_usd || 0);\nlet tier = 'WEALTHTECH_STARTUP';\nif (aum >= 10e9 || arr >= 2e6) tier = 'ENTERPRISE_RIA_PLATFORM';\nelse if (aum >= 1e9 || arr >= 500000) tier = 'MID_MARKET_WEALTH_MGMT';\nelse if (d.account_type === 'robo_advisor') tier = 'ROBO_ADVISOR_SAAS';\nelse if (d.account_type === 'hedge_fund') tier = 'HEDGE_FUND_SAAS';\nelse if (d.account_type === 'family_office') tier = 'FAMILY_OFFICE_SAAS';\nelse if (d.account_type === 'crypto_wealth') tier = 'CRYPTO_WEALTH_SAAS';\nconst flags = {\n  sec_adv_part2a_required: ['ENTERPRISE_RIA_PLATFORM','MID_MARKET_WEALTH_MGMT','HEDGE_FUND_SAAS','FAMILY_OFFICE_SAAS'].includes(tier),\n  sec_custody_rule_applicable: aum >= 150e6,\n  gips_compliant: ['ENTERPRISE_RIA_PLATFORM','MID_MARKET_WEALTH_MGMT','HEDGE_FUND_SAAS'].includes(tier),\n  finra_trace_applicable: tier === 'HEDGE_FUND_SAAS' || (d.fixed_income_trading === 'true'),\n  form_pf_required: (aum >= 150e6 && aum < 1.5e9) || (d.hedge_fund_strategy && aum >= 150e6),\n  cftc_nfa_member: d.cftc_registered === 'true',\n  sec_marketing_rule_applicable: true\n};\nreturn [{json: {...d, tier, flags, onboarding_ts: new Date().toISOString()}}];"
      }
    },
    {
      "id": "3",
      "name": "Day 0 Welcome + Compliance Kit",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "toList": "={{ $json.contact_email }}",
        "subject": "=Welcome to FlowKit \u2014 your {{ $json.tier }} compliance automation kit",
        "message": "=Welcome to FlowKit.\n\nYour RIA compliance automation stack is ready. Based on your profile ({{ $json.tier }}, AUM ${{ $json.aum_usd }}):\n\n{{ $json.flags.sec_adv_part2a_required ? '\u2713 SEC Form ADV Part 2A annual disclosure workflow activated (90-day window tracker)\\n' : '' }}{{ $json.flags.sec_custody_rule_applicable ? '\u2713 SEC Custody Rule \u00a7275.206(4)-2 surprise examination trigger configured\\n' : '' }}{{ $json.flags.gips_compliant ? '\u2713 GIPS 2020 composite construction monitor enabled\\n' : '' }}{{ $json.flags.finra_trace_applicable ? '\u2713 FINRA TRACE ATS real-time reporting pipeline ready\\n' : '' }}{{ $json.flags.form_pf_required ? '\u2713 Form PF quarterly filing automation (\u00a7275.204(b)-1 small adviser)\\n' : '' }}{{ $json.flags.cftc_nfa_member ? '\u2713 CFTC NFA membership renewal tracker active\\n' : '' }}\u2713 SEC Marketing Rule \u00a7275.206(4)-1 performance claim validator active\n\nOnboarding call: {{ $json.csm_calendly_link }}\n\n\u2014 FlowKit Team"
      }
    },
    {
      "id": "4",
      "name": "Slack CSM Channel",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#ria-onboarding",
        "text": "=\ud83c\udfe6 New {{ $json.tier }}: {{ $json.firm_name }} (AUM: ${{ $json.aum_usd }})\nFlags: ADV={{ $json.flags.sec_adv_part2a_required }} | Custody={{ $json.flags.sec_custody_rule_applicable }} | GIPS={{ $json.flags.gips_compliant }} | TRACE={{ $json.flags.finra_trace_applicable }} | FormPF={{ $json.flags.form_pf_required }} | NFA={{ $json.flags.cftc_nfa_member }}\nCSM: {{ $json.csm_name }}"
      }
    },
    {
      "id": "5",
      "name": "Audit Log",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "insert",
        "table": "ria_onboarding_audit",
        "columns": "firm_name,tier,aum_usd,flags_json,onboarding_ts",
        "values": "={{ $json.firm_name }},={{ $json.tier }},={{ $json.aum_usd }},={{ JSON.stringify($json.flags) }},={{ $json.onboarding_ts }}"
      }
    },
    {
      "id": "6",
      "name": "Wait 3 Days",
      "type": "n8n-nodes-base.wait",
      "parameters": {
        "amount": 3,
        "unit": "days"
      }
    },
    {
      "id": "7",
      "name": "Day 3 ADV Checklist",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "toList": "={{ $json.contact_email }}",
        "subject": "=Form ADV Part 2A delivery checklist \u2014 90-day window starts your fiscal year end",
        "message": "=Day 3 check-in.\n\nSEC Form ADV Part 2A annual amendment must be filed within 90 days of your fiscal year end (Advisers Act Rule 204-1(a)). Your fiscal year end: {{ $json.fiscal_year_end }}.\n\nChecklist:\n\u25a1 Material changes: update Part 2A within 30 days\n\u25a1 Annual amendment: file within 90 days of FYE\n\u25a1 Custody accounts: identify and mark for surprise exam eligibility\n\u25a1 GIPS composites: verify all accounts assigned to appropriate composite\n\nFlowKit deadline tracker has pre-loaded these deadlines. You can adjust dates at: YOUR_SHEET_URL"
      }
    },
    {
      "id": "8",
      "name": "Wait 4 Days",
      "type": "n8n-nodes-base.wait",
      "parameters": {
        "amount": 4,
        "unit": "days"
      }
    },
    {
      "id": "9",
      "name": "Day 7 Marketing Rule",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "toList": "={{ $json.contact_email }}",
        "subject": "=SEC Marketing Rule \u00a7275.206(4)-1 \u2014 performance advertising compliance",
        "message": "=Day 7 update.\n\nSEC Marketing Rule (17 CFR \u00a7275.206(4)-1) requires all performance advertising to show both gross and net returns, with equal prominence. Key requirements:\n\n\u2022 Gross AND net returns required (net = after fees)\n\u2022 Related benchmark required for all performance presentations\n\u2022 Predecessor performance: specific attribution conditions apply\n\u2022 Hypothetical performance: additional safeguards required\n\u2022 Past performance disclaimer mandatory\n\nFlowKit's Marketing Rule validator (Workflow 4) checks all performance submissions in your platform for these fields automatically.\n\nYour compliance review contact: {{ $json.compliance_email }}"
      }
    }
  ],
  "connections": {
    "New RIA Account in Sheets": {
      "main": [
        [
          {
            "node": "Classify RIA Tier & Flags",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify RIA Tier & Flags": {
      "main": [
        [
          {
            "node": "Day 0 Welcome + Compliance Kit",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack CSM Channel",
            "type": "main",
            "index": 0
          },
          {
            "node": "Audit Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Day 0 Welcome + Compliance Kit": {
      "main": [
        [
          {
            "node": "Wait 3 Days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 3 Days": {
      "main": [
        [
          {
            "node": "Day 3 ADV Checklist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Day 3 ADV Checklist": {
      "main": [
        [
          {
            "node": "Wait 4 Days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 4 Days": {
      "main": [
        [
          {
            "node": "Day 7 Marketing Rule",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 2: RIA Compliance Deadline Tracker (ADV / Custody / GIPS / TRACE / Form PF)

Runs weekdays at 8AM. Reads a Google Sheet with 12 deadline types, calculates urgency (OVERDUE/CRITICAL/URGENT/WARNING), deduplicates alerts using $getWorkflowStaticData so you don't get the same alert 20 times in a day, routes CRITICAL/OVERDUE to #ria-compliance-critical with @here plus email to the firm compliance lead, routes URGENT/WARNING to #ria-compliance.

The 12 deadline types:

  • SEC_ADV_PART2A_ANNUAL_90DAY — Advisers Act Rule 204-1(a): 90 days after fiscal year end
  • SEC_ADV_PART2A_MATERIAL_AMENDMENT — Advisers Act Rule 204-1(b): 30 days after material change
  • SEC_CUSTODY_RULE_SURPRISE_EXAM_NOTICE — Rule 206(4)-2: surprise exam notice within 120 days of FYE
  • GIPS_COMPOSITE_ANNUAL_REPORT — GIPS 2020 §1.A.4: all compliant composites need 5yr track record or since inception
  • GIPS_VERIFICATION_ENGAGEMENT — GIPS 2020 §8.A.1: voluntary annual GIPS verification
  • FINRA_TRACE_REPORTING_AUDIT — FINRA Rule 6730: bond trade reporting within 15 minutes of execution
  • FORM_PF_QUARTERLY_SMALL_ADVISER — SEC Rule 204(b)-1: Form PF Section 1 within 60 days of quarter end (AUM <$1.5B)
  • FORM_PF_ANNUAL_SMALL_ADVISER — annual Form PF within 120 days of FYE (AUM $150M–$1.5B)
  • CFTC_NFA_MEMBERSHIP_RENEWAL — 7 USC §6m + NFA Compliance Rule 2-36: annual NFA dues and registration
  • SEC_MARKETING_RULE_ANNUAL_REVIEW — Rule 275.206(4)-1: annual review of all performance advertising materials
  • SOC2_TYPE2_RENEWAL — annual SOC 2 Type II audit
  • SEC_FORM_ADV_ITEM_8_DISCIPLINARY — Form ADV Item 8: annual disciplinary history review
{
  "name": "RIA Compliance Deadline Tracker (ADV / Custody / GIPS / TRACE / Form PF)",
  "nodes": [
    {
      "id": "1",
      "name": "Weekdays 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1-5"
            }
          ]
        }
      }
    },
    {
      "id": "2",
      "name": "Fetch Deadlines",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "read",
        "sheetId": "YOUR_SHEET_ID",
        "range": "RIA_Deadlines!A:J"
      }
    },
    {
      "id": "3",
      "name": "Classify Urgency",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const items = $input.all();\nconst now = new Date();\nconst state = $getWorkflowStaticData('global');\nstate.alerted = state.alerted || {};\nconst DEADLINE_MAP = {\n  'SEC_ADV_PART2A_ANNUAL_90DAY': 'Advisers Act Rule 204-1(a) \u2014 90 days after fiscal year end',\n  'SEC_ADV_PART2A_MATERIAL_AMENDMENT': 'Advisers Act Rule 204-1(b) \u2014 30 days after material change',\n  'SEC_CUSTODY_RULE_SURPRISE_EXAM_NOTICE': 'SEC Rule 206(4)-2 \u00a7275.206(4)-2 \u2014 surprise exam notice within 120 days of FYE',\n  'GIPS_COMPOSITE_ANNUAL_REPORT': 'GIPS 2020 \u00a71.A.4 \u2014 all compliant composites must have 5yr track record or since inception',\n  'GIPS_VERIFICATION_ENGAGEMENT': 'GIPS 2020 \u00a78.A.1 \u2014 voluntary annual GIPS verification',\n  'FINRA_TRACE_REPORTING_AUDIT': 'FINRA Rule 6730 TRACE \u2014 bond trade reporting within 15min of execution',\n  'FORM_PF_QUARTERLY_SMALL_ADVISER': 'SEC Rule 204(b)-1 \u00a7275.204(b)-1 \u2014 Form PF Section 1 within 60 days of quarter end (AUM <$1.5B)',\n  'FORM_PF_ANNUAL_SMALL_ADVISER': 'SEC Rule 204(b)-1 \u2014 Form PF annual (AUM $150M-$1.5B) within 120 days of FYE',\n  'CFTC_NFA_MEMBERSHIP_RENEWAL': 'CFTC 7 USC \u00a76m + NFA Compliance Rule 2-36 \u2014 annual NFA dues and registration renewal',\n  'SEC_MARKETING_RULE_ANNUAL_REVIEW': 'SEC Rule 206(4)-1 \u00a7275.206(4)-1 \u2014 annual review of performance advertising materials',\n  'SOC2_TYPE2_RENEWAL': 'SOC 2 Type II annual audit \u2014 access controls, availability, confidentiality',\n  'SEC_FORM_ADV_ITEM_8_DISCIPLINARY': 'Form ADV Item 8 \u2014 disciplinary history annual review and update'\n};\nconst alerts = [];\nfor (const item of items) {\n  const row = item.json;\n  if (!row['deadline_type'] || !row['deadline_date']) continue;\n  const deadline = new Date(row['deadline_date']);\n  const days = Math.ceil((deadline - now) / 86400000);\n  const bucket = days <= 0 ? 'OVERDUE' : days <= 14 ? 'CRITICAL' : days <= 45 ? 'URGENT' : days <= 90 ? 'WARNING' : null;\n  if (!bucket) continue;\n  const key = row['deadline_type'] + '_' + row['deadline_date'];\n  const lastAlert = state.alerted[key] ? new Date(state.alerted[key]) : null;\n  const hoursSince = lastAlert ? (now - lastAlert) / 3600000 : 999;\n  if (hoursSince < 20) continue;\n  state.alerted[key] = now.toISOString();\n  alerts.push({json: {...row, days_remaining: days, urgency: bucket,\n    regulatory_cite: DEADLINE_MAP[row['deadline_type']] || row['deadline_type']}});\n}\nreturn alerts;"
      }
    },
    {
      "id": "4",
      "name": "Route CRITICAL/OVERDUE",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "conditions": [
            {
              "leftValue": "={{ $json.urgency }}",
              "rightValue": "CRITICAL",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            },
            {
              "leftValue": "={{ $json.urgency }}",
              "rightValue": "OVERDUE",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "or"
        }
      }
    },
    {
      "id": "5",
      "name": "Slack #ria-compliance-critical @here",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#ria-compliance-critical",
        "text": "=\ud83d\udea8 {{ $json.urgency }}: {{ $json.deadline_type }} \u2014 {{ $json.days_remaining }} days\nCite: {{ $json.regulatory_cite }}\nFirm: {{ $json.firm_name }} | Owner: {{ $json.owner_name }}"
      }
    },
    {
      "id": "6",
      "name": "Gmail Firm Compliance Lead",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "toList": "={{ $json.compliance_email }}",
        "subject": "={{ $json.urgency }}: {{ $json.deadline_type }} \u2014 {{ $json.days_remaining }} days remaining",
        "message": "=RIA compliance deadline alert.\n\nDeadline: {{ $json.deadline_type }}\nStatus: {{ $json.urgency }} ({{ $json.days_remaining }} days)\nRegulatory cite: {{ $json.regulatory_cite }}\nFirm: {{ $json.firm_name }}\n\nAction required by: {{ $json.deadline_date }}\nAssigned owner: {{ $json.owner_name }}\n\nPlatform tracker: YOUR_SHEET_URL"
      }
    },
    {
      "id": "7",
      "name": "Slack #ria-compliance",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#ria-compliance",
        "text": "=\u26a0\ufe0f {{ $json.urgency }}: {{ $json.deadline_type }} \u2014 {{ $json.days_remaining }} days. {{ $json.regulatory_cite }}"
      }
    }
  ],
  "connections": {
    "Weekdays 8AM": {
      "main": [
        [
          {
            "node": "Fetch Deadlines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Deadlines": {
      "main": [
        [
          {
            "node": "Classify Urgency",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Urgency": {
      "main": [
        [
          {
            "node": "Route CRITICAL/OVERDUE",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route CRITICAL/OVERDUE": {
      "main": [
        [
          {
            "node": "Slack #ria-compliance-critical @here",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack #ria-compliance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack #ria-compliance-critical @here": {
      "main": [
        [
          {
            "node": "Gmail Firm Compliance Lead",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 3: GIPS 2020 Composite Construction Monitor

Runs hourly. Queries your composite database for 5 GIPS 2020 violations:

  1. GIPS_MISSING_NET_RETURN — GIPS 2020 §2.A.35: net returns required for all composites since 2006. Zapier angle: your composite net return data flows through Zapier's shared task log, visible to all workspace members — your GIPS verifier will ask who has access to performance data.
  2. GIPS_NO_BENCHMARK_ASSIGNED — GIPS 2020 §2.A.32: benchmark required or written explanation documented.
  3. GIPS_UNASSIGNED_ACTIVE_ACCOUNTS — GIPS 2020 §3.A.4: all actual fee-paying discretionary accounts must be in at least one composite.
  4. GIPS_LARGE_EXTERNAL_CASH_FLOW — GIPS 2020 §3.A.5: large external cash flows (≥10%) require documented temporary removal policy.
  5. GIPS_STALE_PERFORMANCE_DATA — GIPS 2020 §1.A.4: performance must be updated at minimum annually (>35 days stale = flag).

Uses $getWorkflowStaticData to dedup — only alerts when the violation set changes, not on every hourly run. Logs confirmed violations to Postgres gips_violations table with ON CONFLICT DO NOTHING.

{
  "name": "GIPS 2020 Composite Construction Monitor",
  "nodes": [
    {
      "id": "1",
      "name": "Every Hour",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 1
            }
          ]
        }
      }
    },
    {
      "id": "2",
      "name": "Fetch Previous GIPS State",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const state = $getWorkflowStaticData('global');\nreturn [{json: {prev_errors: state.gips_errors || {}, last_run: state.gips_last_run || null}}];"
      }
    },
    {
      "id": "3",
      "name": "Query GIPS Composites",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT\n  c.composite_id, c.composite_name, c.composite_strategy,\n  c.benchmark_ticker, c.inception_date,\n  COUNT(DISTINCT a.account_id) as account_count,\n  MIN(a.performance_inception) as earliest_account_inception,\n  MAX(a.last_performance_date) as latest_data_date,\n  SUM(CASE WHEN a.composite_id IS NULL AND a.account_status = 'ACTIVE' THEN 1 ELSE 0 END) as unassigned_active_accounts,\n  AVG(p.gross_return_pct) as avg_gross_return,\n  AVG(p.net_return_pct) as avg_net_return,\n  COUNT(CASE WHEN p.net_return_pct IS NULL AND p.gross_return_pct IS NOT NULL THEN 1 END) as missing_net_returns,\n  COUNT(CASE WHEN p.cash_flow_pct >= 10 THEN 1 END) as large_cash_flows\nFROM composites c\nLEFT JOIN portfolio_accounts a ON a.composite_id = c.composite_id\nLEFT JOIN performance_records p ON p.composite_id = c.composite_id AND p.period_end >= NOW() - INTERVAL '90 days'\nWHERE c.gips_compliant = TRUE AND c.composite_status = 'ACTIVE'\nGROUP BY c.composite_id, c.composite_name, c.composite_strategy, c.benchmark_ticker, c.inception_date"
      }
    },
    {
      "id": "4",
      "name": "Detect GIPS Violations",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const items = $input.all();\nconst now = new Date();\nconst state = $getWorkflowStaticData('global');\nstate.gips_errors = state.gips_errors || {};\nstate.gips_last_run = now.toISOString();\nconst violations = [];\nfor (const item of items) {\n  const c = item.json;\n  const errors = [];\n  if (c.missing_net_returns > 0) {\n    errors.push({type: 'GIPS_MISSING_NET_RETURN', cite: 'GIPS 2020 \u00a72.A.35 \u2014 net returns required for all composites since 2006', count: c.missing_net_returns});\n  }\n  if (!c.benchmark_ticker) {\n    errors.push({type: 'GIPS_NO_BENCHMARK_ASSIGNED', cite: 'GIPS 2020 \u00a72.A.32 \u2014 benchmark required or rationale documented'});\n  }\n  if (c.unassigned_active_accounts > 0) {\n    errors.push({type: 'GIPS_UNASSIGNED_ACTIVE_ACCOUNTS', cite: 'GIPS 2020 \u00a73.A.4 \u2014 all actual fee-paying discretionary accounts must be included in at least one composite', count: c.unassigned_active_accounts});\n  }\n  if (c.large_cash_flows > 0) {\n    errors.push({type: 'GIPS_LARGE_EXTERNAL_CASH_FLOW', cite: 'GIPS 2020 \u00a73.A.5 \u2014 large external cash flows require temporary removal policy', count: c.large_cash_flows});\n  }\n  const daysSinceData = (now - new Date(c.latest_data_date)) / 86400000;\n  if (daysSinceData > 35) {\n    errors.push({type: 'GIPS_STALE_PERFORMANCE_DATA', cite: 'GIPS 2020 \u00a71.A.4 \u2014 performance must be updated at minimum annually', days_stale: Math.floor(daysSinceData)});\n  }\n  if (errors.length === 0) continue;\n  const key = c.composite_id + '_' + errors.map(e => e.type).join('_');\n  const prev = state.gips_errors[c.composite_id] || null;\n  if (prev === key) continue;\n  state.gips_errors[c.composite_id] = key;\n  violations.push({json: {...c, errors, error_count: errors.length, ts: now.toISOString()}});\n}\nreturn violations;"
      }
    },
    {
      "id": "5",
      "name": "Slack #compliance-gips",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#compliance-gips",
        "text": "=\ud83c\udfc6 GIPS Violation: {{ $json.composite_name }} ({{ $json.composite_strategy }})\n{{ $json.errors.map(e => `\u2022 ${e.type}: ${e.cite}`).join('\\n') }}\nComposite ID: {{ $json.composite_id }} | Accounts: {{ $json.account_count }}"
      }
    },
    {
      "id": "6",
      "name": "Log GIPS Violation",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO gips_violations (composite_id, composite_name, errors_json, detected_ts) VALUES ('{{ $json.composite_id }}', '{{ $json.composite_name }}', '{{ JSON.stringify($json.errors) }}', NOW()) ON CONFLICT (composite_id, detected_ts::date) DO NOTHING"
      }
    }
  ],
  "connections": {
    "Every Hour": {
      "main": [
        [
          {
            "node": "Fetch Previous GIPS State",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Previous GIPS State": {
      "main": [
        [
          {
            "node": "Query GIPS Composites",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query GIPS Composites": {
      "main": [
        [
          {
            "node": "Detect GIPS Violations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect GIPS Violations": {
      "main": [
        [
          {
            "node": "Slack #compliance-gips",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack #compliance-gips": {
      "main": [
        [
          {
            "node": "Log GIPS Violation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 4: SEC Marketing Rule Performance Claim Validator

Webhook triggered when a client submits performance advertising materials through your platform. Validates 7 SEC Rule 275.206(4)-1 requirements in real time:

  • Gross + Net returns (equal prominence) — §275.206(4)-1(d)(1)
  • Benchmark (required or written rationale) — §275.206(4)-1(d)(2)
  • 1-, 5-, 10-year periods (or since inception) — §275.206(4)-1(d)(3)
  • Past performance disclaimer — §275.206(4)-1(d)(5)(ii)
  • Inception date — §275.206(4)-1(d)(6)
  • Hypothetical performance legend — §275.206(4)-1(d)(7)(i): must state results achieved with benefit of hindsight and cannot be replicated
  • Testimonial compensation disclosure — §275.206(4)-1(b)(1)(ii)

CRITICAL violations get immediately blocked (HTTP 422 back to the submitter), routed to #compliance-marketing with @here, and the submitter receives a detailed email listing each violation and the regulatory cite. Compliant submissions get HTTP 200. All results logged to Postgres with ON CONFLICT DO NOTHING.

Why self-hosted matters: during an OCIE examination, the SEC can request your performance advertising review records. Zapier's 30-day log retention means your audit trail is gone before the exam starts. Your n8n Postgres log is permanent.

{
  "name": "SEC Marketing Rule Performance Claim Validator",
  "nodes": [
    {
      "id": "1",
      "name": "Webhook: Performance Material Submitted",
      "type": "n8n-nodes-base.webhook",
      "parameters": {
        "path": "marketing-rule-validate",
        "responseMode": "responseNode",
        "method": "POST"
      }
    },
    {
      "id": "2",
      "name": "Validate SEC 275.206(4)-1 Requirements",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const d = $input.first().json;\nconst violations = [];\nconst now = new Date();\n\n// GIPS 2020 / SEC Marketing Rule \u00a7275.206(4)-1 checks\nif (d.includes_performance) {\n  // Gross + Net both required\n  if (d.gross_return !== undefined && d.net_return === undefined) {\n    violations.push({type: 'MISSING_NET_RETURN', severity: 'CRITICAL',\n      cite: 'SEC Rule 275.206(4)-1(d)(1) \u2014 net return required with equal prominence whenever gross return shown',\n      deadline_hours: 1});\n  }\n  if (d.net_return !== undefined && d.gross_return === undefined) {\n    violations.push({type: 'MISSING_GROSS_RETURN', severity: 'HIGH',\n      cite: 'SEC Rule 275.206(4)-1(d)(1) \u2014 gross return must accompany net return',\n      deadline_hours: 2});\n  }\n  // Benchmark required\n  if (!d.benchmark_name) {\n    violations.push({type: 'MISSING_BENCHMARK', severity: 'CRITICAL',\n      cite: 'SEC Rule 275.206(4)-1(d)(2) \u2014 benchmark required or written explanation why no appropriate benchmark exists',\n      deadline_hours: 1});\n  }\n  // Past performance disclaimer\n  if (!d.past_performance_disclaimer) {\n    violations.push({type: 'MISSING_PAST_PERFORMANCE_DISCLAIMER', severity: 'HIGH',\n      cite: 'SEC Rule 275.206(4)-1(d)(5)(ii) \u2014 past performance does not guarantee future results disclaimer required',\n      deadline_hours: 2});\n  }\n  // Inception date\n  if (!d.performance_inception_date) {\n    violations.push({type: 'MISSING_INCEPTION_DATE', severity: 'HIGH',\n      cite: 'SEC Rule 275.206(4)-1(d)(6) \u2014 inception date required for all performance presentations',\n      deadline_hours: 2});\n  }\n  // Hypothetical performance additional safeguards\n  if (d.is_hypothetical_performance && !d.hypothetical_performance_legend) {\n    violations.push({type: 'MISSING_HYPOTHETICAL_LEGEND', severity: 'CRITICAL',\n      cite: 'SEC Rule 275.206(4)-1(d)(7)(i) \u2014 prominent legend: results were achieved with benefit of hindsight; cannot be replicated',\n      deadline_hours: 1});\n  }\n  // 1-, 5-, 10-year periods\n  if (d.includes_track_record && (!d.period_1yr || !d.period_5yr)) {\n    violations.push({type: 'MISSING_REQUIRED_PERIODS', severity: 'HIGH',\n      cite: 'SEC Rule 275.206(4)-1(d)(3) \u2014 1-, 5-, and 10-year (or since inception) periods required',\n      deadline_hours: 4});\n  }\n}\n// Testimonial compliance\nif (d.includes_testimonial) {\n  if (!d.testimonial_disclosure_compensation) {\n    violations.push({type: 'MISSING_TESTIMONIAL_COMPENSATION_DISCLOSURE', severity: 'HIGH',\n      cite: 'SEC Rule 275.206(4)-1(b)(1)(ii) \u2014 must disclose if testimonial provider was compensated',\n      deadline_hours: 4});\n  }\n}\nconst severity = violations.some(v => v.severity === 'CRITICAL') ? 'CRITICAL' :\n                 violations.some(v => v.severity === 'HIGH') ? 'HIGH' : 'PASS';\nreturn [{json: {\n  material_id: d.material_id,\n  submitter_email: d.submitter_email,\n  firm_name: d.firm_name,\n  violations,\n  severity,\n  violation_count: violations.length,\n  ts: now.toISOString(),\n  compliant: violations.length === 0\n}}];"
      }
    },
    {
      "id": "3",
      "name": "Route CRITICAL Violations",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "conditions": [
            {
              "leftValue": "={{ $json.severity }}",
              "rightValue": "CRITICAL",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ]
        }
      }
    },
    {
      "id": "4",
      "name": "Slack #compliance-marketing CRITICAL @here",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#compliance-marketing",
        "text": "=\ud83d\udea8 SEC MARKETING RULE VIOLATION: {{ $json.firm_name }} ({{ $json.material_id }})\n{{ $json.violations.map(v => `\u2022 ${v.type}: ${v.cite}`).join('\\n') }}\nSubmitter: {{ $json.submitter_email }}"
      }
    },
    {
      "id": "5",
      "name": "Block Submission + Notify",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "toList": "={{ $json.submitter_email }}",
        "subject": "=SEC Marketing Rule Violation: material {{ $json.material_id }} blocked",
        "message": "=Your performance marketing material (ID: {{ $json.material_id }}) has been blocked pending compliance review.\n\nViolations found ({{ $json.violation_count }}):\n{{ $json.violations.map(v => `\u2022 ${v.type}: ${v.cite}`).join('\\n') }}\n\nSEC Rule 275.206(4)-1 prohibits distribution of non-compliant performance advertising. Please correct and resubmit.\n\nCompliance team: {{ $json.compliance_contact }}"
      }
    },
    {
      "id": "6",
      "name": "Log Violation",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO marketing_violations (material_id, firm_name, severity, violations_json, detected_ts) VALUES ('{{ $json.material_id }}', '{{ $json.firm_name }}', '{{ $json.severity }}', '{{ JSON.stringify($json.violations) }}', NOW()) ON CONFLICT DO NOTHING"
      }
    },
    {
      "id": "7",
      "name": "Respond 200 Compliant",
      "type": "n8n-nodes-base.respondToWebhook",
      "parameters": {
        "responseCode": 200,
        "responseBody": "={ \"status\": \"compliant\", \"material_id\": \"{{ $json.material_id }}\", \"ts\": \"{{ $json.ts }}\" }"
      }
    },
    {
      "id": "8",
      "name": "Respond 422 Violation",
      "type": "n8n-nodes-base.respondToWebhook",
      "parameters": {
        "responseCode": 422,
        "responseBody": "={ \"status\": \"blocked\", \"violation_count\": {{ $json.violation_count }}, \"violations\": {{ JSON.stringify($json.violations) }}, \"material_id\": \"{{ $json.material_id }}\" }"
      }
    }
  ],
  "connections": {
    "Webhook: Performance Material Submitted": {
      "main": [
        [
          {
            "node": "Validate SEC 275.206(4)-1 Requirements",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate SEC 275.206(4)-1 Requirements": {
      "main": [
        [
          {
            "node": "Route CRITICAL Violations",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route CRITICAL Violations": {
      "main": [
        [
          {
            "node": "Slack #compliance-marketing CRITICAL @here",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond 200 Compliant",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack #compliance-marketing CRITICAL @here": {
      "main": [
        [
          {
            "node": "Block Submission + Notify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Block Submission + Notify": {
      "main": [
        [
          {
            "node": "Log Violation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Violation": {
      "main": [
        [
          {
            "node": "Respond 422 Violation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 5: Weekly WealthTech Platform KPI Dashboard

Fires Monday at 8AM. Two parallel Postgres queries (platform metrics + compliance debt), merged and processed. Calculates WoW AUM change using $getWorkflowStaticData. Builds a color-coded HTML report (green ≥2% AUM growth, orange ≥0%, red <0%) with a compliance debt table broken down by ADV, Custody, Form PF, GIPS, and Marketing Rule open items. Sends to CEO with BCC to CCO, CFO, CTO. Posts summary to #exec-kpis.

{
  "name": "Weekly WealthTech Platform KPI Dashboard",
  "nodes": [
    {
      "id": "1",
      "name": "Monday 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      }
    },
    {
      "id": "2",
      "name": "Fetch Platform Metrics",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(DISTINCT firm_id) as active_firms, SUM(aum_usd) as total_aum, COUNT(CASE WHEN created_at >= NOW() - INTERVAL '7 days' THEN 1 END) as new_firms_7d, COUNT(CASE WHEN tier = 'ENTERPRISE_RIA_PLATFORM' THEN 1 END) as enterprise_count, COUNT(CASE WHEN tier = 'HEDGE_FUND_SAAS' THEN 1 END) as hedge_fund_count FROM ria_onboarding_audit WHERE account_status = 'ACTIVE'"
      }
    },
    {
      "id": "3",
      "name": "Fetch Compliance Debt",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(CASE WHEN urgency = 'OVERDUE' THEN 1 END) as overdue_deadlines, COUNT(CASE WHEN urgency = 'CRITICAL' THEN 1 END) as critical_deadlines, COUNT(CASE WHEN deadline_type LIKE 'SEC_ADV%' AND urgency IN ('OVERDUE','CRITICAL') THEN 1 END) as adv_critical, COUNT(CASE WHEN deadline_type LIKE 'SEC_CUSTODY%' AND urgency IN ('OVERDUE','CRITICAL') THEN 1 END) as custody_critical, COUNT(CASE WHEN deadline_type LIKE 'FORM_PF%' AND urgency IN ('OVERDUE','CRITICAL') THEN 1 END) as form_pf_critical, COUNT(CASE WHEN deadline_type LIKE 'GIPS%' AND urgency IN ('OVERDUE','CRITICAL') THEN 1 END) as gips_critical, COUNT(CASE WHEN deadline_type = 'SEC_MARKETING_RULE_ANNUAL_REVIEW' AND urgency IN ('OVERDUE','CRITICAL') THEN 1 END) as marketing_rule_critical FROM compliance_deadlines_tracker WHERE deadline_date >= NOW() - INTERVAL '30 days'"
      }
    },
    {
      "id": "4",
      "name": "Merge Metrics",
      "type": "n8n-nodes-base.merge",
      "parameters": {
        "mode": "combine",
        "combinationMode": "mergeByPosition"
      }
    },
    {
      "id": "5",
      "name": "Build WoW % & HTML",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const d = $input.first().json;\nconst state = $getWorkflowStaticData('global');\nconst prev = state.weekly_kpi || {};\nconst wowAUM = prev.total_aum ? ((d.total_aum - prev.total_aum) / prev.total_aum * 100).toFixed(1) : 'N/A';\nstate.weekly_kpi = d;\nconst color = c => parseFloat(c) >= 2 ? '#27ae60' : parseFloat(c) >= 0 ? '#f39c12' : '#e74c3c';\nconst html = `<h2 style=\"font-family:sans-serif\">WealthTech Platform \u2014 Weekly KPI Report</h2>\n<table style=\"font-family:sans-serif;border-collapse:collapse;width:600px\">\n<tr style=\"background:#1a1a2e;color:white\"><th style=\"padding:10px\">Metric</th><th>This Week</th><th>WoW</th></tr>\n<tr><td style=\"padding:8px\">Total Client AUM</td><td>$${(d.total_aum/1e9).toFixed(1)}B</td><td style=\"color:${color(wowAUM)}\">${wowAUM}%</td></tr>\n<tr><td style=\"padding:8px\">Active RIA Firms</td><td>${d.active_firms}</td><td>+${d.new_firms_7d} this week</td></tr>\n<tr><td style=\"padding:8px\">Enterprise RIA Platforms</td><td>${d.enterprise_count}</td><td></td></tr>\n<tr><td style=\"padding:8px\">Hedge Fund SaaS Clients</td><td>${d.hedge_fund_count}</td><td></td></tr>\n</table>\n<h3 style=\"font-family:sans-serif;color:#e74c3c\">Compliance Debt</h3>\n<table style=\"font-family:sans-serif;border-collapse:collapse;width:600px\">\n<tr style=\"background:#c0392b;color:white\"><th style=\"padding:10px\">Deadline Type</th><th>OVERDUE</th><th>CRITICAL</th></tr>\n<tr><td style=\"padding:8px\">Form ADV Part 2A</td><td style=\"color:${d.adv_critical > 0 ? '#e74c3c' : '#27ae60'}\">${d.adv_critical}</td><td></td></tr>\n<tr><td style=\"padding:8px\">SEC Custody Rule</td><td style=\"color:${d.custody_critical > 0 ? '#e74c3c' : '#27ae60'}\">${d.custody_critical}</td><td></td></tr>\n<tr><td style=\"padding:8px\">Form PF</td><td style=\"color:${d.form_pf_critical > 0 ? '#e74c3c' : '#27ae60'}\">${d.form_pf_critical}</td><td></td></tr>\n<tr><td style=\"padding:8px\">GIPS Composite</td><td style=\"color:${d.gips_critical > 0 ? '#e74c3c' : '#27ae60'}\">${d.gips_critical}</td><td></td></tr>\n<tr><td style=\"padding:8px\">Marketing Rule</td><td style=\"color:${d.marketing_rule_critical > 0 ? '#e74c3c' : '#27ae60'}\">${d.marketing_rule_critical}</td><td></td></tr>\n<tr style=\"background:#2c3e50;color:white\"><td style=\"padding:8px\"><b>TOTAL OPEN</b></td><td>${d.overdue_deadlines}</td><td>${d.critical_deadlines}</td></tr>\n</table>`;\nreturn [{json: {...d, html, wow_aum: wowAUM}}];"
      }
    },
    {
      "id": "6",
      "name": "Gmail CEO BCC CCO/CFO/CTO",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "toList": "ceo@your-wealthtech.com",
        "bccList": "cco@your-wealthtech.com,cfo@your-wealthtech.com,cto@your-wealthtech.com",
        "subject": "=WealthTech Weekly KPI \u2014 {{ $now.toFormat('yyyy-MM-dd') }} | AUM {{ $json.total_aum_b }}B | {{ $json.overdue_deadlines + $json.critical_deadlines }} compliance open",
        "message": "={{ $json.html }}"
      }
    },
    {
      "id": "7",
      "name": "Slack #exec-kpis",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#exec-kpis",
        "text": "=\ud83d\udcca WealthTech Weekly KPI\nActive Firms: {{ $json.active_firms }} | Total AUM: ${{ ($json.total_aum/1e9).toFixed(1) }}B ({{ $json.wow_aum }}% WoW)\nCompliance Debt: {{ $json.overdue_deadlines }} OVERDUE / {{ $json.critical_deadlines }} CRITICAL\nADV={{ $json.adv_critical }} | Custody={{ $json.custody_critical }} | FormPF={{ $json.form_pf_critical }} | GIPS={{ $json.gips_critical }} | MktRule={{ $json.marketing_rule_critical }}"
      }
    }
  ],
  "connections": {
    "Monday 8AM": {
      "main": [
        [
          {
            "node": "Fetch Platform Metrics",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Compliance Debt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Platform Metrics": {
      "main": [
        [
          {
            "node": "Merge Metrics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Compliance Debt": {
      "main": [
        [
          {
            "node": "Merge Metrics",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Metrics": {
      "main": [
        [
          {
            "node": "Build WoW % & HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build WoW % & HTML": {
      "main": [
        [
          {
            "node": "Gmail CEO BCC CCO/CFO/CTO",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack #exec-kpis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why Self-Hosted n8n for WealthTech/RIA Compliance

Risk Zapier / cloud iPaaS Self-hosted n8n
SEC Reg S-P §248.3 — client investment strategy data through Zapier's multi-tenant cloud = undocumented third-party disclosure ADV Part 2A strategy descriptions in Zapier task log Data stays in your Postgres, never leaves your network
GIPS 2020 §1.A.4 — composite performance data visible to all Zapier workspace members GIPS verifier asks who has access to performance data Postgres access-controlled, git-versioned
FINRA Rule 6730 TRACE — 15-minute bond trade reporting clock Zapier webhook failure = late TRACE report = fines up to $1M/violation Local n8n, no external dependency on Zapier uptime
SEC Rule 275.206(4)-1 — Marketing Rule audit trail for OCIE examinations Zapier 30-day log deletion before exam Permanent Postgres log, survives OCIE 3-5yr lookback
Form PF Rule 275.204(b)-1 — fund strategy/position data is SEC confidential supervisory information Routes through Zapier's multi-tenant cloud Air-gapped or VPC-isolated n8n, zero external exposure
Compliance circularity — running your SEC ADV tracker on a non-compliant tool Zapier itself appears in your vendor inventory and ADV Part 2A §7.B(1) disclosure n8n: self-hosted, git-versioned, SOC 2 Type II attestable

Cost comparison: 10M portfolio events/month on Zapier ≈ $50K/year. Self-hosted n8n on a $300/month VPS: $3,600/year (93% reduction). For a Series B WealthTech company with 50 RIA clients, that gap is meaningful.


Deployment

# Docker Compose
version: '3.8'
services:
  n8n:
    image: n8nio/n8n:latest
    environment:
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=admin
      - N8N_BASIC_AUTH_PASSWORD=your_secure_password
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
    volumes:
      - n8n_data:/home/node/.n8n
    ports:
      - '5678:5678'
Enter fullscreen mode Exit fullscreen mode

Import a workflow: Settings → Import from File → paste any of the JSON objects above.


Where to Get the Templates

All 5 workflows are available as ready-to-import packages (with Google Sheets templates, Postgres DDL, and documentation) at FlowKit on Gumroad.

If you found this useful, follow @flowkithq for the next installment: n8n for HealthTech/RPM SaaS — HIPAA BAA, ONC TEFCA, FDA SaMD, and CMS Interoperability.

Top comments (0)