DEV Community

Alex Kane
Alex Kane

Posted on

n8n for HealthTech SaaS: 5 Automations for HIPAA, HITECH, 21 CFR Part 11, and CMS Compliance (Free Workflow JSON)

HealthTech SaaS companies selling to hospitals, payers, clinics, and pharma operate inside one of the most heavily regulated compliance stacks in the US. HIPAA, HITECH, FDA 21 CFR Part 11, and CMS interoperability rules create overlapping obligations — and the automation tools you use to run your platform can become a liability.

Using Zapier or Make.com to run your HIPAA-adjacent workflows? You've added a Business Associate. Every cloud iPaaS that processes Protected Health Information requires a signed BAA. More importantly, under the HIPAA Security Rule (45 CFR §164.312), your risk analysis must account for ePHI in every system it touches — including your automation platform.

Self-hosted n8n changes the equation: your automation layer stays inside your HIPAA-compliant infrastructure. No BAA with the automation vendor. No ePHI routing through a third-party cloud. PHI never leaves your environment.

This post covers 5 production-ready workflows with import-ready JSON for HealthTech SaaS vendors.


Why Self-Hosted n8n for HealthTech SaaS?

Compliance Concern Cloud iPaaS Risk n8n Self-Hosted
HIPAA BAA Requirement Must sign BAA with Zapier/Make No BAA needed — PHI stays in-enclave
45 CFR §164.312 Access Control Cloud vendor controls access You control every access path
21 CFR Part 11 Audit Trail Zapier logs deleted after 30 days Git-versioned workflows + Postgres audit log
CMS-9115-F FHIR Downtime Outage notification via cloud = data egress Local monitoring, no PHI sent to vendor
HIPAA §164.402 Breach Definition Sending PHI to automation vendor = potential breach No third-party automation vendor in PHI path

Workflow 1: HIPAA/HITECH Breach Notification & Incident Response Pipeline

HIPAA Breach Notification Rule (45 CFR §164.400–414) requires notifying affected individuals within 60 days of discovering a breach. HHS OCR requires notification for all breaches. Breaches affecting 500+ individuals in a single state require prominent media notice.

This webhook-triggered pipeline handles 8 breach/incident types:

  • UNAUTHORIZED_ACCESS_PHI — unauthorized workforce access to ePHI
  • RANSOMWARE_PHI_ENCRYPTED — ransomware encrypting PHI (treated as breach per 2016 HHS guidance)
  • THIRD_PARTY_BREACH_BA — Business Associate breach affecting your data
  • INSIDER_PHI_EXFILTRATION — insider threat with PHI exfiltration
  • MISDIRECTED_PHI_EMAIL — PHI sent to wrong recipient
  • LOST_DEVICE_WITH_PHI — lost/stolen device with unencrypted PHI
  • PHISHING_WITH_PHI_ACCESS — phishing resulting in ePHI system access
  • IMPROPER_DISPOSAL_PHI — improper disposal of PHI media

The Code node computes the 60-day HHS OCR notification deadline, flags whether media notice is required (≥500 individuals), routes critical incidents to #hipaa-security Slack + Privacy Officer email + Legal CC, and logs to hipaa_breach_log Postgres table for OCR audit evidence.

