If you build AML, KYC, or regulatory compliance software, you already know the compliance paradox: your customers trust you with transaction data that legally cannot leave a controlled environment.
SAR data is protected by federal law — 31 USC §5318(g) prohibits "tipping off" the subject of a suspicious activity report. FinCEN sanctions screening results are commercially sensitive intelligence. Correspondent banking transaction chains fall under FATF Recommendation 16's travel rule.
Yet every workflow you run through Zapier or Make adds a new GDPR Art.28 sub-processor to your customers' data processing agreements. If your platform handles AML/CFT screening results, routing that data through a third-party cloud automation service creates direct regulatory exposure — for you and for every customer whose transaction data transits Zapier's infrastructure.
Self-hosting n8n on your own infrastructure eliminates that exposure entirely: your workflows run inside your existing security boundary, AML screening results stay in your VPC, and you're one less sub-processor in every customer DPA.
Here are 5 automations RegTech SaaS teams are building with self-hosted n8n.
Workflow 1: AML Screening Result Monitor
Polls Postgres for flagged screening events every 5 minutes and routes CRITICAL alerts to #aml-critical immediately — without any transaction data leaving your network.
What it does:
- Every 5 minutes, query
screening_eventsfor records withconfidence_score >= 0.75from the last 6 minutes - Classify by severity: CRITICAL (≥0.95), HIGH (≥0.85), MEDIUM (≥0.75)
- Deduplicate using
$getWorkflowStaticDataso each event alerts exactly once - Route CRITICAL to
#aml-criticalwith full transaction details; HIGH/MEDIUM to#aml-review
Workflow JSON (import at n8n.io → Workflows → Import):
{
"name": "AML Screening Result Monitor",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 5
}
]
}
},
"name": "Every 5 Minutes",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
240,
300
],
"id": "aml-1"
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT id, customer_id, transaction_id, amount, screening_type, confidence_score, flag_reason, created_at FROM screening_events WHERE created_at > NOW() - INTERVAL '6 minutes' AND confidence_score >= 0.75 ORDER BY confidence_score DESC LIMIT 200",
"options": {}
},
"name": "Get Recent Flags",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
440,
300
],
"id": "aml-2"
},
{
"parameters": {
"jsCode": "const staticData = $getWorkflowStaticData('global');\nconst seenIds = new Set(staticData.seenIds || []);\nconst items = $input.all();\nconst newFlags = items.filter(item => {\n const id = String(item.json.id);\n if (seenIds.has(id)) return false;\n seenIds.add(id);\n return true;\n});\nstaticData.seenIds = [...seenIds].slice(-2000);\nif (newFlags.length === 0) return [];\nreturn newFlags.map(item => {\n const score = parseFloat(item.json.confidence_score);\n const severity = score >= 0.95 ? 'CRITICAL' : score >= 0.85 ? 'HIGH' : 'MEDIUM';\n return { json: { ...item.json, severity } };\n});"
},
"name": "Classify & Deduplicate",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
640,
300
],
"id": "aml-3"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true
},
"conditions": [
{
"leftValue": "={{ $json.severity }}",
"rightValue": "CRITICAL",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
}
},
"name": "Is Critical?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
840,
300
],
"id": "aml-4"
},
{
"parameters": {
"select": "channel",
"channelId": {
"__rl": true,
"value": "#aml-critical",
"mode": "name"
},
"text": "=🚨 *AML CRITICAL ALERT*\n\nCustomer: `{{ $json.customer_id }}`\nTransaction: `{{ $json.transaction_id }}`\nAmount: ${{ $json.amount }}\nConfidence: {{ (parseFloat($json.confidence_score) * 100).toFixed(1) }}%\nType: {{ $json.screening_type }}\nReason: {{ $json.flag_reason }}\n\nImmediate compliance team review required.",
"otherOptions": {}
},
"name": "Slack #aml-critical",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [
1040,
180
],
"id": "aml-5"
},
{
"parameters": {
"select": "channel",
"channelId": {
"__rl": true,
"value": "#aml-review",
"mode": "name"
},
"text": "=⚠️ *AML Flag — {{ $json.severity }}*\n\nCustomer: `{{ $json.customer_id }}` | Score: {{ (parseFloat($json.confidence_score) * 100).toFixed(1) }}%\nReason: {{ $json.flag_reason }}",
"otherOptions": {}
},
"name": "Slack #aml-review",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [
1040,
420
],
"id": "aml-6"
}
],
"connections": {
"Every 5 Minutes": {
"main": [
[
{
"node": "Get Recent Flags",
"type": "main",
"index": 0
}
]
]
},
"Get Recent Flags": {
"main": [
[
{
"node": "Classify & Deduplicate",
"type": "main",
"index": 0
}
]
]
},
"Classify & Deduplicate": {
"main": [
[
{
"node": "Is Critical?",
"type": "main",
"index": 0
}
]
]
},
"Is Critical?": {
"main": [
[
{
"node": "Slack #aml-critical",
"type": "main",
"index": 0
}
],
[
{
"node": "Slack #aml-review",
"type": "main",
"index": 0
}
]
]
}
}
}
Pro tips:
- Add a Postgres INSERT after the Slack nodes to maintain a
sar_queuetable for your compliance team's review workflow - Tune the
confidence_scorethreshold per customer segment (retail vs. high-risk geographies vs. business accounts) - For real-time payment rails, use a Webhook trigger (responseMode: onReceived) instead of polling — you get sub-100ms in-VPC latency vs. Zapier's 2–15s queue
Workflow 2: KYC/KYB Verification API Health Monitor
Catches degraded verification API performance before customers notice failed onboarding flows — critical when KYC failures block payment processing.
What it does:
- Every 5 minutes, ping each verification endpoint (ID verification, business KYB, sanctions screening, PEP check)
- Classify: DOWN (timeout/non-200), DEGRADED (>1.5× SLA), SLOW (>SLA)
- Track first-failure timestamp per endpoint using
$getWorkflowStaticData— one alert per incident, not one per poll - Post to
#kyc-opswith affected customer segment and minutes since first failure
Workflow JSON:
{
"name": "KYC/KYB Verification API Health Monitor",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 5
}
]
}
},
"name": "Every 5 Minutes",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
240,
300
],
"id": "kyc-1"
},
{
"parameters": {
"jsCode": "return [\n { json: { name: 'ID Verification API', url: 'https://api.yourverificationprovider.com/health', customer_segment: 'retail', sla_ms: 2000 } },\n { json: { name: 'Business KYB API', url: 'https://api.kyb.internal/health', customer_segment: 'business', sla_ms: 3000 } },\n { json: { name: 'Sanctions Screening API', url: 'https://sanctions.internal/ping', customer_segment: 'all', sla_ms: 500 } },\n { json: { name: 'PEP Check API', url: 'https://pep.internal/health', customer_segment: 'high-risk', sla_ms: 1000 } }\n];"
},
"name": "Verification Endpoints",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
440,
300
],
"id": "kyc-2"
},
{
"parameters": {
"url": "={{ $json.url }}",
"options": {
"timeout": 8000,
"response": {
"response": {
"fullResponse": true
}
}
}
},
"name": "Health Check",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
640,
300
],
"id": "kyc-3"
},
{
"parameters": {
"jsCode": "const staticData = $getWorkflowStaticData('global');\nconst firstFail = staticData.firstFail || {};\nconst now = Date.now();\nconst results = [];\n\nfor (const item of $input.all()) {\n const name = item.json.name;\n const status = item.json.statusCode || 0;\n const latency = item.json.headers ? parseInt(item.json.headers['x-response-time'] || '0') : 0;\n const sla = item.json.sla_ms || 2000;\n \n let health = 'OK';\n if (status === 0 || status >= 500) health = 'DOWN';\n else if (latency > sla * 1.5) health = 'DEGRADED';\n else if (latency > sla) health = 'SLOW';\n \n if (health !== 'OK') {\n if (!firstFail[name]) firstFail[name] = now;\n const minsSince = Math.floor((now - firstFail[name]) / 60000);\n results.push({ json: { ...item.json, health, latency_ms: latency, mins_since_first_fail: minsSince } });\n } else {\n delete firstFail[name];\n }\n}\n\nstaticData.firstFail = firstFail;\nreturn results;"
},
"name": "Classify & Dedup",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
840,
300
],
"id": "kyc-4"
},
{
"parameters": {
"select": "channel",
"channelId": {
"__rl": true,
"value": "#kyc-ops",
"mode": "name"
},
"text": "=🔴 *KYC/KYB API {{ $json.health }}*: {{ $json.name }}\nSegment: {{ $json.customer_segment }} | Latency: {{ $json.latency_ms }}ms (SLA: {{ $json.sla_ms }}ms)\nDown for: {{ $json.mins_since_first_fail }} min",
"otherOptions": {}
},
"name": "Slack #kyc-ops",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [
1040,
300
],
"id": "kyc-5"
}
],
"connections": {
"Every 5 Minutes": {
"main": [
[
{
"node": "Verification Endpoints",
"type": "main",
"index": 0
}
]
]
},
"Verification Endpoints": {
"main": [
[
{
"node": "Health Check",
"type": "main",
"index": 0
}
]
]
},
"Health Check": {
"main": [
[
{
"node": "Classify & Dedup",
"type": "main",
"index": 0
}
]
]
},
"Classify & Dedup": {
"main": [
[
{
"node": "Slack #kyc-ops",
"type": "main",
"index": 0
}
]
]
}
}
}
Pro tips:
- Set per-endpoint SLA thresholds based on your customer contract SLAs (sanctions screening typically 500ms, full KYB 3–5s)
- Add a Postgres log to track SLA compliance over time for your SOC2 evidence package
- Add a secondary alert to
#kyc-ops-managementwhen downtime exceeds 15 minutes (affects customer onboarding SLA)
Workflow 3: New FinTech/Bank Client Onboarding Drip
Automates the 7-day activation sequence for new RegTech customers — API credentials, integration check-in, and go-live checklist — without any customer data leaving your infrastructure.
What it does:
- Trigger on new row in Google Sheets (or CRM webhook) when a new client is signed
- Classify tier: TIER1_BANK (>500 employees), TIER2_FINTECH, STARTUP
- Day 0: Send API credentials + sandbox access link + Slack DM to CSM
- Day 3: Check-in email with first-screening walkthrough
- Day 7: Production go-live checklist (SAR workflow, threshold configuration, audit log retention)
- Mark
onboarding_complete = truein Sheets when sequence finishes
Workflow JSON:
{
"name": "New FinTech/Bank Client Onboarding Drip",
"nodes": [
{
"parameters": {
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
},
"triggerOn": "anyUpdate",
"sheetName": {
"__rl": true,
"value": "Clients",
"mode": "name"
},
"options": {}
},
"name": "New Client in Sheets",
"type": "n8n-nodes-base.googleSheetsTrigger",
"typeVersion": 4,
"position": [
240,
300
],
"id": "ob-1"
},
{
"parameters": {
"jsCode": "const row = $input.first().json;\nconst employees = parseInt(row.company_size) || 0;\nconst tier = employees > 500 ? 'TIER1_BANK' : employees > 50 ? 'TIER2_FINTECH' : 'STARTUP';\nreturn [{ json: { ...row, tier, sales_rep: row.csm_email || 'cs@yourregtech.com' } }];"
},
"name": "Classify Tier",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
440,
300
],
"id": "ob-2"
},
{
"parameters": {
"fromEmail": "onboarding@yourregtech.com",
"toEmail": "={{ $json.contact_email }}",
"subject": "Your RegTech API credentials + sandbox access",
"emailType": "html",
"message": "=<p>Hi {{ $json.contact_name }},</p><p>Welcome to [RegTech Platform]. Here are your API credentials for the sandbox environment:</p><ul><li><strong>API Key:</strong> <code>{{ $json.api_key_sandbox }}</code></li><li><strong>Sandbox Base URL:</strong> <code>https://sandbox.api.yourregtech.com/v1</code></li><li><strong>Docs:</strong> <a href='https://docs.yourregtech.com'>docs.yourregtech.com</a></li></ul><p>Your dedicated CSM {{ $json.csm_name }} will reach out within 24h to schedule your technical onboarding call.</p><p>— The [RegTech Platform] Team</p>"
},
"name": "Day 0: API Creds + Welcome",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
640,
300
],
"id": "ob-3"
},
{
"parameters": {
"amount": 3,
"unit": "days"
},
"name": "Wait 3 Days",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
840,
300
],
"id": "ob-4"
},
{
"parameters": {
"fromEmail": "onboarding@yourregtech.com",
"toEmail": "={{ $json.contact_email }}",
"subject": "Quick check-in: first screening results ready?",
"emailType": "html",
"message": "=<p>Hi {{ $json.contact_name }},</p><p>Just checking in — have you run your first batch screening through the sandbox? Common first steps:</p><ol><li>POST a test transaction to <code>/v1/screen/transaction</code></li><li>Check the response <code>confidence_score</code> and <code>flag_reason</code> fields</li><li>Configure your webhook endpoint for real-time alerts</li></ol><p>Any blockers? Reply and I'll loop in our integration team.</p>"
},
"name": "Day 3: Screening Check-in",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
1040,
300
],
"id": "ob-5"
},
{
"parameters": {
"amount": 4,
"unit": "days"
},
"name": "Wait 4 Days",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
1240,
300
],
"id": "ob-6"
},
{
"parameters": {
"fromEmail": "onboarding@yourregtech.com",
"toEmail": "={{ $json.contact_email }}",
"subject": "Ready for production? Your go-live checklist",
"emailType": "html",
"message": "=<p>Hi {{ $json.contact_name }},</p><p>Before flipping to production, here's your go-live checklist:</p><ul><li>✅ Webhook endpoint registered and receiving test events</li><li>✅ SAR workflow documented (your compliance team's internal process)</li><li>✅ Screening thresholds configured for your risk appetite</li><li>✅ Alert routing tested (Slack, email, or ticketing system)</li><li>✅ Audit log retention policy set (minimum 5 years for BSA)</li></ul><p>Need a pre-production compliance review call? Book one here: [calendly link]</p>"
},
"name": "Day 7: Go-Live Checklist",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
1440,
300
],
"id": "ob-7"
},
{
"parameters": {
"operation": "update",
"sheetName": {
"__rl": true,
"value": "Clients",
"mode": "name"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"onboarding_complete": true,
"onboarded_at": "={{ new Date().toISOString() }}"
}
},
"options": {}
},
"name": "Mark Onboarding Complete",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
1640,
300
],
"id": "ob-8"
}
],
"connections": {
"New Client in Sheets": {
"main": [
[
{
"node": "Classify Tier",
"type": "main",
"index": 0
}
]
]
},
"Classify Tier": {
"main": [
[
{
"node": "Day 0: API Creds + Welcome",
"type": "main",
"index": 0
}
]
]
},
"Day 0: API Creds + Welcome": {
"main": [
[
{
"node": "Wait 3 Days",
"type": "main",
"index": 0
}
]
]
},
"Wait 3 Days": {
"main": [
[
{
"node": "Day 3: Screening Check-in",
"type": "main",
"index": 0
}
]
]
},
"Day 3: Screening Check-in": {
"main": [
[
{
"node": "Wait 4 Days",
"type": "main",
"index": 0
}
]
]
},
"Wait 4 Days": {
"main": [
[
{
"node": "Day 7: Go-Live Checklist",
"type": "main",
"index": 0
}
]
]
},
"Day 7: Go-Live Checklist": {
"main": [
[
{
"node": "Mark Onboarding Complete",
"type": "main",
"index": 0
}
]
]
}
}
}
Pro tips:
- Add a
Waitnode check at Day 3 to query your API for first screening event — if none, send a more targeted troubleshooting email instead of the generic check-in - For TIER1_BANK clients, add a Slack notification to your enterprise CS channel at Day 0 to trigger a same-day call from an enterprise CSM
Workflow 4: AML/CFT Regulatory Compliance Deadline Tracker
Tracks deadlines across FATF, AMLD6, BSA, FinCEN, SOC2, FCA, and MAS frameworks — and escalates to the right owner before penalties apply.
What it does:
- Runs weekdays at 8AM
- Reads all upcoming deadlines from a Google Sheets compliance calendar
- Calculates urgency: OVERDUE / CRITICAL (≤7 days) / URGENT (≤21 days) / WARNING (≤60 days)
- Maps each regulation key to a specific required action (FATF mutual evaluation prep, BSA AML program review, FinCEN SAR filing, etc.)
- Routes CRITICAL/OVERDUE to
#compliance-critical @here; all levels email the owner with regulation-specific action items
Workflow JSON:
{
"name": "AML/CFT Regulatory Compliance Deadline Tracker",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 8 * * 1-5"
}
]
}
},
"name": "Weekdays 8AM",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
240,
300
],
"id": "reg-1"
},
{
"parameters": {
"operation": "read",
"sheetName": {
"__rl": true,
"value": "AML Compliance Deadlines",
"mode": "name"
},
"options": {}
},
"name": "Read Deadlines Sheet",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
440,
300
],
"id": "reg-2"
},
{
"parameters": {
"jsCode": "const now = new Date();\nconst actionMap = {\n 'FATF_MUTUAL_EVALUATION': 'Prepare self-assessment: policies, controls, STR stats, sanction screening coverage',\n 'AMLD6_ANNUAL_REVIEW': 'Complete annual AML/CFT policy review per AMLD6 Art.8(5), update risk assessment',\n 'BSA_AML_PROGRAM_REVIEW': 'Annual independent review of AML program (5 pillars: policy, controls, testing, training, BSA officer)',\n 'FINCEN_CTR_FILING': 'File Currency Transaction Reports for >$10K cash transactions within 15 days',\n 'FINCEN_SAR_FILING': 'File Suspicious Activity Report within 30 days of detection (60 days with extension)',\n 'SOC2_EVIDENCE': 'Gather SOC2 Type II evidence: access logs, encryption audit, vendor review, incident register',\n 'GDPR_DPA_RENEWAL': 'Renew Data Processing Agreements with all sub-processors processing EU customer PII',\n 'FCA_AML_ANNUAL': 'Submit FCA Annual Financial Crime Report and update MLR 2017 risk assessment',\n 'MAS_NOTICE_626': 'Annual MAS Notice 626 (Singapore AML) compliance review and training records update',\n 'OFAC_SDN_REFRESH': 'Verify your sanctions list version matches latest OFAC SDN + Consolidated Sanctions List'\n};\n\nreturn $input.all()\n .map(item => {\n const deadlineDate = new Date(item.json.deadline_date);\n const daysLeft = Math.ceil((deadlineDate - now) / (1000 * 60 * 60 * 24));\n let urgency = 'NOTICE';\n if (daysLeft < 0) urgency = 'OVERDUE';\n else if (daysLeft <= 7) urgency = 'CRITICAL';\n else if (daysLeft <= 21) urgency = 'URGENT';\n else if (daysLeft <= 60) urgency = 'WARNING';\n const action = actionMap[item.json.regulation_key] || 'Review compliance requirements and prepare documentation';\n return { json: { ...item.json, days_left: daysLeft, urgency, action_required: action } };\n })\n .filter(item => item.json.urgency !== 'NOTICE');"
},
"name": "Calculate Urgency",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
640,
300
],
"id": "reg-3"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true
},
"conditions": [
{
"leftValue": "={{ $json.urgency }}",
"rightValue": "CRITICAL",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"renameOutput": true,
"outputKey": "critical"
},
{
"conditions": {
"options": {
"caseSensitive": true
},
"conditions": [
{
"leftValue": "={{ $json.urgency }}",
"rightValue": "OVERDUE",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
},
"renameOutput": true,
"outputKey": "overdue"
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"name": "Route by Urgency",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
840,
300
],
"id": "reg-4"
},
{
"parameters": {
"select": "channel",
"channelId": {
"__rl": true,
"value": "#compliance-critical",
"mode": "name"
},
"text": "=🚨 *COMPLIANCE {{ $json.urgency }}* — {{ $json.regulation_key }}\n\nDeadline: {{ $json.deadline_date }} ({{ $json.days_left }} days)\nOwner: {{ $json.owner_email }}\nRequired Action: {{ $json.action_required }}",
"otherOptions": {}
},
"name": "Slack #compliance-critical @here",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.3,
"position": [
1040,
180
],
"id": "reg-5"
},
{
"parameters": {
"fromEmail": "compliance@yourregtech.com",
"toEmail": "={{ $json.owner_email }}",
"subject": "=ACTION REQUIRED — {{ $json.urgency }}: {{ $json.regulation_key }} deadline {{ $json.days_left <= 0 ? 'PAST DUE' : 'in ' + $json.days_left + ' days' }}",
"emailType": "html",
"message": "=<p>This is an automated compliance alert from your n8n deadline tracker.</p><p><strong>Regulation:</strong> {{ $json.regulation_key }}<br><strong>Deadline:</strong> {{ $json.deadline_date }}<br><strong>Status:</strong> {{ $json.urgency }} — {{ Math.abs($json.days_left) }} day(s) {{ $json.days_left < 0 ? 'overdue' : 'remaining' }}</p><p><strong>Required Action:</strong><br>{{ $json.action_required }}</p><p>Please update the <a href='[sheet link]'>AML Compliance Deadlines sheet</a> with your progress.</p>"
},
"name": "Email Owner",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
1040,
420
],
"id": "reg-6"
}
],
"connections": {
"Weekdays 8AM": {
"main": [
[
{
"node": "Read Deadlines Sheet",
"type": "main",
"index": 0
}
]
]
},
"Read Deadlines Sheet": {
"main": [
[
{
"node": "Calculate Urgency",
"type": "main",
"index": 0
}
]
]
},
"Calculate Urgency": {
"main": [
[
{
"node": "Route by Urgency",
"type": "main",
"index": 0
}
]
]
},
"Route by Urgency": {
"critical": [
[
{
"node": "Slack #compliance-critical @here",
"type": "main",
"index": 0
}
]
],
"overdue": [
[
{
"node": "Slack #compliance-critical @here",
"type": "main",
"index": 0
}
]
],
"extra": [
[
{
"node": "Email Owner",
"type": "main",
"index": 0
}
]
]
}
}
}
Pro tips:
- Add DORA (EU Digital Operational Resilience Act) Art.28 entries for your ICT third-party register — n8n self-hosted is a controllable internal dependency, Zapier is not
- Add a Postgres insert for each alert to maintain a compliance_alert_audit_log for your SOC2 CC7 controls
- Include OFAC SDN List version date to track when you last refreshed your sanctions list
Workflow 5: Weekly RegTech Compliance Ops KPI Dashboard
Delivers a Monday morning executive report covering screening volume, SAR filing rate, false positive rate, and customer growth — all calculated from your own Postgres, never leaving your VPC.
What it does:
- Monday 8AM: run two parallel Postgres queries (this week vs. last week)
- Merge results and calculate WoW% for each metric
- Compute false positive rate (flagged but no SAR filed / total flagged)
- Build HTML table with color-coded trends
- Email to CCO, CEO, CTO, VP Compliance
Workflow JSON:
{
"name": "Weekly RegTech Compliance Ops KPI Dashboard",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 8 * * 1"
}
]
}
},
"name": "Monday 8AM",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
240,
300
],
"id": "kpi-1"
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT COUNT(*) AS total_screenings, COUNT(*) FILTER (WHERE confidence_score >= 0.85) AS flagged_count, COUNT(*) FILTER (WHERE sar_filed = true) AS sars_filed, COUNT(DISTINCT customer_id) AS active_customers FROM screening_events WHERE created_at >= NOW() - INTERVAL '7 days'",
"options": {}
},
"name": "This Week Metrics",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
440,
180
],
"id": "kpi-2"
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT COUNT(*) AS total_screenings, COUNT(*) FILTER (WHERE confidence_score >= 0.85) AS flagged_count, COUNT(*) FILTER (WHERE sar_filed = true) AS sars_filed, COUNT(DISTINCT customer_id) AS active_customers FROM screening_events WHERE created_at >= NOW() - INTERVAL '14 days' AND created_at < NOW() - INTERVAL '7 days'",
"options": {}
},
"name": "Last Week Metrics",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
440,
420
],
"id": "kpi-3"
},
{
"parameters": {
"mode": "multiplex"
},
"name": "Merge",
"type": "n8n-nodes-base.merge",
"typeVersion": 3,
"position": [
640,
300
],
"id": "kpi-4"
},
{
"parameters": {
"jsCode": "const staticData = $getWorkflowStaticData('global');\nconst items = $input.all();\nconst thisWeek = items[0].json;\nconst lastWeek = items[1].json;\n\nconst wowPct = (curr, prev) => {\n if (!prev || prev == 0) return 'N/A';\n const pct = ((curr - prev) / prev * 100).toFixed(1);\n return (pct > 0 ? '+' : '') + pct + '%';\n};\n\nconst falsePosPct = thisWeek.total_screenings > 0 \n ? ((1 - thisWeek.sars_filed / thisWeek.flagged_count) * 100).toFixed(1) + '%'\n : 'N/A';\n\nconst html = [\n '<h2>RegTech Compliance Ops — Weekly KPI Report</h2>',\n '<p>Week ending: ' + new Date().toDateString() + '</p>',\n '<table border=\"1\" cellpadding=\"8\" style=\"border-collapse:collapse\">',\n '<tr><th>Metric</th><th>This Week</th><th>Last Week</th><th>WoW</th></tr>',\n '<tr><td>Total Screenings</td><td>' + (thisWeek.total_screenings||0).toLocaleString() + '</td><td>' + (lastWeek.total_screenings||0).toLocaleString() + '</td><td>' + wowPct(thisWeek.total_screenings, lastWeek.total_screenings) + '</td></tr>',\n '<tr><td>Flagged (≥0.85)</td><td>' + (thisWeek.flagged_count||0) + '</td><td>' + (lastWeek.flagged_count||0) + '</td><td>' + wowPct(thisWeek.flagged_count, lastWeek.flagged_count) + '</td></tr>',\n '<tr><td>SARs Filed</td><td>' + (thisWeek.sars_filed||0) + '</td><td>' + (lastWeek.sars_filed||0) + '</td><td>' + wowPct(thisWeek.sars_filed, lastWeek.sars_filed) + '</td></tr>',\n '<tr><td>False Positive Rate</td><td>' + falsePosPct + '</td><td>—</td><td>—</td></tr>',\n '<tr><td>Active Customers</td><td>' + (thisWeek.active_customers||0) + '</td><td>' + (lastWeek.active_customers||0) + '</td><td>' + wowPct(thisWeek.active_customers, lastWeek.active_customers) + '</td></tr>',\n '</table>'\n].join('');\n\nreturn [{ json: { html, subject: 'RegTech Weekly KPI — ' + new Date().toDateString() } }];"
},
"name": "Build KPI Report",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
840,
300
],
"id": "kpi-5"
},
{
"parameters": {
"fromEmail": "reporting@yourregtech.com",
"toEmail": "cco@yourregtech.com",
"bccEmail": "ceo@yourregtech.com,cto@yourregtech.com,vp-compliance@yourregtech.com",
"subject": "={{ $json.subject }}",
"emailType": "html",
"message": "={{ $json.html }}"
},
"name": "Email Leadership",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
1040,
300
],
"id": "kpi-6"
}
],
"connections": {
"Monday 8AM": {
"main": [
[
{
"node": "This Week Metrics",
"type": "main",
"index": 0
},
{
"node": "Last Week Metrics",
"type": "main",
"index": 0
}
]
]
},
"This Week Metrics": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"Last Week Metrics": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"Merge": {
"main": [
[
{
"node": "Build KPI Report",
"type": "main",
"index": 0
}
]
]
},
"Build KPI Report": {
"main": [
[
{
"node": "Email Leadership",
"type": "main",
"index": 0
}
]
]
}
}
}
Pro tips:
- Add MRR from your billing database as a 6th metric to correlate compliance ops volume with revenue growth
- Add a
$getWorkflowStaticDatastore for historical trend data to add a 4-week trailing average column
Why RegTech SaaS teams self-host n8n instead of Zapier or Make
| n8n (self-hosted) | Zapier | Make | |
|---|---|---|---|
| SAR data sovereignty | ✅ Stays in your VPC | ❌ Transits Zapier cloud | ❌ Transits Make cloud |
| GDPR Art.28 sub-processor | ✅ No new sub-processor | ❌ Zapier added to all DPAs | ❌ Make added to all DPAs |
| DORA ICT register (EU) | ✅ Internal controlled system | ❌ Third-party ICT provider | ❌ Third-party ICT provider |
| 100M screenings/month cost | ✅ ~$40/mo VPS | ❌ $50,000+/mo | ❌ $25,000+/mo |
| FinCEN SAR tipping-off risk | ✅ Eliminated | ⚠️ Data egress risk | ⚠️ Data egress risk |
| Webhook latency (in-VPC) | ✅ Sub-10ms | ❌ 2–15s queue | ❌ 2–15s queue |
| SOC2 CM-3 audit trail | ✅ Git-versioned JSON | ❌ Unversioned UI config | ❌ Unversioned UI config |
| OFAC/SDN refresh without data egress | ✅ Runs inside boundary | ❌ External processing | ❌ External processing |
The volume economics alone are decisive for any platform processing meaningful transaction volume: at 100M screenings per month, Zapier's per-task pricing makes n8n self-hosting not a preference but a financial requirement.
Get the complete workflow templates
All 5 workflows above are available as ready-to-import n8n templates at stripeai.gumroad.com — along with 10 other automation templates for sales, support, reporting, and content operations.
The Complete Bundle ($97) includes all 15 templates with setup guides.
Self-hosted n8n is free and open source. The templates are paid; the JSON examples in this article are free to use.
Top comments (0)