DEV Community

Alex Kane
Alex Kane

Posted on

n8n for HealthTech SaaS: 5 Automations That Scale Clinical Ops and Keep Patient Data Compliant (Free Workflow JSON)

HealthTech SaaS companies sit at the intersection of clinical urgency and regulatory complexity. Whether you build EHR software, clinical decision support, remote patient monitoring, or FHIR-compliant data platforms, your operations face a problem no generic automation tool handles well: patient data cannot leave your infrastructure.

Routing PHI through Zapier or Make.com creates a HIPAA Business Associate Agreement gap the moment data touches their multi-tenant cloud. The five workflows below run entirely on self-hosted n8n — your data never leaves your VPC.

All workflows include import-ready JSON. They are built for HealthTech SaaS vendors: EHR companies (Epic, Cerner, Athena Health competitors), RCM platforms, clinical decision support SaaS, patient engagement platforms, and FHIR API vendors.


Workflow 1 — New Health System Client Onboarding Drip

Landing a hospital system or large clinic group is a 6-month sales cycle. The onboarding that follows determines whether they renew. This workflow automates the first 7 days:

  • Trigger: Google Sheets row added (new client signed)
  • Classify tier: TIER1_IDN_ENTERPRISE (≥500 beds or ≥$1M ARR) → TIER4_SMALL_PRACTICE, plus compliance flags: HIPAA_BAA_REQUIRED, FDA_SAMD_APPLICABLE, ONC_CURES_FHIR_R4_REQUIRED, CMS_INTEROP_RULE_APPLICABLE, GDPR_DPA_REQUIRED
  • Day 0: Welcome email + CSM Slack alert + Postgres audit log (SOC 2 CC7.1)
  • Day 3: Check-in email
  • Day 7: Value email with three workflows to activate immediately

The compliance flag system means your enterprise CS team knows immediately whether this client needs a BAA countersigned, an FDA SaMD change log, or an ONC FHIR R4 API audit before go-live.

