DEV Community

Alex Kane
Alex Kane

Posted on

n8n for HealthInformatics/HIT SaaS Vendors: 5 Automations for ONC Information Blocking, FHIR API Compliance, and HIPAA Breach Response

The ONC Information Blocking Violation That Happens Before Your Compliance Team Knows About It

Your FHIR API processes a patient data access request at 2:17 AM. A third-party app integration routes the response through your staging environment instead of production. The request is silently dropped. No error is logged. No alert fires. Three weeks later, ONC receives a complaint.

Under the 21st Century Cures Act §4004, that dropped request is a potential information blocking violation — up to $1,000,000 per violation for health IT developers. The clock started the moment the request was dropped, not when your compliance team learned about it.

This article shows five production-ready n8n workflows that close the gap: FHIR API compliance monitoring, ONC information blocking risk tracking, HIPAA breach response pipelines, and compliance deadline management — all self-hosted so your PHI never touches a third-party cloud.


Why HIT Vendors Self-Host n8n

Every time your workflow engine routes patient data, you inherit the data governance obligations of that engine. Zapier's servers receive your PHI. Make.com processes your HL7 FHIR responses. That's a HIPAA Business Associate Agreement gap — and for ONC-certified HIT vendors, routing FHIR data through third-party automation cloud = potential information blocking (you've introduced an intermediary that could impede access).

Self-hosted n8n runs in your VPC or on-prem. PHI never leaves your environment. Your n8n workflows are version-controlled JSON files — git commit history is your ONC audit trail. n8n's self-hosted architecture closes the HIPAA BAA gap that Zapier/Make can't.


Who This Is For

Tier Examples Key Regulations
EHR_VENDOR_ENTERPRISE Epic, Oracle Health, athenahealth, Veradigm 21st Century Cures §4004, ONC 45 CFR §170, HIPAA, USCDI v4, HL7 FHIR R4, ONC TEFCA
HIT_MIDDLEWARE_SAAS Rhapsody, Mirth Connect, Infor Cloverleaf, NextGen ONC info blocking (API interface), HIPAA §164.308, HL7 FHIR R4, CMS §170.315(g)(10)
PATIENT_ENGAGEMENT_SAAS Klara, Luma Health, Relatient, Healow 21st Century Cures patient right of access, HIPAA §164.524, ADA WCAG 2.1 AA, COPPA (minor portals)
CLINICAL_ANALYTICS_SAAS Health Catalyst, Arcadia, Cotiviti, Innovaccer HIPAA §164.514 de-identification, IRB, USCDI, 21st Century Cures, ONC CMC
INTEROPERABILITY_SAAS CommonWell, Carequality, 1upHealth, Redox ONC TEFCA QHIN requirements, HL7 FHIR R4, Direct Trust, USCDI v4, CMS Interoperability
REMOTE_PATIENT_MONITORING_SAAS Current Health, Biofourmis, Validic, Optimize Health FDA SaMD 21 CFR Part 820 (if clinical decision), HIPAA PHI, HL7 FHIR, CMS RPM billing codes
HEALTHIT_STARTUP_SAAS New EHR entrants, specialty HIT startups, health AI platforms ONC 21st Century Cures §4004, HIPAA §164, HL7 FHIR R4, CMS Interoperability Rule

The 5 Workflows

Workflow 1: ONC Information Blocking Risk Monitor

Trigger: Every 15 minutes
Key Regulation: 21st Century Cures Act §4004 / ONC 45 CFR §170.404
Penalty: Up to $1,000,000 per violation for health IT developers, HIEs, and HINs

Reads your information blocking risk registry from Google Sheets. For each open risk, evaluates whether an ONC exception is documented (Preventing Harm / Privacy / Security / Infeasibility / Performance / Content & Manner / Fees). Escalates CONFIRMED_BLOCKING risks as IMMEDIATE_VIOLATION, PROBABLE_BLOCKING without documented exception as HIGH_RISK, and stale risks (30+ days open) as ESCALATE. Alerts #compliance-legal via Slack and emails your ONC compliance officer.

Why it matters: Most information blocking violations are discovered by patient complaints to ONC, not by internal monitoring. By the time a patient files a complaint, the violation clock has been running for weeks. This workflow catches API access failures, routing errors, and missing exception documentation before they become ONC complaints.

{
  "name": "ONC Information Blocking Risk Monitor",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      },
      "id": "w1n01",
      "name": "Every 15 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": "={{ $vars.SHEETS_DOC_ID }}",
        "sheetName": "information_blocking_risks",
        "options": {}
      },
      "id": "w1n02",
      "name": "Read Risk Registry",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// ONC 21st Century Cures Act \u00a74004 Information Blocking Monitor\n// HIT developers: up to $1M/violation | HIEs/HINs: $1M/violation\n// ONC 7 Exceptions: Preventing Harm / Privacy / Security / Infeasibility /\n//   Performance / Content & Manner / Fees\nconst risks = $input.all();\nconst now = new Date();\nconst alerts = [];\nfor (const item of risks) {\n  const d = item.json;\n  if (!d.risk_id || d.status === 'RESOLVED') continue;\n  const daysOpen = d.logged_date\n    ? Math.floor((now - new Date(d.logged_date)) / 86400000) : 0;\n  const exceptionDocs = d.exception_documented === 'YES';\n  let severity, action;\n  if (d.risk_level === 'CONFIRMED_BLOCKING' && !exceptionDocs) {\n    severity = 'IMMEDIATE_VIOLATION';\n    action = 'HALT \u2014 file ONC voluntary disclosure now. $1M/violation risk.';\n  } else if (d.risk_level === 'PROBABLE_BLOCKING' && !exceptionDocs) {\n    severity = 'HIGH_RISK';\n    action = 'Legal review within 24h. Document applicable ONC exception.';\n  } else if (daysOpen > 30 && d.risk_level === 'POTENTIAL_BLOCKING') {\n    severity = 'ESCALATE';\n    action = 'Stale risk (30+ days) \u2014 resolve or document exception within 7 days.';\n  } else {\n    severity = 'MONITORING';\n    action = 'Exception documented. Continue monitoring.';\n  }\n  if (severity !== 'MONITORING') {\n    alerts.push({ severity, action, risk_id: d.risk_id,\n      affected_api: d.affected_api, requestor_org: d.requestor_org,\n      exception_type: d.exception_type || 'NONE', days_open: daysOpen });\n  }\n}\nreturn alerts.length > 0 ? alerts : [{ severity: 'CLEAR', action: 'No active information blocking risks.' }];"
      },
      "id": "w1n03",
      "name": "Assess Information Blocking Risk",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.severity }}",
              "rightValue": "CLEAR",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "w1n04",
      "name": "Active Risk?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.1,
      "position": [
        900,
        300
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "channel": "#compliance-legal",
        "text": "=:rotating_light: *ONC Info Blocking \u2014 {{ $json.severity }}*\nRisk: {{ $json.risk_id }} | API: {{ $json.affected_api }}\nRequestor: {{ $json.requestor_org }} | Days Open: {{ $json.days_open }}\nException: {{ $json.exception_type }}\n*Action:* {{ $json.action }}\n21st Century Cures Act \u00a74004 \u2014 $1M/violation.",
        "otherOptions": {}
      },
      "id": "w1n05",
      "name": "Slack #compliance-legal",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1120,
        220
      ]
    },
    {
      "parameters": {
        "sendTo": "={{ $vars.ONC_OFFICER_EMAIL }}",
        "subject": "=ONC Information Blocking {{ $json.severity }}: {{ $json.risk_id }}",
        "emailType": "text",
        "message": "=ONC Information Blocking Risk\n\nSeverity: {{ $json.severity }}\nRisk ID: {{ $json.risk_id }}\nAffected API: {{ $json.affected_api }}\nRequestor: {{ $json.requestor_org }}\nDays Open: {{ $json.days_open }}\nException Type: {{ $json.exception_type }}\n\nRequired Action: {{ $json.action }}\n\nRef: 21st Century Cures Act \u00a74004 / ONC 45 CFR \u00a7170.404\nPenalty: Up to $1,000,000/violation for HIT developers and HIEs\n\nONC Exceptions: Preventing Harm | Privacy | Security | Infeasibility | Performance | Content & Manner | Fees",
        "options": {}
      },
      "id": "w1n06",
      "name": "Email ONC Officer",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1120,
        380
      ]
    }
  ],
  "connections": {
    "Every 15 Minutes": {
      "main": [
        [
          {
            "node": "Read Risk Registry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Risk Registry": {
      "main": [
        [
          {
            "node": "Assess Information Blocking Risk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Assess Information Blocking Risk": {
      "main": [
        [
          {
            "node": "Active Risk?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Active Risk?": {
      "main": [
        [
          {
            "node": "Slack #compliance-legal",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email ONC Officer",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": []
}
Enter fullscreen mode Exit fullscreen mode

Setup: Import JSON. Set Google Sheets SHEETS_DOC_ID and create information_blocking_risks sheet with columns: risk_id, risk_level (CONFIRMED_BLOCKING/PROBABLE_BLOCKING/POTENTIAL_BLOCKING), exception_documented (YES/NO), exception_type, affected_api, requestor_org, logged_date, status. Configure Slack and Gmail credentials. Set ONC_OFFICER_EMAIL variable.


Workflow 2: FHIR API Uptime & Patient Access Compliance Monitor

Trigger: Every 5 minutes
Key Regulation: 45 CFR §170.315(g)(10) / CMS Interoperability Rule / 21st Century Cures §4004

Fetches your FHIR CapabilityStatement (metadata endpoint). Validates: FHIR version is R4 (4.x), SMART on FHIR authorization endpoint is present in the security extension, and US Core profile count meets USCDI v4 requirements (15+ profiles). Routes NON_COMPLIANT/CRITICAL/WARNING statuses to #platform-ops Slack and logs all checks to a Google Sheet for uptime reporting. Healthy checks are logged silently.

Why it matters: 45 CFR §170.315(g)(10) requires ONC-certified HIT to maintain a FHIR R4 patient access API. Downtime = information blocking. A missing SMART auth endpoint = patients cannot authenticate = access blocked. USCDI v4 profile gaps = ONC certification non-conformance. This workflow gives your engineering team a 5-minute FHIR compliance heartbeat.

{
  "name": "FHIR API Uptime & Patient Access Compliance Monitor",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 5
            }
          ]
        }
      },
      "id": "w2n01",
      "name": "Every 5 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "url": "={{ $vars.FHIR_BASE_URL }}/metadata",
        "options": {
          "timeout": 10000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "w2n02",
      "name": "Fetch FHIR CapabilityStatement",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// 45 CFR \u00a7170.315(g)(10) \u2014 ONC-certified FHIR R4 patient access API\n// API unavailability = potential information blocking (21st Century Cures \u00a74004)\nconst resp = $input.first().json;\nconst fhirVersion = resp.fhirVersion || 'UNKNOWN';\nconst r4 = fhirVersion.startsWith('4.');\nconst restResources = resp.rest?.[0]?.resource || [];\nconst smartExt = resp.rest?.[0]?.security?.extension?.find(e => e.url?.includes('oauth-uris'));\nconst smartPresent = !!smartExt;\nconst profiles = restResources.flatMap(r => r.supportedProfile || []);\nconst usCoreCount = profiles.filter(p => p.includes('us-core')).length;\n// USCDI v4 requires 20+ US Core R4 profiles\nlet status, risk;\nif (!r4) { status = 'NON_COMPLIANT';\n  risk = 'FHIR version ' + fhirVersion + ' \u2014 ONC requires FHIR R4. Immediate remediation.'; }\nelse if (!smartPresent) { status = 'CRITICAL';\n  risk = 'SMART on FHIR auth endpoint missing \u2014 patient access blocked. 45 CFR \u00a7170.315(g)(10).'; }\nelse if (usCoreCount < 15) { status = 'WARNING';\n  risk = usCoreCount + ' US Core profiles detected \u2014 USCDI v4 completeness gap. ONC cert at risk.'; }\nelse { status = 'HEALTHY'; risk = 'FHIR R4 compliant, SMART auth present, ' + usCoreCount + ' US Core profiles.'; }\nreturn [{ status, risk, fhirVersion, smartPresent, usCoreCount, checkedAt: new Date().toISOString() }];"
      },
      "id": "w2n03",
      "name": "Evaluate FHIR Compliance",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.status }}",
              "rightValue": "HEALTHY",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "w2n04",
      "name": "Issue Detected?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.1,
      "position": [
        900,
        300
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "channel": "#platform-ops",
        "text": "=:warning: *FHIR API Compliance \u2014 {{ $json.status }}*\nFHIR Version: {{ $json.fhirVersion }} | SMART Auth: {{ $json.smartPresent }}\nUS Core Profiles: {{ $json.usCoreCount }}\n\n{{ $json.risk }}\n\n45 CFR \u00a7170.315(g)(10) \u2014 ONC-certified patient access API required.\nAPI downtime = information blocking risk ($1M/violation).",
        "otherOptions": {}
      },
      "id": "w2n05",
      "name": "Slack #platform-ops",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1120,
        300
      ]
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": "={{ $vars.SHEETS_DOC_ID }}",
        "sheetName": "fhir_uptime_log",
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "timestamp": "={{ $json.checkedAt }}",
            "status": "={{ $json.status }}",
            "fhir_version": "={{ $json.fhirVersion }}",
            "us_core_profiles": "={{ $json.usCoreCount }}",
            "risk_note": "={{ $json.risk }}"
          }
        },
        "options": {}
      },
      "id": "w2n06",
      "name": "Log to FHIR Uptime Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1120,
        420
      ]
    }
  ],
  "connections": {
    "Every 5 Minutes": {
      "main": [
        [
          {
            "node": "Fetch FHIR CapabilityStatement",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch FHIR CapabilityStatement": {
      "main": [
        [
          {
            "node": "Evaluate FHIR Compliance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evaluate FHIR Compliance": {
      "main": [
        [
          {
            "node": "Issue Detected?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Issue Detected?": {
      "main": [
        [
          {
            "node": "Slack #platform-ops",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log to FHIR Uptime Sheet",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log to FHIR Uptime Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": []
}
Enter fullscreen mode Exit fullscreen mode

Setup: Import JSON. Set FHIR_BASE_URL (e.g., https://api.yourehrsystem.com/fhir/r4). Configure Google Sheets for the fhir_uptime_log sheet. Configure Slack OAuth.


Workflow 3: ONC/HIPAA Compliance Deadline Tracker

Trigger: Weekdays at 8 AM
Key Deadlines: ONC cert renewal, HIPAA annual risk analysis, USCDI version upgrades, HHS OCR breach notification (60-day), FHIR API cert renewal, TEFCA QHIN review, SOC 2 audit, HIPAA BAA renewals

Reads your HIT compliance deadline registry from Google Sheets. Classifies each deadline as OVERDUE / CRITICAL (≤7 days) / URGENT (≤21 days) / WARNING (≤45 days) / NOTICE (≤60 days). Routes alerts to #compliance-team Slack and emails each deadline owner directly. Supports 10 deadline types including the HHS OCR 60-day breach notification window, ONC certification renewals, and Promoting Interoperability reporting.

Why it matters: The HHS OCR 60-day breach notification clock (45 CFR §164.408) starts at discovery, not at report. ONC certification renewal failures create information blocking exposure for the entire period of lapse. USCDI version upgrade deadlines cause ONC certification non-conformance. This workflow ensures no deadline falls through the cracks across your entire HIT compliance calendar.

{
  "name": "ONC/HIPAA Compliance Deadline Tracker",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1-5"
            }
          ]
        }
      },
      "id": "w3n01",
      "name": "Weekdays 8 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": "={{ $vars.SHEETS_DOC_ID }}",
        "sheetName": "hit_compliance_deadlines",
        "options": {}
      },
      "id": "w3n02",
      "name": "Read Deadline Registry",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// ONC/HIPAA/21st Century Cures Compliance Deadline Tracker\nconst deadlines = $input.all();\nconst now = new Date();\nconst alerts = [];\nconst DEADLINE_TYPES = {\n  'ONC_CERT_RENEWAL': { label: 'ONC Certification Renewal', ref: '45 CFR \u00a7170.523' },\n  'HIPAA_RISK_ANALYSIS': { label: 'HIPAA Annual Risk Analysis', ref: '45 CFR \u00a7164.308(a)(1)' },\n  'USCDI_VERSION_UPGRADE': { label: 'USCDI Version Upgrade', ref: 'ONC USCDI v4 mandate' },\n  'HHS_OCR_BREACH_NOTIFICATION': { label: 'HHS OCR Breach Notification 60-day', ref: '45 CFR \u00a7164.408' },\n  'FHIR_API_CERT_RENEWAL': { label: 'FHIR API Certification Renewal', ref: '45 CFR \u00a7170.315(g)(10)' },\n  'TEFCA_QHIN_REVIEW': { label: 'TEFCA QHIN Annual Compliance Review', ref: 'ONC TEFCA Framework v2' },\n  'STATE_HIE_CONTRACT_RENEWAL': { label: 'State HIE Contract Renewal', ref: 'State HIE agreement' },\n  'SOC2_AUDIT_RENEWAL': { label: 'SOC 2 Type II Audit', ref: 'HIPAA BAA / vendor risk' },\n  'BAA_RENEWAL': { label: 'HIPAA BAA Renewal', ref: '45 CFR \u00a7164.308(b)(1)' },\n  'MEANINGFUL_USE_REPORTING': { label: 'Meaningful Use / Promoting Interoperability Reporting', ref: 'CMS EHR Incentive' }\n};\nfor (const item of deadlines) {\n  const d = item.json;\n  if (!d.deadline_type || !d.due_date || d.status === 'COMPLETED') continue;\n  const due = new Date(d.due_date);\n  const daysLeft = Math.ceil((due - now) / 86400000);\n  const meta = DEADLINE_TYPES[d.deadline_type] || { label: d.deadline_type, ref: '' };\n  let severity;\n  if (daysLeft < 0) severity = 'OVERDUE';\n  else if (daysLeft <= 7) severity = 'CRITICAL';\n  else if (daysLeft <= 21) severity = 'URGENT';\n  else if (daysLeft <= 45) severity = 'WARNING';\n  else if (daysLeft <= 60) severity = 'NOTICE';\n  else continue;\n  alerts.push({ severity, days_left: daysLeft, due_date: d.due_date,\n    deadline_type: d.deadline_type, label: meta.label, reg_ref: meta.ref,\n    owner: d.owner, tier: d.customer_tier || 'ALL' });\n}\nreturn alerts.length > 0 ? alerts.sort((a,b) => a.days_left - b.days_left)\n  : [{ severity: 'CLEAR', label: 'No upcoming compliance deadlines.' }];"
      },
      "id": "w3n03",
      "name": "Classify Deadlines",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "leftValue": "={{ $json.severity }}",
              "rightValue": "CLEAR",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "w3n04",
      "name": "Action Required?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.1,
      "position": [
        900,
        300
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "channel": "#compliance-team",
        "text": "=:calendar: *HIT Compliance Deadline \u2014 {{ $json.severity }}*\n{{ $json.label }}\nDue: {{ $json.due_date }} ({{ $json.days_left }} days)\nOwner: {{ $json.owner }} | Tier: {{ $json.tier }}\nRef: {{ $json.reg_ref }}",
        "otherOptions": {}
      },
      "id": "w3n05",
      "name": "Slack #compliance-team",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1120,
        220
      ]
    },
    {
      "parameters": {
        "sendTo": "={{ $json.owner }}",
        "subject": "=HIT Compliance Deadline {{ $json.severity }}: {{ $json.label }} due {{ $json.due_date }}",
        "emailType": "text",
        "message": "=HIT Compliance Deadline Alert\n\nDeadline: {{ $json.label }}\nSeverity: {{ $json.severity }}\nDue Date: {{ $json.due_date }}\nDays Remaining: {{ $json.days_left }}\nRegulatory Reference: {{ $json.reg_ref }}\nCustomer Tier: {{ $json.tier }}\n\nPlease action immediately if CRITICAL or OVERDUE.",
        "options": {}
      },
      "id": "w3n06",
      "name": "Email Deadline Owner",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1120,
        380
      ]
    }
  ],
  "connections": {
    "Weekdays 8 AM": {
      "main": [
        [
          {
            "node": "Read Deadline Registry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Deadline Registry": {
      "main": [
        [
          {
            "node": "Classify Deadlines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Deadlines": {
      "main": [
        [
          {
            "node": "Action Required?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Action Required?": {
      "main": [
        [
          {
            "node": "Slack #compliance-team",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email Deadline Owner",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": []
}
Enter fullscreen mode Exit fullscreen mode

Setup: Import JSON. Create hit_compliance_deadlines Google Sheet with columns: deadline_type (use DEADLINE_TYPES keys), due_date, owner (email), customer_tier, status. Configure Slack and Gmail.


Workflow 4: HIPAA PHI Breach & HHS OCR Incident Response Pipeline

Trigger: Webhook (POST from your breach detection system, SIEM, or manual form)
Key Regulation: 45 CFR §164.402 (breach definition) / §164.404(b) (media notification ≥500) / §164.408 (HHS OCR 60-day) / §164.410 (BA → CE notification)

Accepts a breach report webhook. Classifies the incident type (PHI_UNAUTHORIZED_ACCESS, RANSOMWARE_PHI_EXPOSURE, STOLEN_DEVICE_PHI, MISDIRECTED_PHI_EMAIL, BA_REPORTED_BREACH, and more). Calculates the HHS OCR 60-day notification deadline from discovery date. Flags media notification requirement if 500+ individuals affected. Immediately alerts #breach-response Slack, emails your privacy officer with the full compliance deadline analysis, logs to breach_incident_log, and responds 200 to the caller with the incident ID and HHS deadline.

Why it matters: The most common HIPAA enforcement failure is not the breach itself — it's the missed 60-day HHS OCR notification deadline. This workflow starts the clock and documents it in writing the moment a breach is reported. For 500+ individual breaches, the media notification requirement (§164.404(b)) is frequently missed. For business associates, the BA-to-CE notification obligation (§164.410) without unreasonable delay is a separate clock this workflow tracks.

{
  "name": "HIPAA PHI Breach & HHS OCR Incident Response Pipeline",
  "nodes": [
    {
      "parameters": {
        "path": "phi-breach",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "w4n01",
      "name": "Breach Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        300
      ],
      "webhookId": "phi-breach-001"
    },
    {
      "parameters": {
        "jsCode": "// HIPAA Breach Response Pipeline\n// 45 CFR \u00a7164.402 \u2014 breach of unsecured PHI\n// 45 CFR \u00a7164.408 \u2014 HHS OCR notification within 60 days of discovery\n// 45 CFR \u00a7164.404(b) \u2014 if 500+ individuals: media notification required\n// 45 CFR \u00a7164.410 \u2014 business associate notification to covered entity without unreasonable delay\nconst body = $input.first().json.body;\nconst incidentTypes = {\n  'PHI_UNAUTHORIZED_ACCESS': { hhs60day: true, mediaIf500: true, severity: 'CRITICAL' },\n  'PHI_DISCLOSURE_WITHOUT_AUTHORIZATION': { hhs60day: true, mediaIf500: true, severity: 'CRITICAL' },\n  'RANSOMWARE_PHI_EXPOSURE': { hhs60day: true, mediaIf500: true, severity: 'CRITICAL' },\n  'STOLEN_DEVICE_PHI': { hhs60day: true, mediaIf500: true, severity: 'CRITICAL' },\n  'MISDIRECTED_PHI_EMAIL': { hhs60day: true, mediaIf500: false, severity: 'HIGH' },\n  'PHI_IN_TEST_ENVIRONMENT': { hhs60day: false, mediaIf500: false, severity: 'HIGH' },\n  'UNAUTHORIZED_INTERNAL_ACCESS': { hhs60day: true, mediaIf500: false, severity: 'HIGH' },\n  'BA_REPORTED_BREACH': { hhs60day: true, mediaIf500: true, severity: 'CRITICAL' }\n};\nconst meta = incidentTypes[body.incident_type] || { hhs60day: true, mediaIf500: true, severity: 'CRITICAL' };\nconst discoveryDate = body.discovery_date || new Date().toISOString().split('T')[0];\nconst hhsDeadline = new Date(discoveryDate);\nhhsDeadline.setDate(hhsDeadline.getDate() + 60);\nconst individualsAffected = parseInt(body.individuals_affected) || 0;\nconst mediaRequired = meta.mediaIf500 && individualsAffected >= 500;\nreturn [{\n  incident_id: body.incident_id || 'INC-' + Date.now(),\n  incident_type: body.incident_type, severity: meta.severity,\n  discovery_date: discoveryDate,\n  hhs_ocr_deadline: hhsDeadline.toISOString().split('T')[0],\n  hhs_notification_required: meta.hhs60day,\n  individuals_affected: individualsAffected,\n  media_notification_required: mediaRequired,\n  phi_types: body.phi_types || 'UNKNOWN',\n  incident_description: body.description || '',\n  discovered_by: body.discovered_by || 'SYSTEM'\n}];"
      },
      "id": "w4n02",
      "name": "Parse & Classify Breach",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "channel": "#breach-response",
        "text": "=:sos: *HIPAA PHI BREACH \u2014 {{ $json.severity }}*\nIncident: {{ $json.incident_id }}\nType: {{ $json.incident_type }}\nDiscovered: {{ $json.discovery_date }}\nIndividuals Affected: {{ $json.individuals_affected }}\nPHI Types: {{ $json.phi_types }}\n\n:clock1: HHS OCR 60-day deadline: *{{ $json.hhs_ocr_deadline }}*\n:newspaper: Media notification required: *{{ $json.media_notification_required }}*\n\n45 CFR \u00a7164.408 \u2014 notify HHS OCR within 60 days of discovery.\n45 CFR \u00a7164.404(b) \u2014 500+ individuals requires media notification.\nActivate breach response plan immediately.",
        "otherOptions": {}
      },
      "id": "w4n03",
      "name": "Slack #breach-response",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        680,
        220
      ]
    },
    {
      "parameters": {
        "sendTo": "={{ $vars.PRIVACY_OFFICER_EMAIL }}",
        "subject": "=HIPAA PHI BREACH {{ $json.severity }}: {{ $json.incident_id }} \u2014 HHS OCR deadline {{ $json.hhs_ocr_deadline }}",
        "emailType": "text",
        "message": "=HIPAA PHI BREACH NOTIFICATION\n\nIncident ID: {{ $json.incident_id }}\nSeverity: {{ $json.severity }}\nIncident Type: {{ $json.incident_type }}\nDiscovery Date: {{ $json.discovery_date }}\nDiscovered By: {{ $json.discovered_by }}\nIndividuals Affected: {{ $json.individuals_affected }}\nPHI Types Involved: {{ $json.phi_types }}\nDescription: {{ $json.incident_description }}\n\nCOMPLIANCE DEADLINES:\n- HHS OCR Notification Required: {{ $json.hhs_notification_required }}\n- HHS OCR 60-Day Deadline: {{ $json.hhs_ocr_deadline }}\n- Media Notification Required (500+ individuals): {{ $json.media_notification_required }}\n\nREGULATORY REFERENCES:\n- 45 CFR \u00a7164.402 \u2014 definition of breach of unsecured PHI\n- 45 CFR \u00a7164.408 \u2014 HHS OCR notification within 60 calendar days of discovery\n- 45 CFR \u00a7164.404(b) \u2014 media notification if 500+ individuals in a state\n- 45 CFR \u00a7164.410 \u2014 BA notification to covered entity without unreasonable delay\n\nACTION: Activate breach response plan. Engage legal counsel. Begin HHS OCR notification process.",
        "options": {}
      },
      "id": "w4n04",
      "name": "Email Privacy Officer",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        680,
        380
      ]
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": "={{ $vars.SHEETS_DOC_ID }}",
        "sheetName": "breach_incident_log",
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "incident_id": "={{ $json.incident_id }}",
            "type": "={{ $json.incident_type }}",
            "severity": "={{ $json.severity }}",
            "discovery_date": "={{ $json.discovery_date }}",
            "hhs_deadline": "={{ $json.hhs_ocr_deadline }}",
            "individuals": "={{ $json.individuals_affected }}",
            "media_required": "={{ $json.media_notification_required }}",
            "status": "OPEN"
          }
        },
        "options": {}
      },
      "id": "w4n05",
      "name": "Log Incident",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        680,
        540
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={ \"status\": \"breach_logged\", \"incident_id\": \"{{ $json.incident_id }}\", \"hhs_deadline\": \"{{ $json.hhs_ocr_deadline }}\" }",
        "options": {}
      },
      "id": "w4n06",
      "name": "Respond 200",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        900,
        380
      ]
    }
  ],
  "connections": {
    "Breach Webhook": {
      "main": [
        [
          {
            "node": "Parse & Classify Breach",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse & Classify Breach": {
      "main": [
        [
          {
            "node": "Slack #breach-response",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email Privacy Officer",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log Incident",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Privacy Officer": {
      "main": [
        [
          {
            "node": "Respond 200",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": []
}
Enter fullscreen mode Exit fullscreen mode

Setup: Import JSON. Note the webhook path phi-breach — your SIEM or breach detection system posts to https://your-n8n/webhook/phi-breach. Set PRIVACY_OFFICER_EMAIL. Configure Gmail and Slack. Create breach_incident_log sheet.


Workflow 5: Weekly HIT Platform Compliance & KPI Dashboard

Trigger: Monday at 8 AM
Audience: CEO + CISO BCC

Reads platform metrics and breach log from Google Sheets. Builds a weekly KPI report: MRR with WoW%, active customers, FHIR API uptime (with 99.5% SLA flag), FHIR query volume, information blocking incident count, open HIPAA breaches, and USCDI v4 completeness score. Color-coded risk status for each compliance metric. Sends HTML email to CEO with CISO BCC, and posts a one-liner to #leadership Slack.

Why it matters: Most HIT executive teams track product KPIs weekly but review compliance metrics only at audit time. That gap is where ONC information blocking violations and HIPAA breach deadline misses happen. This workflow puts FHIR uptime and information blocking incidents on the same weekly dashboard as MRR — making compliance a business metric, not a legal afterthought.

{
  "name": "Weekly HIT Platform Compliance & KPI Dashboard",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "id": "w5n01",
      "name": "Monday 8 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": "={{ $vars.SHEETS_DOC_ID }}",
        "sheetName": "platform_metrics",
        "options": {}
      },
      "id": "w5n02",
      "name": "Read Platform Metrics",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": "={{ $vars.SHEETS_DOC_ID }}",
        "sheetName": "breach_incident_log",
        "options": {}
      },
      "id": "w5n03",
      "name": "Read Breach Log",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        460,
        500
      ]
    },
    {
      "parameters": {
        "mode": "combine",
        "combinationMode": "mergeByPosition",
        "options": {}
      },
      "id": "w5n04",
      "name": "Merge Data",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        680,
        400
      ]
    },
    {
      "parameters": {
        "jsCode": "// Weekly HIT Platform KPI Dashboard\nconst metrics = $input.first().json;\nconst now = new Date();\nconst fhirUptimePct = parseFloat(metrics.fhir_uptime_7d || 99.9);\nconst prevUptimePct = parseFloat(metrics.fhir_uptime_prev_7d || 99.9);\nconst fhirQueries = parseInt(metrics.fhir_queries_7d || 0);\nconst prevQueries = parseInt(metrics.fhir_queries_prev_7d || 1);\nconst activeCustomers = parseInt(metrics.active_customers || 0);\nconst newCustomers = parseInt(metrics.new_customers_7d || 0);\nconst mrr = parseFloat(metrics.mrr_usd || 0);\nconst prevMrr = parseFloat(metrics.prev_mrr_usd || 1);\nconst infoBlockingIncidents = parseInt(metrics.info_blocking_incidents_7d || 0);\nconst openBreaches = parseInt(metrics.open_breaches || 0);\nconst uscdiScore = parseFloat(metrics.uscdi_completeness_pct || 100);\nconst mrrWoW = ((mrr - prevMrr) / prevMrr * 100).toFixed(1);\nconst uptimeWoW = (fhirUptimePct - prevUptimePct).toFixed(2);\nconst queryWoW = ((fhirQueries - prevQueries) / prevQueries * 100).toFixed(1);\n// Risk flags\nconst fhirRisk = fhirUptimePct < 99.5 ? '\ud83d\udd34 BELOW SLA' : '\ud83d\udfe2 OK';\nconst blockingRisk = infoBlockingIncidents > 0 ? '\ud83d\udd34 ' + infoBlockingIncidents + ' INCIDENTS' : '\ud83d\udfe2 CLEAR';\nconst breachRisk = openBreaches > 0 ? '\ud83d\udd34 ' + openBreaches + ' OPEN' : '\ud83d\udfe2 CLEAR';\nconst uscdiRisk = uscdiScore < 95 ? '\ud83d\udfe1 ' + uscdiScore + '%' : '\ud83d\udfe2 ' + uscdiScore + '%';\nreturn [{ mrr, mrrWoW, activeCustomers, newCustomers, fhirUptimePct, uptimeWoW,\n  fhirQueries, queryWoW, infoBlockingIncidents, openBreaches, uscdiScore,\n  fhirRisk, blockingRisk, breachRisk, uscdiRisk,\n  reportDate: now.toISOString().split('T')[0] }];"
      },
      "id": "w5n05",
      "name": "Build KPI Report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        400
      ]
    },
    {
      "parameters": {
        "sendTo": "={{ $vars.CEO_EMAIL }}",
        "subject": "=Weekly HIT Platform Report \u2014 {{ $json.reportDate }}",
        "emailType": "html",
        "message": "=<h2>HIT Platform Weekly Compliance & KPI Report</h2><p>Week ending {{ $json.reportDate }}</p><table border='1' cellpadding='6' style='border-collapse:collapse'><tr><th>Metric</th><th>Value</th><th>WoW</th><th>Status</th></tr><tr><td>MRR</td><td>${{ $json.mrr }}</td><td>{{ $json.mrrWoW }}%</td><td>-</td></tr><tr><td>Active Customers</td><td>{{ $json.activeCustomers }}</td><td>+{{ $json.newCustomers }} new</td><td>-</td></tr><tr><td>FHIR API Uptime</td><td>{{ $json.fhirUptimePct }}%</td><td>{{ $json.uptimeWoW }}pp</td><td>{{ $json.fhirRisk }}</td></tr><tr><td>FHIR Queries</td><td>{{ $json.fhirQueries }}</td><td>{{ $json.queryWoW }}%</td><td>-</td></tr><tr><td>Info Blocking Incidents</td><td>{{ $json.infoBlockingIncidents }}</td><td>-</td><td>{{ $json.blockingRisk }}</td></tr><tr><td>Open HIPAA Breaches</td><td>{{ $json.openBreaches }}</td><td>-</td><td>{{ $json.breachRisk }}</td></tr><tr><td>USCDI v4 Completeness</td><td>{{ $json.uscdiScore }}%</td><td>-</td><td>{{ $json.uscdiRisk }}</td></tr></table><p><small>ONC 21st Century Cures Act \u00a74004 | 45 CFR \u00a7170.315(g)(10) | 45 CFR \u00a7164.408 HIPAA Breach</small></p>",
        "options": {
          "ccList": "={{ $vars.CISO_EMAIL }}"
        }
      },
      "id": "w5n06",
      "name": "Email Weekly Report",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1120,
        400
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "channel": "#leadership",
        "text": "=:bar_chart: *HIT Platform Weekly Report \u2014 {{ $json.reportDate }}*\nMRR: ${{ $json.mrr }} ({{ $json.mrrWoW }}% WoW) | Customers: {{ $json.activeCustomers }}\nFHIR Uptime: {{ $json.fhirUptimePct }}% {{ $json.fhirRisk }}\nInfo Blocking: {{ $json.blockingRisk }} | HIPAA Breaches: {{ $json.breachRisk }}\nUSCDI v4: {{ $json.uscdiRisk }}",
        "otherOptions": {}
      },
      "id": "w5n07",
      "name": "Slack #leadership",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1120,
        560
      ]
    }
  ],
  "connections": {
    "Monday 8 AM": {
      "main": [
        [
          {
            "node": "Read Platform Metrics",
            "type": "main",
            "index": 0
          },
          {
            "node": "Read Breach Log",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Read Platform Metrics": {
      "main": [
        [
          {
            "node": "Merge Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Breach Log": {
      "main": [
        [
          {
            "node": "Merge Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Data": {
      "main": [
        [
          {
            "node": "Build KPI Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build KPI Report": {
      "main": [
        [
          {
            "node": "Email Weekly Report",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack #leadership",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": []
}
Enter fullscreen mode Exit fullscreen mode

Setup: Import JSON. Populate platform_metrics Google Sheet weekly with: fhir_uptime_7d, fhir_uptime_prev_7d, fhir_queries_7d, fhir_queries_prev_7d, active_customers, new_customers_7d, mrr_usd, prev_mrr_usd, info_blocking_incidents_7d, open_breaches, uscdi_completeness_pct. Set CEO_EMAIL and CISO_EMAIL. Configure Gmail and Slack.


n8n vs Zapier/Make for HIT SaaS Vendors

Factor n8n (self-hosted) Zapier / Make.com
PHI Data Flow Stays in your VPC Routes through vendor cloud
HIPAA BAA Not required (no third-party) Required — vendor must sign
ONC Info Blocking Risk FHIR data stays in-environment Third-party intermediary = access concern
Audit Trail Git-versioned JSON = ONC evidence Vendor-controlled logs
FHIR R4 Support Native HTTP + custom JSON Limited — no SMART auth support
HL7 v2 Parsing Code node (custom parser) Unsupported
Cost $0 (self-hosted) $20–$800+/month
ONC Certification Impact No third-party dependency Potential ONC attestation gap

Get the Templates

All five workflows are part of the FlowKit n8n Template Pack — ready-to-import JSON files at stripeai.gumroad.com. Each template includes the workflow JSON, a Google Sheets setup guide, and environment variable reference.

Pro Tips:

  • Replace Google Sheets with Postgres for production-scale deadline tracking (n8n's Postgres node is a drop-in swap)
  • Chain Workflow 4 (breach response) to Workflow 3 (deadline tracker) — when a breach fires, automatically create the 60-day HHS deadline entry
  • For TEFCA QHIN members: add a TEFCA-specific exception to Workflow 1 for the TEFCA_PERMITTED_PURPOSE carve-out
  • For RPM vendors: Workflow 2's FHIR check should also validate CMS billing code endpoint availability (separate from ONC FHIR certification)
  • Self-host n8n on your HIPAA-eligible infrastructure (AWS with BAA, Azure Government, GCP HIPAA-eligible) — not on a shared cloud host

Top comments (0)