{
  "name": "HIPAA/HITECH Breach Notification & Incident Response Pipeline",
  "nodes": [
    {
      "parameters": {
        "path": "phi-security-incident",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        260,
        300
      ],
      "id": "wh1",
      "name": "PHI Incident Webhook"
    },
    {
      "parameters": {
        "jsCode": "\nconst evt = $input.first().json.body || $input.first().json;\nconst now = new Date();\n\nconst severityMap = {\n  UNAUTHORIZED_ACCESS_PHI: { severity: 'CRITICAL', hitech: 'BREACH', hhs60Day: True, mediaNotice: false },\n  RANSOMWARE_PHI_ENCRYPTED: { severity: 'CRITICAL', hitech: 'BREACH', hhs60Day: True, mediaNotice: true },\n  THIRD_PARTY_BREACH_BA: { severity: 'HIGH', hitech: 'BREACH', hhs60Day: True, mediaNotice: false },\n  INSIDER_PHI_EXFILTRATION: { severity: 'CRITICAL', hitech: 'BREACH', hhs60Day: True, mediaNotice: false },\n  MISDIRECTED_PHI_EMAIL: { severity: 'HIGH', hitech: 'BREACH', hhs60Day: True, mediaNotice: false },\n  LOST_DEVICE_WITH_PHI: { severity: 'HIGH', hitech: 'BREACH', hhs60Day: True, mediaNotice: false },\n  PHISHING_WITH_PHI_ACCESS: { severity: 'CRITICAL', hitech: 'BREACH', hhs60Day: True, mediaNotice: false },\n  IMPROPER_DISPOSAL_PHI: { severity: 'MEDIUM', hitech: 'BREACH', hhs60Day: True, mediaNotice: false }\n};\n\nconst classification = severityMap[evt.incident_type] || { severity: 'MEDIUM', hitech: 'POTENTIAL_BREACH', hhs60Day: False, mediaNotice: false };\n\nconst hhsDeadlineTs = new Date(now.getTime() + 60 * 24 * 60 * 60 * 1000).toISOString();\nconst affected = parseInt(evt.affected_individuals || 0);\nconst requiresMediaNotice = affected >= 500 || classification.mediaNotice;\n\nreturn [{\n  json: {\n    ...evt,\n    ...classification,\n    incident_id: 'PHI-' + now.getTime(),\n    detected_at: now.toISOString(),\n    hhs_notification_deadline: hhsDeadlineTs,\n    requires_media_notice: requiresMediaNotice,\n    affected_individuals: affected,\n    risk_assessment_required: true\n  }\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ],
      "id": "cl1",
      "name": "Classify Breach & Compute Deadlines"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true
          },
          "conditions": [
            {
              "leftValue": "={{ $json.severity }}",
              "rightValue": "CRITICAL",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ]
        },
        "combineOperation": "all"
      },
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2,
      "position": [
        660,
        300
      ],
      "id": "fi1",
      "name": "Critical Severity Filter"
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "#hipaa-security",
          "mode": "name"
        },
        "text": "=\ud83d\udea8 *HIPAA BREACH - {{ $json.severity }}* \ud83d\udea8\n\n*Incident ID:* {{ $json.incident_id }}\n*Type:* {{ $json.incident_type }}\n*Affected Individuals:* {{ $json.affected_individuals }}\n*HITECH Classification:* {{ $json.hitech }}\n*HHS 60-Day Deadline:* {{ $json.hhs_notification_deadline }}\n*Media Notice Required:* {{ $json.requires_media_notice }}\n\n\u26a0\ufe0f Immediate risk assessment required. Activate HIPAA Incident Response Plan.",
        "otherOptions": {}
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        860,
        200
      ],
      "id": "sl1",
      "name": "Slack #hipaa-security Alert"
    },
    {
      "parameters": {
        "fromEmail": "security@yourhealthtech.com",
        "toEmail": "={{ $json.privacy_officer_email || 'privacy@yourhealthtech.com' }}",
        "ccEmail": "legal@yourhealthtech.com",
        "subject": "=[HIPAA BREACH - {{ $json.severity }}] {{ $json.incident_type }} - ID {{ $json.incident_id }}",
        "emailType": "html",
        "message": "=<h2>HIPAA Security Incident Notification</h2><p><strong>Incident ID:</strong> {{ $json.incident_id }}</p><p><strong>Incident Type:</strong> {{ $json.incident_type }}</p><p><strong>Severity:</strong> {{ $json.severity }}</p><p><strong>HITECH Classification:</strong> {{ $json.hitech }}</p><p><strong>Affected Individuals:</strong> {{ $json.affected_individuals }}</p><p><strong>HHS OCR 60-Day Notification Deadline:</strong> {{ $json.hhs_notification_deadline }}</p><p><strong>Media Notice Required (\u2265500 in state):</strong> {{ $json.requires_media_notice }}</p><p><strong>Risk Assessment Required:</strong> Yes \u2014 within 72 hours per HIPAA \u00a7164.402</p><hr><p>Reference: 45 CFR \u00a7164.400-414 (HITECH Breach Notification Rule). For breaches affecting \u2265500 individuals, HHS OCR notification and prominent media notice in affected states required within 60 days of discovery.</p>"
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        860,
        380
      ],
      "id": "em1",
      "name": "Email Privacy Officer + Legal"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO hipaa_breach_log (incident_id, incident_type, severity, hitech_class, affected_individuals, hhs_deadline, requires_media_notice, detected_at, raw_payload) VALUES ('{{ $json.incident_id }}', '{{ $json.incident_type }}', '{{ $json.severity }}', '{{ $json.hitech }}', {{ $json.affected_individuals }}, '{{ $json.hhs_notification_deadline }}', {{ $json.requires_media_notice }}, '{{ $json.detected_at }}', '{{ JSON.stringify($json) }}'::jsonb) ON CONFLICT (incident_id) DO NOTHING"
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1060,
        300
      ],
      "id": "pg1",
      "name": "Log to hipaa_breach_log"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({status:'ok', incident_id: $('Classify Breach & Compute Deadlines').first().json.incident_id, hhs_deadline: $('Classify Breach & Compute Deadlines').first().json.hhs_notification_deadline}) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        1260,
        300
      ],
      "id": "rw1",
      "name": "ACK 200"
    }
  ],
  "connections": {
    "PHI Incident Webhook": {
      "main": [
        [
          {
            "node": "Classify Breach & Compute Deadlines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Breach & Compute Deadlines": {
      "main": [
        [
          {
            "node": "Critical Severity Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Critical Severity Filter": {
      "main": [
        [
          {
            "node": "Slack #hipaa-security Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email Privacy Officer + Legal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Privacy Officer + Legal": {
      "main": [
        [
          {
            "node": "Log to hipaa_breach_log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log to hipaa_breach_log": {
      "main": [
        [
          {
            "node": "ACK 200",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 2: 21 CFR Part 11 / Electronic Signature Audit Trail Monitor

FDA 21 CFR Part 11 requires that computerized systems used in FDA-regulated activities maintain complete, accurate audit trails with date/time stamps. Invalid electronic signatures can invalidate clinical trial records, drug manufacturing batch records, and regulatory submissions.

HealthTech SaaS platforms that support clinical operations, lab data systems, or pharmaceutical manufacturing (LIMS, eTMF, QMS) must demonstrate 21 CFR Part 11 compliance for FDA inspections.

This workflow queries your cfr_part11_records Postgres table daily for:

  • Records with signature_valid = false → CRITICAL
  • Records with audit_trail_complete = false → CRITICAL
  • Records not reviewed in 90+ days → WARNING

It routes to #regulatory-compliance Slack with the FDA reference (§11.10 — computerized system controls). The note in the Slack message about audit trails being a prerequisite for FDA inspection readiness is directly actionable for CTO and QA teams.

{
  "name": "21 CFR Part 11 / eSig Audit Trail Compliance Monitor",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 7 * * 1-5"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        260,
        300
      ],
      "id": "sc2",
      "name": "Weekday 7AM Trigger"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT record_id, record_type, signer_email, signed_at, signature_valid, audit_trail_complete, last_reviewed_at, EXTRACT(EPOCH FROM (NOW() - COALESCE(last_reviewed_at, signed_at)))/86400 AS days_since_review FROM cfr_part11_records WHERE signature_valid = false OR audit_trail_complete = false OR last_reviewed_at < NOW() - INTERVAL '90 days' ORDER BY days_since_review DESC LIMIT 100"
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        460,
        300
      ],
      "id": "pg2",
      "name": "Query 21 CFR Part 11 Records"
    },
    {
      "parameters": {
        "jsCode": "\nconst records = $input.all().map(r => r.json);\nconst now = new Date();\n\nconst flagged = records.map(r => {\n  const issues = [];\n  if (!r.signature_valid) issues.push('INVALID_ESIG');\n  if (!r.audit_trail_complete) issues.push('INCOMPLETE_AUDIT_TRAIL');\n  if (r.days_since_review > 90) issues.push('REVIEW_OVERDUE');\n\n  let status = 'OK';\n  let priority = 'LOW';\n  if (issues.includes('INVALID_ESIG') || issues.includes('INCOMPLETE_AUDIT_TRAIL')) {\n    status = 'NON_COMPLIANT';\n    priority = 'CRITICAL';\n  } else if (issues.includes('REVIEW_OVERDUE')) {\n    status = 'REVIEW_REQUIRED';\n    priority = 'WARNING';\n  }\n\n  return { ...r, issues, status, priority };\n}).filter(r => r.status !== 'OK');\n\nconst critical = flagged.filter(r => r.priority === 'CRITICAL');\nconst warnings = flagged.filter(r => r.priority === 'WARNING');\n\nreturn [{ json: { flagged_total: flagged.length, critical_count: critical.length, warning_count: warnings.length, critical_records: critical.slice(0, 10), warning_records: warnings.slice(0, 10), checked_at: now.toISOString() } }];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        300
      ],
      "id": "cl2",
      "name": "Classify 21 CFR Part 11 Issues"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true
          },
          "conditions": [
            {
              "leftValue": "={{ $json.flagged_total }}",
              "rightValue": 0,
              "operator": {
                "type": "number",
                "operation": "gt"
              }
            }
          ]
        },
        "combineOperation": "all"
      },
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2,
      "position": [
        860,
        300
      ],
      "id": "fi2",
      "name": "Only If Issues Found"
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "#regulatory-compliance",
          "mode": "name"
        },
        "text": "=\u26a0\ufe0f *21 CFR Part 11 Audit Trail Issues Detected*\n\n*Critical (Invalid eSig / Incomplete Trail):* {{ $json.critical_count }}\n*Review Overdue (>90 days):* {{ $json.warning_count }}\n\nReference: FDA 21 CFR Part 11 \u00a711.10 \u2014 computerized systems must maintain complete audit trails with date/time stamps for all records. Invalid electronic signatures may invalidate clinical/regulatory submissions.",
        "otherOptions": {}
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1060,
        300
      ],
      "id": "sl2",
      "name": "Slack #regulatory-compliance"
    }
  ],
  "connections": {
    "Weekday 7AM Trigger": {
      "main": [
        [
          {
            "node": "Query 21 CFR Part 11 Records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query 21 CFR Part 11 Records": {
      "main": [
        [
          {
            "node": "Classify 21 CFR Part 11 Issues",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify 21 CFR Part 11 Issues": {
      "main": [
        [
          {
            "node": "Only If Issues Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Only If Issues Found": {
      "main": [
        [
          {
            "node": "Slack #regulatory-compliance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 3: HIPAA/HITECH/CMS Compliance Deadline Tracker

HealthTech SaaS vendors face a stack of recurring compliance deadlines across multiple frameworks. Missing the HHS OCR annual breach report submission window or failing to renew a BAA before it expires can trigger OCR investigations.

This weekday 8AM workflow reads from a Google Sheets hipaa_deadlines register and tracks 12 deadline types:

Deadline Type Framework Action
HIPAA_ANNUAL_RISK_ASSESSMENT 45 CFR §164.308(a)(1) Conduct annual security risk analysis
HIPAA_SECURITY_AWARENESS_TRAINING 45 CFR §164.308(a)(5) Complete workforce HIPAA training
HIPAA_BAA_ANNUAL_REVIEW HITECH §13401 Review all Business Associate Agreements
HITECH_BREACH_ANNUAL_REPORT 45 CFR §164.408 Submit <500 breaches to HHS OCR
OCR_AUDIT_RESPONSE_DEADLINE 45 CFR §164.504 Respond to OCR document requests (10-day window)
CMS_COPs_ANNUAL_REVIEW 42 CFR §482/485 CMS Conditions of Participation review
FHIR_INTEROPERABILITY_RULE_AUDIT CMS-9115-F FHIR R4 Patient Access API audit
HIPAA_EPHI_BACKUP_TEST 45 CFR §164.308(a)(7) ePHI backup and recovery test
HIPAA_INCIDENT_RESPONSE_DRILL 45 CFR §164.308(a)(6) Annual incident response tabletop
HIPAA_FACILITY_ACCESS_AUDIT 45 CFR §164.310(a)(1) Physical access control audit
CMS_MEANINGFUL_USE_ATTESTATION CMS PI Program Promoting Interoperability attestation window
HIPAA_MEDIA_DISPOSAL_REVIEW 45 CFR §164.310(d)(2) ePHI media disposal certificates review

Tiers: OVERDUE / CRITICAL (≤14 days) / URGENT (≤30) / WARNING (≤60) / NOTICE (≤90). Dedup via alert_sent_date — one alert per deadline per day.

{
  "name": "HIPAA/HITECH/CMS Compliance Deadline Tracker",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1-5"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        260,
        300
      ],
      "id": "sc3",
      "name": "Weekday 8AM Trigger"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "YOUR_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "hipaa_deadlines",
          "mode": "name"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        460,
        300
      ],
      "id": "gs3",
      "name": "Read hipaa_deadlines Sheet"
    },
    {
      "parameters": {
        "jsCode": "\nconst rows = $input.all().map(r => r.json);\nconst now = new Date();\n\nconst actionMap = {\n  HIPAA_ANNUAL_RISK_ASSESSMENT: 'Conduct HIPAA Security Risk Assessment per 45 CFR \u00a7164.308(a)(1). Document all ePHI flows, threats, vulnerabilities.',\n  HIPAA_SECURITY_AWARENESS_TRAINING: 'Complete annual HIPAA Security Awareness Training for all workforce members per 45 CFR \u00a7164.308(a)(5).',\n  HIPAA_BAA_ANNUAL_REVIEW: 'Review all Business Associate Agreements for completeness per HITECH \u00a713401. Confirm BAA covers all current data flows.',\n  HITECH_BREACH_ANNUAL_REPORT: 'Submit annual HIPAA Breach Notification Report to HHS OCR for breaches affecting <500 individuals per 45 CFR \u00a7164.408.',\n  OCR_AUDIT_RESPONSE_DEADLINE: 'Respond to HHS OCR Phase 2 Audit document request. 10-day response window per 45 CFR \u00a7164.504.',\n  CMS_COPs_ANNUAL_REVIEW: 'Review compliance with CMS Conditions of Participation (42 CFR Part 482/485) for certified facilities.',\n  FHIR_INTEROPERABILITY_RULE_AUDIT: 'Audit FHIR R4 Patient Access API compliance per CMS-9115-F. Confirm SMART on FHIR app gallery current.',\n  HIPAA_EPHI_BACKUP_TEST: 'Test ePHI backup and recovery procedures per 45 CFR \u00a7164.308(a)(7). Document recovery time in DR plan.',\n  HIPAA_INCIDENT_RESPONSE_DRILL: 'Conduct annual HIPAA Security Incident Response tabletop exercise per 45 CFR \u00a7164.308(a)(6).',\n  HIPAA_FACILITY_ACCESS_AUDIT: 'Audit physical facility access controls for areas containing ePHI per 45 CFR \u00a7164.310(a)(1).',\n  CMS_MEANINGFUL_USE_ATTESTATION: 'Complete CMS Promoting Interoperability Program attestation window for eligible hospitals/professionals.',\n  HIPAA_MEDIA_DISPOSAL_REVIEW: 'Review ePHI media disposal procedures per 45 CFR \u00a7164.310(d)(2). Confirm shredding/degaussing certificates current.'\n};\n\nconst results = rows.map(row => {\n  const due = new Date(row.due_date);\n  const daysLeft = Math.floor((due - now) / 86400000);\n  const alreadySent = row.alert_sent_date === new Date().toISOString().split('T')[0];\n\n  let status, slackChannel, emailRecipient;\n  if (daysLeft < 0) {\n    status = 'OVERDUE'; slackChannel = '#hipaa-compliance'; emailRecipient = row.compliance_lead_email;\n  } else if (daysLeft <= 14) {\n    status = 'CRITICAL'; slackChannel = '#hipaa-compliance'; emailRecipient = row.compliance_lead_email;\n  } else if (daysLeft <= 30) {\n    status = 'URGENT'; slackChannel = '#compliance-ops'; emailRecipient = row.assigned_owner_email;\n  } else if (daysLeft <= 60) {\n    status = 'WARNING'; slackChannel = '#compliance-ops'; emailRecipient = row.assigned_owner_email;\n  } else if (daysLeft <= 90) {\n    status = 'NOTICE'; slackChannel = null; emailRecipient = row.assigned_owner_email;\n  } else {\n    status = 'OK';\n  }\n\n  return {\n    ...row, daysLeft, status, slackChannel, emailRecipient, alreadySent,\n    action_guidance: actionMap[row.deadline_type] || 'Review compliance requirements for this deadline.'\n  };\n}).filter(r => r.status !== 'OK' && !r.alreadySent);\n\nreturn results.map(r => ({ json: r }));\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        300
      ],
      "id": "cl3",
      "name": "Compute Status & Action Guidance"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true
          },
          "conditions": [
            {
              "leftValue": "={{ $json.status }}",
              "rightValue": "OK",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ]
        },
        "combineOperation": "all"
      },
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2,
      "position": [
        860,
        300
      ],
      "id": "fi3",
      "name": "Filter Actionable"
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "={{ $json.slackChannel || '#compliance-ops' }}",
          "mode": "name"
        },
        "text": "=[{{ $json.status }}] HIPAA/CMS Deadline: {{ $json.deadline_type }}\nDue: {{ $json.due_date }} ({{ $json.daysLeft }} days)\nAssigned: {{ $json.assigned_owner_email }}\nAction: {{ $json.action_guidance }}",
        "otherOptions": {}
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1060,
        200
      ],
      "id": "sl3",
      "name": "Slack Alert"
    },
    {
      "parameters": {
        "fromEmail": "compliance@yourhealthtech.com",
        "toEmail": "={{ $json.emailRecipient }}",
        "subject": "=[{{ $json.status }}] HIPAA Deadline: {{ $json.deadline_type }} \u2014 {{ $json.daysLeft }} days remaining",
        "emailType": "html",
        "message": "=<h3>HIPAA/CMS Compliance Deadline Alert</h3><p><strong>Deadline Type:</strong> {{ $json.deadline_type }}</p><p><strong>Status:</strong> {{ $json.status }}</p><p><strong>Due Date:</strong> {{ $json.due_date }}</p><p><strong>Days Remaining:</strong> {{ $json.daysLeft }}</p><p><strong>Required Action:</strong> {{ $json.action_guidance }}</p>"
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1060,
        420
      ],
      "id": "em3",
      "name": "Email Assigned Owner"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "YOUR_SHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "hipaa_deadlines",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "alert_sent_date": "={{ new Date().toISOString().split('T')[0] }}"
          },
          "matchingColumns": [
            "deadline_id"
          ],
          "schema": [
            {
              "id": "deadline_id",
              "displayName": "deadline_id",
              "required": true,
              "defaultMatch": true,
              "canBeUsedToMatch": true,
              "display": true,
              "type": "string"
            },
            {
              "id": "alert_sent_date",
              "displayName": "alert_sent_date",
              "required": false,
              "defaultMatch": false,
              "canBeUsedToMatch": false,
              "display": true,
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1260,
        300
      ],
      "id": "gs3u",
      "name": "Mark Alert Sent"
    }
  ],
  "connections": {
    "Weekday 8AM Trigger": {
      "main": [
        [
          {
            "node": "Read hipaa_deadlines Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read hipaa_deadlines Sheet": {
      "main": [
        [
          {
            "node": "Compute Status & Action Guidance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute Status & Action Guidance": {
      "main": [
        [
          {
            "node": "Filter Actionable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Actionable": {
      "main": [
        [
          {
            "node": "Slack Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email Assigned Owner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Assigned Owner": {
      "main": [
        [
          {
            "node": "Mark Alert Sent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 4: CMS FHIR R4 & HL7 Interoperability API Health Monitor

CMS-9115-F (21st Century Cures Act Interoperability Rule) requires Impacted Payers to maintain FHIR R4 Patient Access APIs with high availability. The CMS Conditions of Participation (42 CFR Part 482) similarly require certified EHRs to maintain uptime. Persistent downtime may constitute a compliance event requiring CMS notification.

This 15-minute poll monitors 5 FHIR/HL7 endpoints:

  • FHIR_R4_PATIENT_ACCESS — CMS-9115-F §§ 156.120–156.127 Patient Access API
  • FHIR_R4_MEDICATION_REQUEST — USCDI v1 Medication data
  • SMART_ON_FHIR_AUTHORIZATION — SMART on FHIR v1 authorization server
  • HL7_V2_INTERFACE — HL7 v2.x ADT/ORU message interface
  • CMS_DRUG_FORMULARY — CMS-9115-F Part D Formulary API

Uses $getWorkflowStaticData for state transition dedup — only alerts on OPERATIONAL → DOWN or OPERATIONAL → DEGRADED transitions, not on every poll cycle. The Slack alert includes the CMS rule reference and a 72-hour outage notification reminder.

{
  "name": "CMS FHIR R4 & HL7 Interoperability API Health Monitor",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "*/15 * * * *"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        260,
        300
      ],
      "id": "sc4",
      "name": "Every 15 Minutes"
    },
    {
      "parameters": {
        "jsCode": "\nreturn [\n  { json: { endpoint_name: 'FHIR_R4_PATIENT_ACCESS', url: 'https://api.yourhealthtech.com/fhir/r4/Patient', method: 'GET', cms_rule: 'CMS-9115-F \u00a7\u00a7 156.120\u2013156.127', sla_ms: 2000 } },\n  { json: { endpoint_name: 'FHIR_R4_MEDICATION_REQUEST', url: 'https://api.yourhealthtech.com/fhir/r4/MedicationRequest', method: 'GET', cms_rule: 'CMS-9115-F USCDI v1', sla_ms: 2000 } },\n  { json: { endpoint_name: 'SMART_ON_FHIR_AUTHORIZATION', url: 'https://auth.yourhealthtech.com/smart/.well-known/smart-configuration', method: 'GET', cms_rule: 'CMS-9115-F SMART on FHIR v1', sla_ms: 1000 } },\n  { json: { endpoint_name: 'HL7_V2_INTERFACE', url: 'https://hl7.yourhealthtech.com/health', method: 'GET', cms_rule: 'HL7 v2.x ADT/ORU message interface', sla_ms: 3000 } },\n  { json: { endpoint_name: 'CMS_DRUG_FORMULARY', url: 'https://api.yourhealthtech.com/fhir/r4/MedicationKnowledge', method: 'GET', cms_rule: 'CMS-9115-F Part D Formulary', sla_ms: 2000 } }\n];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ],
      "id": "cl4a",
      "name": "Build Endpoint List"
    },
    {
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {
          "timeout": 5000,
          "response": {
            "response": {
              "fullResponse": true
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        300
      ],
      "id": "hr4",
      "name": "Health Check Request"
    },
    {
      "parameters": {
        "jsCode": "\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n  const prev = $getWorkflowStaticData('node');\n  const endpointName = item.json.endpoint_name || 'UNKNOWN';\n  const statusCode = item.json.statusCode || 0;\n  const responseTimeMs = item.json.responseTime || 9999;\n  const slaMs = item.json.sla_ms || 2000;\n\n  let currentStatus = 'OPERATIONAL';\n  if (statusCode === 0 || statusCode >= 500) currentStatus = 'DOWN';\n  else if (responseTimeMs > slaMs) currentStatus = 'DEGRADED';\n  else if (statusCode >= 400) currentStatus = 'DEGRADED';\n\n  const prevStatus = prev[endpointName] || 'OPERATIONAL';\n  const isTransition = prevStatus !== currentStatus;\n\n  prev[endpointName] = currentStatus;\n  $setWorkflowStaticData('node', prev);\n\n  if (isTransition && currentStatus !== 'OPERATIONAL') {\n    results.push({ json: { ...item.json, current_status: currentStatus, previous_status: prevStatus, is_transition: True, checked_at: new Date().toISOString() } });\n  }\n}\n\nreturn results.length > 0 ? results : [{ json: { skip: true } }];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        860,
        300
      ],
      "id": "cl4b",
      "name": "State Transition Dedup"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true
          },
          "conditions": [
            {
              "leftValue": "={{ $json.skip }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "notEquals"
              }
            }
          ]
        },
        "combineOperation": "all"
      },
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2,
      "position": [
        1060,
        300
      ],
      "id": "fi4",
      "name": "Skip If No Transition"
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "#platform-ops",
          "mode": "name"
        },
        "text": "=\ud83d\udd34 *FHIR/HL7 API DOWN: {{ $json.endpoint_name }}*\n\nStatus: {{ $json.current_status }}\nPrev: {{ $json.previous_status }}\nCMS Rule: {{ $json.cms_rule }}\nChecked: {{ $json.checked_at }}\n\n\u26a0\ufe0f CMS-9115-F: Persistent FHIR R4 Patient Access API downtime may constitute a CMS Conditions of Participation compliance event. Notify CMS within 72h if outage exceeds 8h.",
        "otherOptions": {}
      },
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1260,
        300
      ],
      "id": "sl4",
      "name": "Slack #platform-ops Alert"
    }
  ],
  "connections": {
    "Every 15 Minutes": {
      "main": [
        [
          {
            "node": "Build Endpoint List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Endpoint List": {
      "main": [
        [
          {
            "node": "Health Check Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Health Check Request": {
      "main": [
        [
          {
            "node": "State Transition Dedup",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "State Transition Dedup": {
      "main": [
        [
          {
            "node": "Skip If No Transition",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Skip If No Transition": {
      "main": [
        [
          {
            "node": "Slack #platform-ops Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 5: Weekly HealthTech SaaS Compliance KPI Dashboard

Your CEO and Privacy Officer need a weekly view: BAA coverage rate, FHIR enablement, open HIPAA incidents, and any overdue HHS OCR notifications — before the OCR does their investigation.

Monday 8AM dual Postgres query merges customer_health and hipaa_breach_log into a single KPI email. Subject line flags [HHS NOTIFICATION OVERDUE] and [CRITICAL BREACH OPEN] when action is required — so the email subject alone signals urgency even in a crowded inbox.

The footer note is intentional: n8n self-hosted — PHI never leaves your infrastructure. No cloud iPaaS = no HIPAA BAA required for the automation platform itself. That's a direct, accurate statement your Privacy Officer can include in your HIPAA risk analysis documentation.

{
  "name": "Weekly HealthTech SaaS Compliance KPI Dashboard",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        260,
        300
      ],
      "id": "sc5",
      "name": "Monday 8AM Trigger"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(*) AS total_customers, COUNT(CASE WHEN hipaa_baa_signed = true THEN 1 END) AS baa_signed, COUNT(CASE WHEN fhir_r4_enabled = true THEN 1 END) AS fhir_enabled, COUNT(CASE WHEN contract_tier = 'ENTERPRISE' THEN 1 END) AS enterprise_count FROM customer_health"
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        460,
        200
      ],
      "id": "pg5a",
      "name": "Query customer_health"
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(*) AS total_incidents, COUNT(CASE WHEN severity = 'CRITICAL' AND resolved_at IS NULL THEN 1 END) AS open_critical, COUNT(CASE WHEN incident_type LIKE 'HIPAA%' AND created_at > NOW() - INTERVAL '7 days' THEN 1 END) AS hipaa_incidents_7d, COUNT(CASE WHEN hhs_notification_deadline < NOW() AND hhs_notified_at IS NULL THEN 1 END) AS overdue_hhs_notifications FROM hipaa_breach_log"
      },
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        460,
        420
      ],
      "id": "pg5b",
      "name": "Query hipaa_breach_log"
    },
    {
      "parameters": {
        "mode": "combine",
        "combinationMode": "mergeByIndex",
        "options": {}
      },
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        660,
        300
      ],
      "id": "mg5",
      "name": "Merge KPIs"
    },
    {
      "parameters": {
        "jsCode": "\nconst d = $input.first().json;\nconst baaRate = d.total_customers > 0 ? Math.round((d.baa_signed / d.total_customers) * 100) : 0;\nconst fhirRate = d.total_customers > 0 ? Math.round((d.fhir_enabled / d.total_customers) * 100) : 0;\nconst hasOpenCritical = parseInt(d.open_critical) > 0;\nconst hasOverdueHHS = parseInt(d.overdue_hhs_notifications) > 0;\n\nconst subjectFlags = [];\nif (hasOverdueHHS) subjectFlags.push('[HHS NOTIFICATION OVERDUE]');\nif (hasOpenCritical) subjectFlags.push('[CRITICAL BREACH OPEN]');\nconst subjectPrefix = subjectFlags.length > 0 ? subjectFlags.join(' ') + ' ' : '';\n\nconst html = `\n<h2>Weekly HealthTech Compliance KPI \u2014 ${new Date().toISOString().split('T')[0]}</h2>\n<table border=\"1\" cellpadding=\"8\" cellspacing=\"0\" style=\"border-collapse:collapse;font-family:monospace\">\n  <tr style=\"background:#1a1a2e;color:#fff\"><th>KPI</th><th>Value</th><th>Status</th></tr>\n  <tr><td>Total Customers</td><td>${d.total_customers}</td><td>\u2014</td></tr>\n  <tr><td>BAA Signed Rate</td><td>${baaRate}%</td><td style=\"color:${baaRate < 100 ? 'red' : 'green'}\">${baaRate < 100 ? '\u26a0 BAA GAPS' : '\u2713'}</td></tr>\n  <tr><td>FHIR R4 Enabled Rate</td><td>${fhirRate}%</td><td style=\"color:${fhirRate < 80 ? 'orange' : 'green'}\">${fhirRate < 80 ? '\u26a0 LOW' : '\u2713'}</td></tr>\n  <tr><td>Enterprise Customers</td><td>${d.enterprise_count}</td><td>\u2014</td></tr>\n  <tr><td>Total HIPAA Incidents</td><td>${d.total_incidents}</td><td>\u2014</td></tr>\n  <tr><td>Open Critical Incidents</td><td>${d.open_critical}</td><td style=\"color:${hasOpenCritical ? 'red' : 'green'}\">${hasOpenCritical ? '\ud83d\udea8 ACTION REQUIRED' : '\u2713 Clean'}</td></tr>\n  <tr><td>HIPAA Incidents (Last 7d)</td><td>${d.hipaa_incidents_7d}</td><td>\u2014</td></tr>\n  <tr><td>Overdue HHS Notifications</td><td>${d.overdue_hhs_notifications}</td><td style=\"color:${hasOverdueHHS ? 'red' : 'green'}\">${hasOverdueHHS ? '\ud83d\udea8 HHS BREACH' : '\u2713 On Track'}</td></tr>\n</table>\n<p style=\"font-size:11px;color:#666\">n8n self-hosted \u2014 PHI never leaves your infrastructure. No cloud iPaaS = no HIPAA BAA required for the automation platform itself. 45 CFR \u00a7164.502(e) Business Associate Rule.</p>`;\n\nreturn [{ json: { ...d, html, subjectPrefix, baaRate, fhirRate, hasOpenCritical, hasOverdueHHS } }];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        860,
        300
      ],
      "id": "cl5",
      "name": "Build KPI HTML"
    },
    {
      "parameters": {
        "fromEmail": "compliance@yourhealthtech.com",
        "toEmail": "ceo@yourhealthtech.com,cto@yourhealthtech.com",
        "ccEmail": "privacy-officer@yourhealthtech.com",
        "subject": "={{ $json.subjectPrefix }}Weekly HealthTech Compliance KPI \u2014 {{ new Date().toISOString().split('T')[0] }}",
        "emailType": "html",
        "message": "={{ $json.html }}"
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        1060,
        300
      ],
      "id": "em5",
      "name": "Email CEO/CTO + Privacy Officer BCC"
    }
  ],
  "connections": {
    "Monday 8AM Trigger": {
      "main": [
        [
          {
            "node": "Query customer_health",
            "type": "main",
            "index": 0
          },
          {
            "node": "Query hipaa_breach_log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query customer_health": {
      "main": [
        [
          {
            "node": "Merge KPIs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query hipaa_breach_log": {
      "main": [
        [
          {
            "node": "Merge KPIs",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge KPIs": {
      "main": [
        [
          {
            "node": "Build KPI HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build KPI HTML": {
      "main": [
        [
          {
            "node": "Email CEO/CTO + Privacy Officer BCC",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The Self-Hosting Argument for HealthTech SaaS

HIPAA Business Associate Rule (45 CFR §164.502(e)): Any entity that creates, receives, maintains, or transmits PHI on behalf of a covered entity or another BA is a Business Associate. If Zapier or Make.com processes ePHI — even in transit — you need a signed BAA with them. If that BAA doesn't exist and PHI passes through their infrastructure, you may have an impermissible disclosure under 45 CFR §164.502.

21 CFR Part 11 Audit Retention: Zapier retains task history for 30 days on free/starter plans. FDA 21 CFR Part 11 §11.10(e) requires audit trail records to be retained for the lifetime of the record, plus 1 year (or the applicable regulation's longer requirement). A 30-day log window is structurally incompatible with Part 11 compliance. n8n's Postgres-backed execution log is Git-versioned and retention-controlled by you.

CMS-9115-F FHIR Downtime Reporting: If you use a cloud automation tool to monitor FHIR APIs and that tool goes down, you've lost your early warning. More problematically, any PHI passed through the monitoring workflow (even patient identifiers used for test queries) flows through the vendor's infrastructure. Self-hosted monitoring stays inside your HIPAA boundary.


Get the Complete HealthTech Automation Pack

All 5 workflows above are available as import-ready JSON on my n8n automation template store:

👉 stripeai.gumroad.com

The store also has workflows for RegTech SaaS, FinTech, GovTech, PropertyTech, MedTech, InsurTech, and more — all production-grade with full HIPAA/HITECH/FDA compliance context.


Free to use, import, and modify. If this saves you time, share it with your compliance team or engineering org. The n8n community is the best thing about the platform.

Top comments (0)