{
  "name": "HealthTech New Client Onboarding Drip",
  "nodes": [
    {
      "id": "1",
      "name": "Google Sheets Trigger",
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "parameters": {
        "sheetId": "YOUR_SHEET_ID",
        "range": "Clients!A:Z",
        "event": "rowAdded"
      },
      "position": [
        100,
        300
      ]
    },
    {
      "id": "2",
      "name": "Classify Client Tier",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const r = $input.first().json;\nconst arr = r.arr_usd ? parseFloat(r.arr_usd) : 0;\nconst beds = r.beds_or_covered_lives ? parseInt(r.beds_or_covered_lives) : 0;\nlet tier = 'TIER4_SMALL_PRACTICE';\nif (arr >= 1000000 || beds >= 500) tier = 'TIER1_IDN_ENTERPRISE';\nelse if (arr >= 250000 || beds >= 100) tier = 'TIER2_HOSPITAL_SYSTEM';\nelse if (arr >= 50000 || beds >= 10) tier = 'TIER3_CLINIC_GROUP';\nconst flags = [];\nif (r.processes_phi === 'yes') flags.push('HIPAA_BAA_REQUIRED');\nif (r.clinical_decision_support === 'yes') flags.push('FDA_SAMD_APPLICABLE');\nif (r.fhir_api === 'yes') flags.push('ONC_CURES_FHIR_R4_REQUIRED');\nif (r.accepts_medicare === 'yes') flags.push('CMS_INTEROP_RULE_APPLICABLE');\nif (r.processes_eu_data === 'yes') flags.push('GDPR_DPA_REQUIRED');\nreturn [{json: {...r, tier, compliance_flags: flags.join(',')} }];"
      },
      "position": [
        300,
        300
      ]
    },
    {
      "id": "3",
      "name": "Send Day 0 Welcome",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "={{ $json.contact_email }}",
        "subject": "Welcome to FlowKit \u2014 HealthTech Onboarding",
        "message": "Hi {{ $json.contact_name }}, welcome! Your CSM will reach out within 1 business day."
      },
      "position": [
        500,
        300
      ]
    },
    {
      "id": "4",
      "name": "Notify CSM on Slack",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#cs-healthtech",
        "text": "New client: {{ $json.company_name }} | Tier: {{ $json.tier }} | Flags: {{ $json.compliance_flags }} | ARR: ${{ $json.arr_usd }}"
      },
      "position": [
        500,
        450
      ]
    },
    {
      "id": "5",
      "name": "Log to Postgres",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO onboarding_log (client_id, company_name, tier, compliance_flags, arr_usd, created_at) VALUES ('{{ $json.client_id }}', '{{ $json.company_name }}', '{{ $json.tier }}', '{{ $json.compliance_flags }}', {{ $json.arr_usd }}, NOW()) ON CONFLICT (client_id) DO UPDATE SET tier=EXCLUDED.tier, compliance_flags=EXCLUDED.compliance_flags"
      },
      "position": [
        500,
        600
      ]
    },
    {
      "id": "6",
      "name": "Wait 3 Days",
      "type": "n8n-nodes-base.wait",
      "parameters": {
        "amount": 3,
        "unit": "days"
      },
      "position": [
        700,
        300
      ]
    },
    {
      "id": "7",
      "name": "Day 3 Check-In",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "={{ $json.contact_email }}",
        "subject": "Day 3 Check-In \u2014 HealthTech Setup Progress",
        "message": "Hi {{ $json.contact_name }}, just checking in \u2014 how is the setup going? Reply to this email with any questions."
      },
      "position": [
        900,
        300
      ]
    },
    {
      "id": "8",
      "name": "Wait 4 More Days",
      "type": "n8n-nodes-base.wait",
      "parameters": {
        "amount": 4,
        "unit": "days"
      },
      "position": [
        1100,
        300
      ]
    },
    {
      "id": "9",
      "name": "Day 7 Value Email",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "={{ $json.contact_email }}",
        "subject": "Week 1 Complete \u2014 3 Workflows to Try This Week",
        "message": "Hi {{ $json.contact_name }}, here are three workflows your team should activate this week to get immediate value from n8n."
      },
      "position": [
        1300,
        300
      ]
    }
  ],
  "connections": {
    "Google Sheets Trigger": {
      "main": [
        [
          {
            "node": "Classify Client Tier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Client Tier": {
      "main": [
        [
          {
            "node": "Send Day 0 Welcome",
            "type": "main",
            "index": 0
          },
          {
            "node": "Notify CSM on Slack",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log to Postgres",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Day 0 Welcome": {
      "main": [
        [
          {
            "node": "Wait 3 Days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 3 Days": {
      "main": [
        [
          {
            "node": "Day 3 Check-In",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Day 3 Check-In": {
      "main": [
        [
          {
            "node": "Wait 4 More Days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 4 More Days": {
      "main": [
        [
          {
            "node": "Day 7 Value Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 2 — EHR/FHIR API Health Monitor

When your EHR integration goes down at 3 AM, clinicians can't access patient records. This monitor catches failures before your customers page your on-call team:

  • Every 3 minutes: pings all EHR endpoints from Postgres config table
  • DOWN: HTTP 5xx or timeout — Slack alert with HIPAA §164.312(a)(2)(ii) automatic logoff risk note
  • STALE_DATA: data timestamp >15 minutes old — flags ONC Cures §170.315(g)(10) real-time freshness requirement
  • DEGRADED: response >5 seconds — flags ONC API availability guidelines
  • 30-minute dedup: $getWorkflowStaticData prevents alert storms during extended outages
  • Postgres audit trail: ON CONFLICT DO NOTHING ensures HIPAA §164.312(b) audit log integrity
{
  "name": "EHR/FHIR API Health Monitor",
  "nodes": [
    {
      "id": "1",
      "name": "Every 3 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 3
            }
          ]
        }
      },
      "position": [
        100,
        300
      ]
    },
    {
      "id": "2",
      "name": "Load EHR Endpoints",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT endpoint_id, name, url, type, criticality FROM ehr_endpoints WHERE active = true"
      },
      "position": [
        300,
        300
      ]
    },
    {
      "id": "3",
      "name": "Split Into Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "parameters": {
        "batchSize": 1
      },
      "position": [
        500,
        300
      ]
    },
    {
      "id": "4",
      "name": "Ping EHR Endpoint",
      "type": "n8n-nodes-base.httpRequest",
      "parameters": {
        "url": "={{ $json.url }}",
        "method": "GET",
        "timeout": 10000,
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "position": [
        700,
        300
      ]
    },
    {
      "id": "5",
      "name": "Evaluate Health",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const endpoint = $('Load EHR Endpoints').first().json;\nconst resp = $input.first();\nconst statusCode = resp.json.statusCode || 200;\nconst respTime = resp.json.responseTime || 0;\nconst lastData = endpoint.last_data_timestamp ? new Date(endpoint.last_data_timestamp) : null;\nconst staleMins = lastData ? (Date.now() - lastData.getTime()) / 60000 : 0;\nlet status = 'HEALTHY';\nlet message = '';\nif (statusCode >= 500 || resp.json.error) {\n  status = 'DOWN'; message = `EHR endpoint ${endpoint.name} returned ${statusCode} \u2014 HIPAA \u00a7164.312(a)(2)(ii) automatic logoff risk if auth service affected`;\n} else if (staleMins > 15) {\n  status = 'STALE_DATA'; message = `${endpoint.name} data stale ${Math.round(staleMins)} min \u2014 FHIR R4 \u00a7170.315(g)(10) real-time data freshness requirement at risk`;\n} else if (respTime > 5000) {\n  status = 'DEGRADED'; message = `${endpoint.name} slow ${respTime}ms \u2014 patient access API latency may breach ONC response time guidelines`;\n}\nconst prev = $getWorkflowStaticData('global');\nconst dedupKey = `${endpoint.endpoint_id}_${status}`;\nconst lastAlert = prev[dedupKey] || 0;\nconst shouldAlert = status !== 'HEALTHY' && (Date.now() - lastAlert) > 1800000;\nif (shouldAlert) prev[dedupKey] = Date.now();\n$setWorkflowStaticData('global', prev);\nreturn [{json: {...endpoint, status, message, shouldAlert, statusCode, respTime}}];"
      },
      "position": [
        900,
        300
      ]
    },
    {
      "id": "6",
      "name": "Alert If Unhealthy",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.shouldAlert }}",
              "value2": true
            }
          ]
        }
      },
      "position": [
        1100,
        300
      ]
    },
    {
      "id": "7",
      "name": "Slack #healthtech-ops",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#healthtech-ops",
        "text": "\ud83d\udea8 EHR API {{ $json.status }}: {{ $json.message }}"
      },
      "position": [
        1300,
        200
      ]
    },
    {
      "id": "8",
      "name": "Log to Postgres",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO ehr_incidents (endpoint_id, status, message, detected_at) VALUES ('{{ $json.endpoint_id }}', '{{ $json.status }}', '{{ $json.message }}', NOW()) ON CONFLICT (endpoint_id, detected_at) DO NOTHING"
      },
      "position": [
        1300,
        400
      ]
    }
  ],
  "connections": {
    "Every 3 Minutes": {
      "main": [
        [
          {
            "node": "Load EHR Endpoints",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load EHR Endpoints": {
      "main": [
        [
          {
            "node": "Split Into Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Into Batches": {
      "main": [
        [
          {
            "node": "Ping EHR Endpoint",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ping EHR Endpoint": {
      "main": [
        [
          {
            "node": "Evaluate Health",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evaluate Health": {
      "main": [
        [
          {
            "node": "Alert If Unhealthy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert If Unhealthy": {
      "main": [
        [
          {
            "node": "Slack #healthtech-ops",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log to Postgres",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 3 — HIPAA/HITECH/ONC Cures/FDA SaMD Compliance Deadline Tracker

HealthTech SaaS compliance is not one regulation — it is a stack. This tracker covers the full stack:

Deadline Type Regulation Consequence of Missing
HIPAA_BAA_RENEWAL §164.504(e)(2) BAA lapse = PHI disclosure without authorization
HIPAA_SECURITY_RISK_ANALYSIS §164.308(a)(1)(ii)(A) Annual SRA required — OCR audit finding
HITECH_BREACH_HHS_ANNUAL §13402(e)(4) Annual small-breach log submission to HHS
ONC_FHIR_R4_AUDIT §170.315(g)(10) SMART on FHIR R4 mandatory Dec 2022
ONC_INFO_BLOCKING_REVIEW 21st Century Cures Act §4006 Up to $1M/violation — OIG enforcement
FDA_SAMD_CHANGE_CONTROL 21 CFR §820.30 / IEC 62304 SaMD change without assessment = recall risk
CMS_INTEROP_RULE_API_AUDIT CMS-9115-F Patient access API non-compliance = CMS contract risk
GDPR_DSR_30DAY GDPR Art.17 €20M or 4% global revenue

The workflow runs weekdays at 8 AM, tiers deadlines OVERDUE/CRITICAL/URGENT/WARNING/NOTICE, deduplicates with $getWorkflowStaticData (4-hour window), and routes Slack @here for the first three tiers plus email for URGENT and NOTICE.

{
  "name": "HIPAA/HITECH/ONC Cures Compliance Deadline Tracker",
  "nodes": [
    {
      "id": "1",
      "name": "Weekdays 8 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1-5"
            }
          ]
        }
      },
      "position": [
        100,
        300
      ]
    },
    {
      "id": "2",
      "name": "Load Deadlines",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "readRows",
        "sheetId": "YOUR_SHEET_ID",
        "range": "Deadlines!A:G"
      },
      "position": [
        300,
        300
      ]
    },
    {
      "id": "3",
      "name": "Classify Urgency",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const actionMap = {\n  HIPAA_BAA_RENEWAL: 'Renew BAA with all Business Associates \u2014 HIPAA \u00a7164.504(e)(2)',\n  HIPAA_SECURITY_RISK_ANALYSIS: 'Annual Security Risk Analysis \u2014 HIPAA \u00a7164.308(a)(1)(ii)(A)',\n  HIPAA_PRIVACY_TRAINING: 'Annual workforce privacy training \u2014 HIPAA \u00a7164.530(b)',\n  HIPAA_BREACH_DRILL: 'Test breach notification procedure \u2014 HIPAA \u00a7164.410 60-day window',\n  HITECH_BREACH_HHS_ANNUAL: 'Submit annual small-breach log to HHS \u2014 HITECH \u00a713402(e)(4)',\n  ONC_FHIR_R4_AUDIT: 'SMART on FHIR R4 \u00a7170.315(g)(10) API access audit',\n  ONC_INFO_BLOCKING_REVIEW: '21st Century Cures Act \u00a74006 info-blocking practice review \u2014 up to $1M/violation',\n  FDA_SAMD_CHANGE_CONTROL: 'FDA SaMD software change assessment \u2014 21 CFR Part 820.30 / IEC 62304',\n  CMS_INTEROP_RULE_API_AUDIT: 'CMS-9115-F patient access API performance review',\n  GDPR_DPA_REVIEW: 'Review Data Processing Agreements with EU data processors \u2014 GDPR Art.28',\n  GDPR_DSR_30DAY: 'Data Subject Request response within 30 days \u2014 GDPR Art.17',\n  CCPA_ANNUAL_PRIVACY: 'Annual California Privacy Notice update \u2014 CCPA \u00a71798.100',\n  SOC2_TYPE2_RENEWAL: 'SOC 2 Type II annual renewal \u2014 CC6.1 logical access controls',\n  NIST_CSF_REVIEW: 'NIST Cybersecurity Framework annual gap assessment'\n};\nconst today = new Date();\nconst prev = $getWorkflowStaticData('global');\nconst results = [];\nfor (const row of $input.all().map(i => i.json)) {\n  const due = new Date(row.due_date);\n  const days = Math.round((due - today) / 86400000);\n  let urgency = null;\n  if (days < 0) urgency = 'OVERDUE';\n  else if (days <= 3) urgency = 'CRITICAL';\n  else if (days <= 7) urgency = 'URGENT';\n  else if (days <= 14) urgency = 'WARNING';\n  else if (days <= 30) urgency = 'NOTICE';\n  if (!urgency) continue;\n  const key = row.deadline_id + '_' + urgency;\n  const last = prev[key] || 0;\n  if (Date.now() - last < 14400000) continue;\n  prev[key] = Date.now();\n  results.push({json: {...row, urgency, days_until: days, action: actionMap[row.deadline_type] || row.deadline_type}});\n}\n$setWorkflowStaticData('global', prev);\nreturn results;"
      },
      "position": [
        500,
        300
      ]
    },
    {
      "id": "4",
      "name": "Route by Urgency",
      "type": "n8n-nodes-base.switch",
      "parameters": {
        "rules": {
          "rules": [
            {
              "value1": "={{ $json.urgency }}",
              "operation": "equal",
              "value2": "OVERDUE"
            },
            {
              "value1": "={{ $json.urgency }}",
              "operation": "equal",
              "value2": "CRITICAL"
            },
            {
              "value1": "={{ $json.urgency }}",
              "operation": "equal",
              "value2": "URGENT"
            }
          ]
        },
        "fallbackOutput": 3
      },
      "position": [
        700,
        300
      ]
    },
    {
      "id": "5",
      "name": "Slack @here",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#healthtech-compliance",
        "text": "<!here> {{ $json.urgency }} \u2014 {{ $json.deadline_type }} | Due: {{ $json.due_date }} ({{ $json.days_until }} days) | Action: {{ $json.action }}"
      },
      "position": [
        900,
        200
      ]
    },
    {
      "id": "6",
      "name": "Email Compliance Officer",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "compliance@yourcompany.com",
        "subject": "[{{ $json.urgency }}] {{ $json.deadline_type }} due {{ $json.due_date }}",
        "message": "{{ $json.urgency }}: {{ $json.deadline_type }}\\nDue: {{ $json.due_date }} ({{ $json.days_until }} days)\\nAction required: {{ $json.action }}\\nRegulation: {{ $json.regulation_ref }}"
      },
      "position": [
        900,
        400
      ]
    }
  ],
  "connections": {
    "Weekdays 8 AM": {
      "main": [
        [
          {
            "node": "Load Deadlines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Deadlines": {
      "main": [
        [
          {
            "node": "Classify Urgency",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Urgency": {
      "main": [
        [
          {
            "node": "Route by Urgency",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Urgency": {
      "main": [
        [
          {
            "node": "Slack @here",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack @here",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Email Compliance Officer",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Email Compliance Officer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 4 — PHI Breach & 21st Century Cures Info-Blocking Alert Pipeline

Your EHR integration fires a webhook when something goes wrong. This pipeline classifies the incident and opens the compliance clock:

  • PHI_BREACH_MAJOR (≥500 records): CRITICAL — 60-day HHS notification + individual notification (HITECH §13402)
  • INFO_BLOCKING_COMPLAINT: CRITICAL — Immediate OIG review, up to $1M/violation (21st Century Cures Act §4006). The irony: if you track info-blocking complaints in Zapier, Zapier itself becomes part of your API audit chain.
  • UNAUTHORIZED_EHR_ACCESS: HIGH — 24-hour audit log review (HIPAA §164.312(b))
  • SAMD_ADVERSE_EVENT: HIGH — 30-day FDA MDR report (21 CFR Part 803.50)
  • FHIR_API_OUTAGE: HIGH — 4-hour ONC Cures availability window
  • GDPR_DATA_BREACH: HIGH — 72-hour supervisory authority notification (GDPR Art.33)

Responds 200 immediately with incident classification. ON CONFLICT DO NOTHING in Postgres ensures audit trail deduplication.

{
  "name": "PHI Breach & Info-Blocking Alert Pipeline",
  "nodes": [
    {
      "id": "1",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "parameters": {
        "path": "healthtech-incident",
        "responseMode": "responseNode"
      },
      "position": [
        100,
        300
      ]
    },
    {
      "id": "2",
      "name": "Classify Incident",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const e = $input.first().json.body || $input.first().json;\nconst type = e.incident_type || '';\nconst phiRecords = parseInt(e.phi_records_affected) || 0;\nconst classes = {\n  PHI_BREACH_MAJOR: {severity:'CRITICAL', window:'60 days to HHS + individual notification', regulation:'HITECH \u00a713402 / HIPAA \u00a7164.410', slah: 60},\n  PHI_BREACH_MINOR: {severity:'CRITICAL', window:'60 days to HHS', regulation:'HIPAA \u00a7164.410 <500 records \u2014 annual log to HHS', slah: 60},\n  INFO_BLOCKING_COMPLAINT: {severity:'CRITICAL', window:'Immediate OIG review \u2014 up to $1M/violation', regulation:'21st Century Cures Act \u00a74006 / ONC Cures Rule \u00a7170.315(g)(10)', slah: 1},\n  UNAUTHORIZED_EHR_ACCESS: {severity:'HIGH', window:'24 hours \u2014 HIPAA \u00a7164.308(a)(1)(ii)(D) audit log review', regulation:'HIPAA \u00a7164.312(b) audit controls', slah: 24},\n  SAMD_ADVERSE_EVENT: {severity:'HIGH', window:'30 days to FDA \u2014 21 CFR Part 803.50', regulation:'FDA MDR reporting requirement for SaMD malfunctions', slah: 720},\n  FHIR_API_OUTAGE: {severity:'HIGH', window:'4 hours \u2014 ONC Cures Rule API availability requirement', regulation:'\u00a7170.315(g)(10) API must be continuously available', slah: 4},\n  GDPR_DATA_BREACH: {severity:'HIGH', window:'72 hours to supervisory authority', regulation:'GDPR Art.33', slah: 72}\n};\nconst cls = classes[type] || {severity:'MEDIUM', window:'Review within 48h', regulation:'Internal policy', slah: 48};\nconst prev = $getWorkflowStaticData('global');\nconst key = e.incident_id + '_' + type;\nconst last = prev[key] || 0;\nconst shouldAlert = (Date.now() - last) > 1800000;\nif (shouldAlert) prev[key] = Date.now();\n$setWorkflowStaticData('global', prev);\nreturn [{json: {...e, ...cls, type, phiRecords, shouldAlert}}];"
      },
      "position": [
        300,
        300
      ]
    },
    {
      "id": "3",
      "name": "Respond 200 ACK",
      "type": "n8n-nodes-base.respondToWebhook",
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({received: true, incident_id: $json.incident_id, severity: $json.severity, response_window: $json.window}) }}"
      },
      "position": [
        500,
        500
      ]
    },
    {
      "id": "4",
      "name": "Alert If New",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.shouldAlert }}",
              "value2": true
            }
          ]
        }
      },
      "position": [
        500,
        300
      ]
    },
    {
      "id": "5",
      "name": "Slack #security-emergency",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#healthtech-security",
        "text": "<!channel> {{ $json.severity }} \u2014 {{ $json.type }} | Records: {{ $json.phiRecords }} | Window: {{ $json.window }} | Reg: {{ $json.regulation }} | ID: {{ $json.incident_id }}"
      },
      "position": [
        700,
        200
      ]
    },
    {
      "id": "6",
      "name": "Log to Postgres",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO phi_incidents (incident_id, incident_type, severity, phi_records, window_hours, regulation, detected_at) VALUES ('{{ $json.incident_id }}', '{{ $json.type }}', '{{ $json.severity }}', {{ $json.phiRecords }}, {{ $json.slah }}, '{{ $json.regulation }}', NOW()) ON CONFLICT (incident_id) DO NOTHING"
      },
      "position": [
        700,
        400
      ]
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Classify Incident",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Incident": {
      "main": [
        [
          {
            "node": "Alert If New",
            "type": "main",
            "index": 0
          }
        ]
      ],
      "main2": [
        [
          {
            "node": "Respond 200 ACK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Alert If New": {
      "main": [
        [
          {
            "node": "Slack #security-emergency",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log to Postgres",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 5 — Weekly HealthTech Platform KPI Dashboard

Your CEO, CTO, and Chief Compliance Officer need one email every Monday morning that tells them whether the platform is healthy:

  • Parallel Postgres queries: platform metrics (ARR, API calls, PHI records, latency) + client health distribution
  • WoW% via $getWorkflowStaticData: stores last week's ARR for accurate comparison
  • Color-coded HTML table: ARR green/orange/red by growth %, at-risk client count flagged
  • CCO BCC: closes the SOC 2 CC7.2 monitoring governance gap
{
  "name": "Weekly HealthTech Platform KPI Dashboard",
  "nodes": [
    {
      "id": "1",
      "name": "Monday 8 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "position": [
        100,
        300
      ]
    },
    {
      "id": "2",
      "name": "Platform Metrics",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT date_trunc('week', created_at) as week, COUNT(DISTINCT client_id) as active_clients, SUM(api_calls) as total_api_calls, SUM(phi_records_processed) as phi_records, AVG(api_latency_ms) as avg_latency, SUM(arr_usd) as total_arr FROM platform_metrics WHERE created_at >= NOW() - INTERVAL '14 days' GROUP BY 1 ORDER BY 1 DESC LIMIT 2"
      },
      "position": [
        300,
        200
      ]
    },
    {
      "id": "3",
      "name": "Client Health",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(*) FILTER (WHERE health_score >= 80) as healthy, COUNT(*) FILTER (WHERE health_score >= 60 AND health_score < 80) as at_risk, COUNT(*) FILTER (WHERE health_score < 60) as critical, COUNT(*) FILTER (WHERE tier = 'TIER1_IDN_ENTERPRISE') as enterprise_count FROM client_health WHERE active = true"
      },
      "position": [
        300,
        450
      ]
    },
    {
      "id": "4",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "parameters": {
        "mode": "combine",
        "combinationMode": "multiplex"
      },
      "position": [
        500,
        300
      ]
    },
    {
      "id": "5",
      "name": "Build Report",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "const [thisWeek, lastWeek] = $input.all().filter(i => i.json.week).map(i => i.json);\nconst health = $input.all().find(i => i.json.healthy !== undefined)?.json || {};\nconst prev = $getWorkflowStaticData('global');\nconst wowArr = thisWeek && lastWeek && lastWeek.total_arr > 0\n  ? (((thisWeek.total_arr - lastWeek.total_arr) / lastWeek.total_arr) * 100).toFixed(1)\n  : 'N/A';\nconst wowCalls = thisWeek && lastWeek && lastWeek.total_api_calls > 0\n  ? (((thisWeek.total_api_calls - lastWeek.total_api_calls) / lastWeek.total_api_calls) * 100).toFixed(1)\n  : 'N/A';\nprev.last_arr = thisWeek?.total_arr || 0;\n$setWorkflowStaticData('global', prev);\nconst arrColor = wowArr >= 5 ? 'green' : wowArr >= 0 ? 'orange' : 'red';\nconst html = \\`<h2>HealthTech Platform Weekly KPI</h2>\n<table border=\"1\" cellpadding=\"8\" style=\"border-collapse:collapse\">\n<tr><th>Metric</th><th>This Week</th><th>WoW %</th></tr>\n<tr><td>Total ARR</td><td>$\\${(thisWeek?.total_arr||0).toLocaleString()}</td><td style=\"color:\\${arrColor}\">\\${wowArr}%</td></tr>\n<tr><td>API Calls</td><td>\\${(thisWeek?.total_api_calls||0).toLocaleString()}</td><td>\\${wowCalls}%</td></tr>\n<tr><td>PHI Records Processed</td><td>\\${(thisWeek?.phi_records||0).toLocaleString()}</td><td></td></tr>\n<tr><td>Avg API Latency</td><td>\\${Math.round(thisWeek?.avg_latency||0)}ms</td><td></td></tr>\n<tr><td>Enterprise Clients</td><td>\\${health.enterprise_count||0}</td><td></td></tr>\n<tr><td>Healthy Clients</td><td style=\"color:green\">\\${health.healthy||0}</td><td></td></tr>\n<tr><td>At-Risk Clients</td><td style=\"color:orange\">\\${health.at_risk||0}</td><td></td></tr>\n<tr><td>Critical Clients</td><td style=\"color:red\">\\${health.critical||0}</td><td></td></tr>\n</table>\n<p>Store: <a href=\"https://stripeai.gumroad.com\">stripeai.gumroad.com</a></p>\\`;\nreturn [{json: {html, wowArr, wowCalls, thisWeek, health}}];"
      },
      "position": [
        700,
        300
      ]
    },
    {
      "id": "6",
      "name": "Email CEO",
      "type": "n8n-nodes-base.gmail",
      "parameters": {
        "to": "ceo@yourcompany.com",
        "cc": "cto@yourcompany.com,cco@yourcompany.com,vp-cs@yourcompany.com",
        "subject": "HealthTech Weekly KPI \u2014 {{ $now.format('YYYY-MM-DD') }}",
        "message": "={{ $json.html }}",
        "options": {
          "isHtml": true
        }
      },
      "position": [
        900,
        200
      ]
    },
    {
      "id": "7",
      "name": "Slack #exec-kpis",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#exec-kpis",
        "text": "HealthTech Weekly KPI: ARR WoW={{ $json.wowArr }}% | API Calls WoW={{ $json.wowCalls }}% | At-Risk={{ $json.health.at_risk }} clients"
      },
      "position": [
        900,
        400
      ]
    }
  ],
  "connections": {
    "Monday 8 AM": {
      "main": [
        [
          {
            "node": "Platform Metrics",
            "type": "main",
            "index": 0
          },
          {
            "node": "Client Health",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Platform Metrics": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Client Health": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Build Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Report": {
      "main": [
        [
          {
            "node": "Email CEO",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack #exec-kpis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why Self-Host n8n for HealthTech SaaS

Factor Zapier/Make Self-Hosted n8n
HIPAA BAA Standard ToS ≠ BAA You execute your own BAA as data controller
PHI data residency Multi-tenant US/EU cloud Stays in your HIPAA-compliant VPC
HITECH audit trail 30-day task log Permanent Postgres log (HITECH §13402 60-day + 6-year retention)
ONC FHIR R4 audit chain Zapier in your Art.30 chain n8n inside your authorization boundary
FDA SaMD change control Software changes not versioned n8n workflows are git-versionable JSON
Cost at 50M EHR events/month ~$50,000/month ~$400/month VPS (99.2% reduction)

The 21st Century Cures Act §4006 irony: if your information-blocking compliance tracker runs on a third-party cloud platform, that platform is in your API audit chain. OCR and OIG examiners will ask.


Get These Workflows + 10 More

All five workflows above are import-ready. Get them plus a full library of n8n templates at stripeai.gumroad.com.

Templates include: Email Auto-Responder, AI Customer Support Bot, Lead Capture to CRM, Invoice Generator, Social Cross-Poster, and more — all production-ready JSON.

Top comments (0)