DEV Community

Alex Kane
Alex Kane

Posted on

n8n for RegTech SaaS: 5 Automations for MiFID II, AMLD6, Basel IV, and DORA Compliance (Free Workflow JSON)

n8n for RegTech SaaS: 5 Automations for MiFID II, AMLD6, Basel IV, and DORA Compliance (Free Workflow JSON)

Your RegTech platform helps banks and investment firms comply with MiFID II, AMLD6, Basel IV, and DORA. But who automates the compliance workflows that run your SaaS operations?

This is the RegTech SaaS vendor edition — five production-ready n8n automations built for companies selling compliance technology into regulated financial institutions. Customer onboarding by compliance regime, regulatory API health monitoring, incident pipelines with correct notification deadlines, and executive dashboards that surface the metrics that matter.

All workflow JSON is importable into any n8n instance. Self-hosted n8n is particularly relevant here — DORA Article 28 (effective January 2025) means your automation tooling is itself a third-party ICT service provider. Using Zapier or Make.com to run your operational workflows adds another vendor to your DORA third-party ICT risk register.

Full template bundle: stripeai.gumroad.com


The DORA Art. 28 Problem Every RegTech SaaS Has

DORA (Digital Operational Resilience Act) applies to financial entities — but it also captures ICT third-party service providers who serve those entities. If your RegTech SaaS runs operational workflows on Zapier or Make.com, those platforms are now third-party ICT providers in your DORA scope.

Self-hosted n8n is the architectural answer: every workflow runs inside your ICT infrastructure. Git-versioned workflow JSON is a DORA Art. 28 audit artifact your regulators can review. You're not adding a new third-party ICT provider to your operational resilience assessment — you're removing one.

Compliance Concern Zapier / Make.com Self-Hosted n8n
DORA Art. 28 — third-party ICT scope Zapier = new ICT provider in scope Your infra, not in scope
MiFID II Art. 26 transaction data Flows to cloud, possible ESMA gap Stays in regulatory reporting enclave
AMLD6 AML / SAR data sensitivity External cloud processes FIU data On-prem, your FIU obligation only
GDPR Art. 28 sub-processor chain Zapier = sub-processor, requires DPA No new DPA needed
DORA Art. 17 ICT incident audit trail 30-day external log limit Full git-versioned JSON audit

Workflow 1: RegTech Customer Onboarding & Compliance Regime Drip

The problem: New customers at your RegTech SaaS span every compliance regime. A TIER1_BANK onboarding for DORA + Basel IV + MiFID II needs different guidance on day one than an SME investment firm onboarding for AMLD6 only. Manual triage does not scale.

The automation: Webhook fires on new customer signup. Code node detects which regimes they selected (MiFID_II_ARM, EMIR_TRADE_REPO, AMLD6_GOAML, DORA_ICT, Basel_IV_FRTB, Solvency_II_ORSA) and classifies account tier (ENTERPRISE, MID_MARKET, SME). Fires regime-specific Day 0 email, Slack CSM alert, and a Postgres SOC 2 CC7.1 audit log. Waits 3 days, sends integration checklist. Waits 4 days, sends Day 7 value milestone and escalates ENTERPRISE accounts to a dedicated Slack channel.

Google Sheets / CRM fields: firm_id, firm_name, firm_type, contact_email, arr_eur, mifid_ii (bool), emir (bool), aml (bool), dora (bool), basel_iv (bool), solvency_ii (bool)

