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
}
]
]
}
}
}
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:
$getWorkflowStaticDataprevents alert storms during extended outages -
Postgres audit trail:
ON CONFLICT DO NOTHINGensures 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
}
]
]
}
}
}
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
}
]
]
}
}
}
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
}
]
]
}
}
}
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
}
]
]
}
}
}
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)