{
  "name": "RegTech Customer Onboarding & Compliance Regime Drip",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "regtech-signup",
        "responseMode": "lastNode",
        "options": {}
      },
      "name": "New Customer Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const c = $json.body || $json;\nconst regimes = [];\nif (c.mifid_ii) regimes.push('MiFID_II_ARM');\nif (c.emir) regimes.push('EMIR_TRADE_REPO');\nif (c.aml) regimes.push('AMLD6_GOAML');\nif (c.dora) regimes.push('DORA_ICT');\nif (c.basel_iv) regimes.push('Basel_IV_FRTB');\nif (c.solvency_ii) regimes.push('Solvency_II_ORSA');\nconst firmType = c.firm_type || 'INVESTMENT_FIRM';\nconst arr = parseInt(c.arr_eur) || 0;\nconst tier = (arr >= 500000 || firmType === 'TIER1_BANK') ? 'ENTERPRISE' : arr >= 100000 ? 'MID_MARKET' : 'SME';\nreturn [{ json: { ...c, regimes: regimes.join(','), firmType, tier, onboarded_at: new Date().toISOString() } }];"
      },
      "name": "Classify Regime & Tier",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "fromEmail": "onboarding@yourregtechco.com",
        "toEmail": "={{ $json.contact_email }}",
        "subject": "=Welcome \u2014 your {{ $json.regimes.split(',').length }} compliance regimes, configured",
        "emailType": "html",
        "message": "=<p>Hi {{ $json.contact_name }},</p><p>Your RegTech platform account for <strong>{{ $json.firm_name }}</strong> is live.</p><p><strong>Compliance regimes:</strong> {{ $json.regimes.replace(/,/g, ', ') }}</p><p><strong>Firm type:</strong> {{ $json.firmType }}</p><p>Your compliance dashboard is pre-configured for your regimes. <a href='https://docs.yourregtech.com/setup'>Setup guide</a></p>"
      },
      "name": "Day 0 Welcome Email",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        680,
        220
      ]
    },
    {
      "parameters": {
        "channel": "#customer-success",
        "text": "=:white_check_mark: *New {{ $json.tier }}:* {{ $json.firm_name }} ({{ $json.firmType }})\nRegimes: {{ $json.regimes }}\nContact: {{ $json.contact_name }} \u2014 {{ $json.contact_email }}\nAssign CSM within {{ $json.tier === 'ENTERPRISE' ? '2 hours' : '24 hours' }}."
      },
      "name": "Slack CSM Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        680,
        380
      ]
    },
    {
      "parameters": {
        "operation": "insert",
        "schema": "public",
        "table": "customer_onboarding_log",
        "columns": "firm_id,firm_name,firm_type,regimes,tier,contact_email,onboarded_at",
        "values": "={{ $json.firm_id }},={{ $json.firm_name }},={{ $json.firmType }},={{ $json.regimes }},={{ $json.tier }},={{ $json.contact_email }},={{ $json.onboarded_at }}"
      },
      "name": "Postgres SOC2 Audit Log",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        680,
        500
      ]
    },
    {
      "parameters": {
        "amount": 3,
        "unit": "days"
      },
      "name": "Wait 3 Days",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        900,
        220
      ]
    },
    {
      "parameters": {
        "fromEmail": "onboarding@yourregtechco.com",
        "toEmail": "={{ $json.contact_email }}",
        "subject": "=Day 3: Integration checklist for {{ $json.regimes.split(',')[0] }}",
        "emailType": "html",
        "message": "=<p>Hi {{ $json.contact_name }},</p><p>Integration checklist for <strong>{{ $json.regimes.replace(/,/g, ', ') }}</strong>:</p><ul><li>Connect your transaction reporting endpoint</li><li>Configure regime-specific alert thresholds</li><li>Invite your compliance officer to the dashboard</li><li>Run your first compliance health check</li></ul>"
      },
      "name": "Day 3 Checklist",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1120,
        220
      ]
    },
    {
      "parameters": {
        "amount": 4,
        "unit": "days"
      },
      "name": "Wait 4 Days",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1340,
        220
      ]
    },
    {
      "parameters": {
        "fromEmail": "onboarding@yourregtechco.com",
        "toEmail": "={{ $json.contact_email }}",
        "subject": "Day 7: Your compliance automation is live \u2014 quick check",
        "emailType": "html",
        "message": "=<p>Hi {{ $json.contact_name }},</p><p>It has been a week since {{ $json.firm_name }} joined. Is your first compliance deadline tracked? Any integration questions? Reply to this email or <a href='https://calendly.com/yourregtechco/review'>book a 20-minute review call</a>.</p>"
      },
      "name": "Day 7 Value Email",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1560,
        220
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true
          },
          "conditions": [
            {
              "leftValue": "={{ $json.tier }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "rightValue": "ENTERPRISE"
            }
          ]
        },
        "combineOperation": "any"
      },
      "name": "Is Enterprise?",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2,
      "position": [
        1560,
        380
      ]
    },
    {
      "parameters": {
        "channel": "#enterprise-csm",
        "text": "=:star: *ENTERPRISE Day-7 Follow-up Due:* {{ $json.firm_name }}\nContact: {{ $json.contact_name }} \u2014 {{ $json.contact_email }}\nRegimes: {{ $json.regimes }}\nAction: Schedule QBR call this week."
      },
      "name": "Escalate Enterprise CSM",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1780,
        380
      ]
    }
  ],
  "connections": {
    "New Customer Webhook": {
      "main": [
        [
          {
            "node": "Classify Regime & Tier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Regime & Tier": {
      "main": [
        [
          {
            "node": "Day 0 Welcome Email",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack CSM Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Postgres SOC2 Audit Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Day 0 Welcome Email": {
      "main": [
        [
          {
            "node": "Wait 3 Days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 3 Days": {
      "main": [
        [
          {
            "node": "Day 3 Checklist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Day 3 Checklist": {
      "main": [
        [
          {
            "node": "Wait 4 Days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 4 Days": {
      "main": [
        [
          {
            "node": "Day 7 Value Email",
            "type": "main",
            "index": 0
          },
          {
            "node": "Is Enterprise?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is Enterprise?": {
      "main": [
        [
          {
            "node": "Escalate Enterprise CSM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

ENTERPRISE escalation at Day 7: The Is Enterprise? filter fires a Slack alert to #enterprise-csm on day seven. Most churn in RegTech SaaS happens in weeks 2-4 when integration complexity blocks first value. This workflow makes the CSM intervention automatic.


Workflow 2: RegTech Platform Regulatory API Health Monitor

The problem: Your regulatory APIs — transaction reporting, KYC verification, AML screening, audit trail — are the core of your product. Fifteen minutes of TRANSACTION_REPORTING_API downtime for a MiFID II customer can mean missed T+1 trade reporting. Under DORA Art. 17, an ICT incident that materially affects your customers' regulatory compliance capabilities needs to be classified as major or significant — and major incidents require notification to the competent authority within 4 hours.

The automation: Runs every 15 minutes. Polls five regulatory API endpoints. Uses $getWorkflowStaticData to detect state transitions (ONLINE → DEGRADED/OFFLINE) without re-alerting on persistent outages. When a state change occurs, sends Slack alert to #platform-ops and emails the CTO with a DORA Art. 17 classification reminder.

{
  "name": "RegTech Platform Regulatory API Health Monitor",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "*/15 * * * *"
            }
          ]
        }
      },
      "name": "Every 15 Min",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const endpoints = [\n  { name: 'TRANSACTION_REPORTING_API', url: process.env.TX_HEALTH_URL || 'https://api.yourregtech.com/health/transaction-reporting', regimes: ['MiFID_II_ARM','EMIR_TRADE_REPO'] },\n  { name: 'KYC_VERIFICATION_API', url: process.env.KYC_HEALTH_URL || 'https://api.yourregtech.com/health/kyc', regimes: ['AMLD6_GOAML'] },\n  { name: 'AML_SCREENING_API', url: process.env.AML_HEALTH_URL || 'https://api.yourregtech.com/health/aml-screening', regimes: ['AMLD6_GOAML','Basel_IV_FRTB'] },\n  { name: 'REGULATORY_REPORTING_API', url: process.env.REG_HEALTH_URL || 'https://api.yourregtech.com/health/regulatory-reporting', regimes: ['MiFID_II_ARM','DORA_ICT'] },\n  { name: 'AUDIT_TRAIL_API', url: process.env.AUDIT_HEALTH_URL || 'https://api.yourregtech.com/health/audit-trail', regimes: ['DORA_ICT','Basel_IV_FRTB'] }\n];\nreturn endpoints.map(e => ({ json: e }));"
      },
      "name": "Build Endpoint List",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $json.url }}",
        "options": {
          "timeout": 5000,
          "response": {
            "response": {
              "fullResponse": true
            }
          }
        }
      },
      "name": "Health Check",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const staticData = $getWorkflowStaticData('global');\nif (!staticData.states) staticData.states = {};\nconst prev = staticData.states[$json.name] || 'ONLINE';\nconst code = $json.statusCode || 0;\nconst current = code === 200 ? 'ONLINE' : code >= 500 ? 'OFFLINE' : code >= 400 ? 'DEGRADED' : 'OFFLINE';\nconst changed = prev !== current;\nstaticData.states[$json.name] = current;\nif (!changed && current === 'ONLINE') return [];\nreturn [{ json: { ...$json, status: current, prev_status: prev, changed, alert_at: new Date().toISOString() } }];"
      },
      "name": "Detect State Change",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        300
      ]
    },
    {
      "parameters": {
        "channel": "#platform-ops",
        "text": "=:rotating_light: *Regulatory API {{ $json.status }}* \u2014 {{ $json.name }}\nPrevious: {{ $json.prev_status }} | Now: {{ $json.status }}\nCritical for: {{ $json.regimes.join(', ') }}\nTime: {{ $json.alert_at }}\nAction: Check logs. If DORA_ICT in regimes, assess DORA Art. 17 incident classification."
      },
      "name": "Slack Platform Ops",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1120,
        220
      ]
    },
    {
      "parameters": {
        "fromEmail": "ops@yourregtechco.com",
        "toEmail": "cto@yourregtechco.com",
        "subject": "=[REGTECH] {{ $json.name }} is {{ $json.status }} \u2014 affects {{ $json.regimes.join(', ') }}",
        "emailType": "html",
        "message": "=<p><strong>Regulatory API status change</strong></p><p>API: {{ $json.name }}<br>Status: {{ $json.status }} (was {{ $json.prev_status }})<br>Critical for: {{ $json.regimes.join(', ') }}<br>Time: {{ $json.alert_at }}</p><p>If this API handles DORA-regulated ICT functions, assess whether this qualifies as a DORA ICT incident under Art. 17 classification criteria. Major incidents require initial notification within 4 hours (Art. 19).</p>"
      },
      "name": "Email CTO",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1120,
        380
      ]
    }
  ],
  "connections": {
    "Every 15 Min": {
      "main": [
        [
          {
            "node": "Build Endpoint List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Endpoint List": {
      "main": [
        [
          {
            "node": "Health Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Health Check": {
      "main": [
        [
          {
            "node": "Detect State Change",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect State Change": {
      "main": [
        [
          {
            "node": "Slack Platform Ops",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email CTO",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

DORA Art. 17 note: The email body includes a classification reminder. If an API outage materially affects a customer's regulatory reporting capability, the ICT incident may qualify as major or significant under DORA — triggering your 4-hour initial notification obligation under Art. 19. Having the reminder in the alert reduces the risk of misclassifying an incident as minor.


Workflow 3: MiFID II / AMLD6 / DORA Compliance Deadline Tracker

The problem: Your customers have compliance deadlines across every regime you support. Missing a deadline — or failing to notify the customer in time — creates liability and churn.

The automation: Weekdays at 7AM, reads a Google Sheets deadline register, classifies each entry by days remaining (OVERDUE / CRITICAL ≤14d / URGENT ≤30d / WARNING ≤60d / NOTICE ≤90d), deduplicates via alert_sent_date, and routes to Slack + email for OVERDUE/CRITICAL items and email for lower severity. The actionMap covers 12 deadline types with specific required actions:

  • MiFID_II_RTS28_ANNUAL — Best execution annual report (RTS 28 Art. 3)
  • MiFID_II_PRIIP_KID_REVIEW — Key Information Document review
  • EMIR_TRADE_REPORTING_REFIT — T+1 trade reporting verification
  • AMLD6_KYC_PERIODIC_REVIEW — Enhanced due diligence cycle
  • AMLD6_GOAML_SAR_DEADLINE — SAR filing via goAML (30-day window)
  • DORA_ICT_RISK_ASSESSMENT_ANNUAL — Annual ICT risk assessment (Art. 6)
  • DORA_INCIDENT_CLASSIFICATION_REVIEW — Open incident classification (Art. 17)
  • DORA_THIRD_PARTY_REGISTER_UPDATE — ICT third-party register (Art. 28, quarterly)
  • Basel_IV_FRTB_SA_REPORTING — FRTB Sensitivity-Based Approach quarterly report
  • Basel_IV_CET1_MONTHLY_CHECK — Capital adequacy vs 4.5% minimum + buffer
  • SOLVENCY_II_ORSA_ANNUAL — Own Risk and Solvency Assessment
  • GDPR_DPIA_TRIENNIAL_REVIEW — Data Privacy Impact Assessment review

Google Sheets columns: deadline_id, deadline_type, regulation_area, deadline_date, firm_name, regulation_owner, regulation_owner_email, slack_channel, alert_sent_date

{
  "name": "MiFID II / AMLD6 / DORA Compliance Deadline Tracker",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 7 * * 1-5"
            }
          ]
        }
      },
      "name": "Weekdays 7AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "documentId": "YOUR_SHEET_ID",
        "sheetName": "reg_deadlines",
        "operation": "getAll",
        "options": {}
      },
      "name": "Read Reg Deadlines",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const today = new Date();\nconst actionMap = {\n  'MiFID_II_RTS28_ANNUAL': 'Publish best execution annual report on website (RTS 28 Art. 3)',\n  'MiFID_II_PRIIP_KID_REVIEW': 'Review and update Key Information Document \u2014 PRIIPs Regulation Art. 10',\n  'EMIR_TRADE_REPORTING_REFIT': 'Verify T+1 trade reporting to ARM under EMIR Refit',\n  'AMLD6_KYC_PERIODIC_REVIEW': 'Complete enhanced due diligence periodic review for high-risk customers',\n  'AMLD6_GOAML_SAR_DEADLINE': 'Submit Suspicious Activity Report via goAML portal (30-day window)',\n  'DORA_ICT_RISK_ASSESSMENT_ANNUAL': 'Complete annual ICT risk assessment per DORA Art. 6 \u2014 document ICT asset inventory',\n  'DORA_INCIDENT_CLASSIFICATION_REVIEW': 'Review open ICT incidents for DORA Art. 17 major/significant classification',\n  'DORA_THIRD_PARTY_REGISTER_UPDATE': 'Update ICT third-party risk register per DORA Art. 28 \u2014 quarterly review',\n  'Basel_IV_FRTB_SA_REPORTING': 'Submit FRTB Sensitivity-Based Approach capital report to national CA (quarterly)',\n  'Basel_IV_CET1_MONTHLY_CHECK': 'Monthly capital adequacy check \u2014 CET1 vs 4.5% minimum + combined buffer requirement',\n  'SOLVENCY_II_ORSA_ANNUAL': 'Complete Own Risk and Solvency Assessment (ORSA) per Solvency II Art. 45',\n  'GDPR_DPIA_TRIENNIAL_REVIEW': 'Review Data Privacy Impact Assessments per GDPR Art. 35 \u2014 triennial full review'\n};\nconst items = [];\nfor (const row of $input.all()) {\n  const d = row.json;\n  const deadline = new Date(d.deadline_date);\n  const daysLeft = Math.ceil((deadline - today) / 86400000);\n  const sentToday = d.alert_sent_date === today.toISOString().split('T')[0];\n  if (sentToday) continue;\n  let severity = null;\n  if (daysLeft < 0) severity = 'OVERDUE';\n  else if (daysLeft <= 14) severity = 'CRITICAL';\n  else if (daysLeft <= 30) severity = 'URGENT';\n  else if (daysLeft <= 60) severity = 'WARNING';\n  else if (daysLeft <= 90) severity = 'NOTICE';\n  if (severity) items.push({ json: { ...d, daysLeft, severity, required_action: actionMap[d.deadline_type] || 'Review and complete required action' } });\n}\nreturn items;"
      },
      "name": "Classify Deadlines",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true
          },
          "conditions": [
            {
              "leftValue": "={{ $json.severity }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "rightValue": "OVERDUE"
            },
            {
              "leftValue": "={{ $json.severity }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "rightValue": "CRITICAL"
            }
          ]
        },
        "combineOperation": "any"
      },
      "name": "OVERDUE or CRITICAL",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2,
      "position": [
        900,
        220
      ]
    },
    {
      "parameters": {
        "channel": "={{ $json.slack_channel || '#compliance-ops' }}",
        "text": "=:red_circle: *{{ $json.severity }}: {{ $json.deadline_type }}*\n{{ $json.regulation_area }} \u2014 {{ $json.firm_name || 'All accounts' }}\nDeadline: {{ $json.deadline_date }} ({{ $json.daysLeft >= 0 ? $json.daysLeft + ' days' : 'OVERDUE by ' + Math.abs($json.daysLeft) + ' days' }})\nRequired: {{ $json.required_action }}\nOwner: {{ $json.regulation_owner }}"
      },
      "name": "Slack Urgent Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1120,
        220
      ]
    },
    {
      "parameters": {
        "fromEmail": "compliance@yourregtechco.com",
        "toEmail": "={{ $json.regulation_owner_email }}",
        "subject": "=[{{ $json.severity }}] {{ $json.deadline_type }} \u2014 {{ $json.deadline_date }}",
        "emailType": "html",
        "message": "=<p>Hi {{ $json.regulation_owner_name }},</p><p>Compliance deadline: <strong>{{ $json.deadline_type }}</strong> is <strong>{{ $json.severity }}</strong>.</p><p>Regulation: {{ $json.regulation_area }}<br>Deadline: {{ $json.deadline_date }} ({{ $json.daysLeft >= 0 ? $json.daysLeft + ' days remaining' : 'OVERDUE by ' + Math.abs($json.daysLeft) + ' days' }})<br><br><strong>Required action:</strong> {{ $json.required_action }}</p>"
      },
      "name": "Email Regulation Owner",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1120,
        380
      ]
    },
    {
      "parameters": {
        "documentId": "YOUR_SHEET_ID",
        "sheetName": "reg_deadlines",
        "operation": "update",
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "deadline_id",
              "lookupValue": "={{ $json.deadline_id }}"
            }
          ]
        },
        "fieldsUi": {
          "values": [
            {
              "fieldId": "alert_sent_date",
              "fieldValue": "={{ new Date().toISOString().split('T')[0] }}"
            }
          ]
        }
      },
      "name": "Mark Alert Sent",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1340,
        300
      ]
    }
  ],
  "connections": {
    "Weekdays 7AM": {
      "main": [
        [
          {
            "node": "Read Reg Deadlines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Reg Deadlines": {
      "main": [
        [
          {
            "node": "Classify Deadlines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Deadlines": {
      "main": [
        [
          {
            "node": "OVERDUE or CRITICAL",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email Regulation Owner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OVERDUE or CRITICAL": {
      "main": [
        [
          {
            "node": "Slack Urgent Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Regulation Owner": {
      "main": [
        [
          {
            "node": "Mark Alert Sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

DORA THIRD_PARTY_REGISTER_UPDATE is quarterly: Art. 28 requires a documented register of all ICT third-party service providers. For RegTech SaaS vendors, this means tracking not just your cloud infrastructure but every SaaS tool in your operational stack — including any cloud automation tool you use to run these workflows.


Workflow 4: DORA ICT Incident & AML Alert Pipeline

The problem: DORA Art. 19 requires notification to the competent authority within 4 hours for major ICT incidents. AMLD6 requires SAR filing within 30 days. These deadlines are non-negotiable and non-extendable. A webhook that routes ICT incidents to Slack is insufficient — you need the notification deadline calculated and surfaced the moment the incident is detected.

The automation: Webhook receives ICT or compliance incidents from your monitoring systems. Code node classifies the incident type and calculates the regulatory notification deadline for each:

  • DORA_MAJOR_ICT_INCIDENT — 4-hour initial notification to ECB/NCA (Art. 19)
  • DORA_SIGNIFICANT_INCIDENT — 24-hour intermediate report to NCA
  • AMLD6_SAR_THRESHOLD_BREACH — 30-day goAML filing (720h)
  • MiFID_II_TRANSACTION_FAIL_RATE — 24h ARM escalation for >5% T+1 failures
  • Basel_IV_CAPITAL_BREACH — 24h regulatory notification (CET1 < 4.5%)
  • EMIR_REPORTING_OUTAGE — 24h NCA notification
  • GDPR_PERSONAL_DATA_BREACH — 72h supervisory authority notification (Art. 33)

Uses $getWorkflowStaticData to dedup: the same incident type + system will not re-alert within 30 minutes. Logs every incident to a Postgres table for your DORA Art. 17 ICT incident register.

{
  "name": "DORA ICT Incident & AML Alert Pipeline",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "regtech-incident",
        "responseMode": "responseNode",
        "options": {}
      },
      "name": "Incident Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const inc = $json.body || $json;\nconst staticData = $getWorkflowStaticData('global');\nif (!staticData.sent) staticData.sent = {};\nconst key = `${inc.incident_type}_${inc.system_id}_${new Date().toISOString().split('T')[0]}`;\nconst now = Date.now();\nif (staticData.sent[key] && (now - staticData.sent[key]) < 1800000) return [];\nstaticData.sent[key] = now;\nconst meta = {\n  'DORA_MAJOR_ICT_INCIDENT': { sev: 'CRITICAL', hours: 4, authority: 'ECB/NCA', note: 'DORA Art. 19 \u2014 initial notification to competent authority within 4h' },\n  'DORA_SIGNIFICANT_INCIDENT': { sev: 'HIGH', hours: 24, authority: 'NCA', note: 'DORA Art. 19 \u2014 intermediate report within 24h' },\n  'AMLD6_SAR_THRESHOLD_BREACH': { sev: 'HIGH', hours: 720, authority: 'FIU/goAML', note: 'AMLD6 \u2014 SAR must be filed within 30 days of suspicion arising' },\n  'MiFID_II_TRANSACTION_FAIL_RATE': { sev: 'HIGH', hours: 24, authority: 'ARM', note: 'MiFID II Art. 26 \u2014 >5% T+1 fail rate requires ARM escalation' },\n  'Basel_IV_CAPITAL_BREACH': { sev: 'CRITICAL', hours: 24, authority: 'National CA/ECB', note: 'CET1 < 4.5% combined buffer \u2014 immediate regulatory notification required' },\n  'EMIR_REPORTING_OUTAGE': { sev: 'HIGH', hours: 24, authority: 'ESMA/NCA', note: 'EMIR Refit \u2014 trade reporting outage must be notified to NCA within 24h' },\n  'GDPR_PERSONAL_DATA_BREACH': { sev: 'HIGH', hours: 72, authority: 'DPA/Supervisory Authority', note: 'GDPR Art. 33 \u2014 notify supervisory authority within 72h if likely risk' }\n}[inc.incident_type] || { sev: 'MEDIUM', hours: 72, authority: 'Compliance Team', note: 'Review and classify manually' };\nconst deadline = new Date(now + meta.hours * 3600000).toISOString();\nreturn [{ json: { ...inc, ...meta, notification_deadline: deadline, incident_id: 'REGTECH-' + now } }];"
      },
      "name": "Classify & Dedup",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "channel": "#ict-compliance",
        "text": "=:rotating_light: *REGTECH {{ $json.sev }}: {{ $json.incident_type }}*\nSystem: {{ $json.system_id }} | ID: {{ $json.incident_id }}\nAuthority: {{ $json.authority }} | Notification Deadline: {{ $json.notification_deadline }}\nNote: {{ $json.note }}"
      },
      "name": "Slack Incident Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        680,
        220
      ]
    },
    {
      "parameters": {
        "fromEmail": "ciso@yourregtechco.com",
        "toEmail": "compliance-officer@yourregtechco.com",
        "cc": "legal@yourregtechco.com",
        "subject": "=[{{ $json.sev }}] {{ $json.incident_type }} \u2014 Deadline: {{ $json.notification_deadline }}",
        "emailType": "html",
        "message": "=<p><strong>Regulatory Incident Alert</strong></p><p>ID: {{ $json.incident_id }}<br>Type: {{ $json.incident_type }}<br>Severity: {{ $json.sev }}<br>System: {{ $json.system_id }}<br>Authority: {{ $json.authority }}</p><p><strong>Note:</strong> {{ $json.note }}</p><p><strong>Notification deadline:</strong> {{ $json.notification_deadline }}</p><p>Actions: Assess scope, prepare notification for {{ $json.authority }}, log in ICT incident register (DORA Art. 17).</p>"
      },
      "name": "Email Compliance + Legal",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        680,
        380
      ]
    },
    {
      "parameters": {
        "operation": "insert",
        "schema": "public",
        "table": "ict_incident_log",
        "columns": "incident_id,incident_type,severity,system_id,authority,notification_deadline,detected_at",
        "values": "={{ $json.incident_id }},={{ $json.incident_type }},={{ $json.sev }},={{ $json.system_id }},={{ $json.authority }},={{ $json.notification_deadline }},={{ new Date().toISOString() }}"
      },
      "name": "Postgres Incident Log",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        680,
        500
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ status: 'received', incident_id: $json.incident_id, notification_deadline: $json.notification_deadline }) }}"
      },
      "name": "Acknowledge",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        900,
        300
      ]
    }
  ],
  "connections": {
    "Incident Webhook": {
      "main": [
        [
          {
            "node": "Classify & Dedup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify & Dedup": {
      "main": [
        [
          {
            "node": "Slack Incident Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email Compliance + Legal",
            "type": "main",
            "index": 0
          },
          {
            "node": "Postgres Incident Log",
            "type": "main",
            "index": 0
          },
          {
            "node": "Acknowledge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The 4-hour window: DORA Art. 19 is strict about the major incident initial notification timeline. The notification deadline is computed from the moment the incident webhook fires, not from when someone manually classifies it. Having this surfaced immediately in the alert email prevents the classic 'we thought it was minor' delay that causes DORA deadline breaches.


Workflow 5: Weekly RegTech Executive KPI Dashboard

The problem: Your CEO sees customer count and ARR in the CRM. Your CISO sees incidents in the security tooling. Nobody has a single view that connects platform health, customer risk, and compliance incident load — the three dimensions that actually determine whether your RegTech SaaS has a good or bad week.

The automation: Every Monday at 8AM, runs two parallel Postgres queries (customer health and incident metrics), merges the results, builds a color-coded HTML dashboard, and emails CEO with CFO CC and CISO on BCC. Slack gets a one-line summary.

{
  "name": "Weekly RegTech Executive KPI Dashboard",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "name": "Monday 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(*) as active_customers, SUM(arr_eur) as total_arr, COUNT(CASE WHEN health_status = 'AT_RISK' THEN 1 END) as at_risk FROM customers WHERE status = 'ACTIVE'"
      },
      "name": "Query Customer Health",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        460,
        220
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(*) as open_incidents, COUNT(CASE WHEN incident_type LIKE 'DORA%' THEN 1 END) as dora_incidents, COUNT(CASE WHEN incident_type LIKE 'AMLD6%' THEN 1 END) as aml_incidents, COUNT(CASE WHEN incident_type LIKE 'MiFID%' THEN 1 END) as mifid_incidents, COUNT(CASE WHEN incident_type LIKE 'Basel%' THEN 1 END) as basel_incidents FROM ict_incident_log WHERE detected_at > NOW() - INTERVAL '7 days'"
      },
      "name": "Query Incident Metrics",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        460,
        380
      ]
    },
    {
      "parameters": {
        "mode": "combine",
        "combinationMode": "multiplex",
        "options": {}
      },
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const c = $input.all()[0]?.json || {};\nconst i = $input.all()[1]?.json || {};\nconst week = new Date().toISOString().split('T')[0];\nconst rows = [\n  ['Active Customers', c.active_customers || 0, ''],\n  ['Total ARR (EUR)', `EUR ${Number(c.total_arr || 0).toLocaleString()}`, ''],\n  ['Customers At Risk', c.at_risk || 0, c.at_risk > 0 ? 'REVIEW' : 'OK'],\n  ['Open Incidents (7d)', i.open_incidents || 0, i.open_incidents > 5 ? 'ELEVATED' : 'NORMAL'],\n  ['DORA ICT Incidents', i.dora_incidents || 0, i.dora_incidents > 0 ? 'MONITOR' : 'OK'],\n  ['AMLD6 AML Incidents', i.aml_incidents || 0, i.aml_incidents > 0 ? 'REVIEW' : 'OK'],\n  ['MiFID II Incidents', i.mifid_incidents || 0, i.mifid_incidents > 0 ? 'REVIEW' : 'OK'],\n  ['Basel IV Incidents', i.basel_incidents || 0, i.basel_incidents > 0 ? 'REVIEW' : 'OK']\n];\nconst tableRows = rows.map(([k, v, s]) => `<tr><td style='padding:8px;border:1px solid #ddd'>${k}</td><td style='padding:8px;border:1px solid #ddd;font-weight:bold'>${v}</td><td style='padding:8px;border:1px solid #ddd;color:${s==='OK'||s===''?'green':'red'}'>${s||'-'}</td></tr>`).join('');\nconst html = `<h2>RegTech Platform Weekly KPI \u2014 ${week}</h2><table style='border-collapse:collapse;width:100%'><tr style='background:#f5f5f5'><th style='padding:8px;border:1px solid #ddd;text-align:left'>Metric</th><th style='padding:8px;border:1px solid #ddd;text-align:left'>Value</th><th style='padding:8px;border:1px solid #ddd;text-align:left'>Status</th></tr>${tableRows}</table><p style='font-size:12px;color:#666'>DORA Art. 17: all open incidents require classification review. Incidents not classified as major/significant within 24h create DORA Art. 19 risk.</p>`;\nreturn [{ json: { html, week, ...c, ...i } }];"
      },
      "name": "Build KPI HTML",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        300
      ]
    },
    {
      "parameters": {
        "fromEmail": "kpi@yourregtechco.com",
        "toEmail": "ceo@yourregtechco.com",
        "cc": "cfo@yourregtechco.com",
        "bcc": "ciso@yourregtechco.com",
        "subject": "=RegTech Weekly KPI \u2014 {{ $json.week }}",
        "emailType": "html",
        "message": "={{ $json.html }}"
      },
      "name": "Email CEO + CFO (CISO BCC)",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1120,
        220
      ]
    },
    {
      "parameters": {
        "channel": "#exec-regtech-kpis",
        "text": "=:bar_chart: *RegTech Weekly KPI \u2014 {{ $json.week }}*\nActive Customers: {{ $json.active_customers }} | At Risk: {{ $json.at_risk }}\nOpen Incidents (7d): {{ $json.open_incidents }} \u2014 DORA: {{ $json.dora_incidents }} / AMLD6: {{ $json.aml_incidents }} / MiFID: {{ $json.mifid_incidents }}\nFull dashboard in CEO email."
      },
      "name": "Slack KPI Summary",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1120,
        380
      ]
    }
  ],
  "connections": {
    "Monday 8AM": {
      "main": [
        [
          {
            "node": "Query Customer Health",
            "type": "main",
            "index": 0
          },
          {
            "node": "Query Incident Metrics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Customer Health": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Incident Metrics": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Build KPI HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build KPI HTML": {
      "main": [
        [
          {
            "node": "Email CEO + CFO (CISO BCC)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack KPI Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

CISO on BCC (not TO): RegTech SaaS CISOs are often the last to know about compliance incidents that don't hit the security threshold but do hit the regulatory threshold. BCC on the weekly KPI email closes that gap without requiring a separate reporting process.


Putting It Together

Workflow Framework Trigger Output
Customer Onboarding Drip MiFID II / AMLD6 / DORA / Basel IV Webhook (signup) Email sequence + Slack
Regulatory API Monitor DORA Art. 17/19 Every 15 min Slack + Email (DORA hint)
Compliance Deadline Tracker MiFID II / AMLD6 / DORA / Basel IV Weekdays 7AM Slack + Email
DORA ICT Incident Pipeline DORA / AMLD6 / MiFID II / GDPR Webhook Slack + Email + Postgres
Weekly KPI Dashboard All regimes Monday 8AM HTML Email + Slack

All 5 workflows are available individually or as a bundle at stripeai.gumroad.com — import-ready JSON, production-tested logic, placeholder credentials ready to replace.


All workflows include placeholder credentials. Replace YOUR_SHEET_ID with your Google Sheets document ID, update Slack channel names and email addresses, and configure your Postgres connection before deploying.

This post is for informational purposes. Consult qualified legal counsel and your compliance team for your specific DORA, MiFID II, AMLD6, and Basel IV obligations.

Top comments (0)