DEV Community

Alex Kane
Alex Kane

Posted on

n8n for MedTech & DeviceTech SaaS: 5 Automations That Accelerate FDA Submissions and Keep Device Data Compliant (Free Workflow JSON)

If you build MedTech or DeviceTech SaaS — FDA 510(k)/PMA clearance tools, MDR/IVDR compliance platforms, LIMS for device labs, post-market surveillance software, QMS platforms — your ops team is drowning in regulatory deadlines, adverse event timelines, and device data pipelines.

Zapier and Make.com can automate simple tasks. But they can't handle FDA 21 CFR Part 820 QMSR audit trail requirements, EU MDR vigilance reporting SLAs, or 21 CFR Part 11 electronic records — and they route your clients' device data through third-party cloud infrastructure, which creates regulatory exposure your buyers will flag in every enterprise security review.

n8n self-hosted gives MedTech SaaS teams:

  • Zero data egress — device data, adverse event reports, MAUDE submissions, trial records stay in your VPC
  • Permanent audit trail — git-versioned workflow JSON + Postgres logs satisfy FDA 21 CFR Part 11.10(e) record retention requirements; Zapier deletes logs after 30 days
  • SLA enforcement — EU MDR Art.87 vigilance reporting: 2 days for life loss, 15 days for serious incidents; 21 CFR Part 803: 5-day expedited MDR; n8n enforces these as hard workflow branches, not manual reminders
  • Cost — 500K tasks/month = Zapier Business $800+/mo vs a $30 VPS running n8n

Here are 5 production workflows for MedTech and DeviceTech SaaS vendors, with import-ready JSON.


The self-hosting case for MedTech SaaS

Factor n8n self-hosted Zapier Make.com
Device data routing Your VPC only Zapier cloud Make cloud
Audit trail retention Permanent (Postgres) 30 days 30 days
FDA 21 CFR Part 11 Configurable validation No GxP validation pack No GxP validation pack
QMSR record control Self-managed, git-versioned Vendor-controlled Vendor-controlled
EU MDR Art.28 sub-processor None (self-hosted) Zapier Inc. Celonis SE
Cost at 500K tasks/mo ~$30/mo VPS ~$800/mo ~$350/mo

When a TIER 1 device OEM's procurement team runs a vendor security review and sees 'your n8n automation SaaS routes device data through Zapier cloud,' that's a blocker. Self-hosted n8n removes it.


Workflow 1: New MedDevice Client Onboarding Drip

What it does: Detects new clients in a Google Sheet, routes them into a tier-specific 3-touch onboarding sequence, and logs every touchpoint to an audit trail.

Trigger: Google Sheets row added (new client signup)

Tiers:

  • TIER_1_GLOBAL_OEM — Medtronic, Stryker, J&J Device: Enterprise onboarding pack, dedicated CSM intro
  • TIER_2_SMB_MANUFACTURER — mid-size device manufacturers: standard onboarding + weekly check-in
  • TIER_3_STARTUP_INNOVATOR — pre-market startups: lighter touch, resource-heavy
  • TIER_4_ACADEMIC_RESEARCH — university labs: compliance-forward, budget-conscious
{
  "name": "MedTech Client Onboarding Drip",
  "nodes": [
    {
      "parameters": {
        "pollTimes": {"item": [{"mode": "everyMinute"}]},
        "sheetId": {"__rl": true, "value": "YOUR_SHEET_ID", "mode": "id"},
        "sheetName": {"__rl": true, "value": "Clients", "mode": "name"}
      },
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "name": "New Client Trigger",
      "position": [240, 300]
    },
    {
      "parameters": {
        "jsCode": "const tier = $json.company_type?.toUpperCase() || 'TIER_2_SMB_MANUFACTURER';\nconst tierMap = {\n  'TIER_1_GLOBAL_OEM': {label: 'Global OEM', subject: 'Your enterprise onboarding starts now', csmSlack: '#csm-enterprise'},\n  'TIER_2_SMB_MANUFACTURER': {label: 'SMB Manufacturer', subject: 'Welcome to the platform', csmSlack: '#csm-smb'},\n  'TIER_3_STARTUP_INNOVATOR': {label: 'Startup', subject: 'Let's get your device to market faster', csmSlack: '#csm-startup'},\n  'TIER_4_ACADEMIC_RESEARCH': {label: 'Academic', subject: 'Your research automation is ready', csmSlack: '#csm-academic'}\n};\nconst config = tierMap[tier] || tierMap['TIER_2_SMB_MANUFACTURER'];\nreturn [{json: {...$json, tier, ...config, onboardedAt: new Date().toISOString()}}];"
      },
      "type": "n8n-nodes-base.code",
      "name": "Classify Tier",
      "position": [460, 300]
    },
    {
      "parameters": {
        "sendTo": "={{ $json.contact_email }}",
        "subject": "={{ $json.subject }}",
        "message": "Hi {{ $json.contact_name }},\n\nWelcome! Your {{ $json.tier }} onboarding pack is attached.\n\nYour CSM will reach out within 1 business day.\n\nFlowKit Automation Team"
      },
      "type": "n8n-nodes-base.gmail",
      "name": "Day 0 Welcome Email",
      "position": [680, 300]
    },
    {
      "parameters": {"amount": 3, "unit": "days"},
      "type": "n8n-nodes-base.wait",
      "name": "Wait 3 Days",
      "position": [900, 300]
    },
    {
      "parameters": {
        "sendTo": "={{ $json.contact_email }}",
        "subject": "Day 3 check-in: any questions on your setup?",
        "message": "Hi {{ $json.contact_name }},\n\nHow is the setup going? Reply with any questions — we typically respond in 2 hours.\n\nFlowKit Team"
      },
      "type": "n8n-nodes-base.gmail",
      "name": "Day 3 Check-in",
      "position": [1120, 300]
    },
    {
      "parameters": {
        "operation": "append",
        "sheetId": {"__rl": true, "value": "YOUR_AUDIT_SHEET_ID", "mode": "id"},
        "sheetName": {"__rl": true, "value": "OnboardingAuditLog", "mode": "name"},
        "columns": {"mappingMode": "autoMapInputData"}
      },
      "type": "n8n-nodes-base.googleSheets",
      "name": "QMSR Audit Log",
      "position": [1340, 300]
    }
  ],
  "connections": {
    "New Client Trigger": {"main": [[{"node": "Classify Tier", "type": "main", "index": 0}]]},
    "Classify Tier": {"main": [[{"node": "Day 0 Welcome Email", "type": "main", "index": 0}]]},
    "Day 0 Welcome Email": {"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": "QMSR Audit Log", "type": "main", "index": 0}]]}
  },
  "active": false,
  "settings": {"executionOrder": "v1"}
}
Enter fullscreen mode Exit fullscreen mode

Why it matters: Every onboarding touchpoint is logged with timestamp, tier, and email content — satisfying ISO 13485 §4.2.4 quality record requirements without any manual effort.


Workflow 2: FDA 510(k)/PMA & MDR/IVDR Regulatory Deadline Tracker

What it does: Runs every weekday morning, checks a Sheets-based regulatory calendar, and routes escalating alerts to Slack and email based on days remaining.

Trigger: Schedule (weekdays 8 AM)

Deadlines tracked:

  • FDA_510K_SUBMISSION — premarket notification due dates
  • PMA_ANNUAL_REPORT — post-approval study annual reports
  • MDR_ADVERSE_EVENT_REPORT — 21 CFR Part 803 (5-day/30-day)
  • EU_MDR_VIGILANCE_REPORT — Art.87: 2-day/15-day/30-day timelines
  • ISO_13485_SURVEILLANCE_AUDIT — third-party QMS audit schedule
  • UDI_DATABASE_UPDATE — GUDID submission deadlines
  • QMSR_ANNUAL_REVIEW — 21 CFR 820.22 quality audit
  • SOC2_EVIDENCE_COLLECTION — for enterprise SaaS buyers
{
  "name": "FDA/MDR Regulatory Deadline Tracker",
  "nodes": [
    {
      "parameters": {"rule": {"interval": [{"field": "cronExpression", "expression": "0 8 * * 1-5"}]}},
      "type": "n8n-nodes-base.scheduleTrigger",
      "name": "Weekday 8AM",
      "position": [240, 300]
    },
    {
      "parameters": {
        "operation": "readRows",
        "sheetId": {"__rl": true, "value": "YOUR_REGULATORY_SHEET_ID", "mode": "id"},
        "sheetName": {"__rl": true, "value": "RegulatoryCalendar", "mode": "name"}
      },
      "type": "n8n-nodes-base.googleSheets",
      "name": "Read Regulatory Calendar",
      "position": [460, 300]
    },
    {
      "parameters": {
        "jsCode": "const today = new Date(); const items = [];\nfor (const row of $input.all()) {\n  const d = row.json;\n  const due = new Date(d.due_date);\n  const daysLeft = Math.ceil((due - today) / 86400000);\n  let urgency = null;\n  if (daysLeft < 0) urgency = 'OVERDUE';\n  else if (daysLeft <= 2) urgency = 'CRITICAL';\n  else if (daysLeft <= 7) urgency = 'URGENT';\n  else if (daysLeft <= 14) urgency = 'WARNING';\n  else if (daysLeft <= 30) urgency = 'NOTICE';\n  if (urgency) items.push({json: {...d, daysLeft, urgency}});\n}\nreturn items;"
      },
      "type": "n8n-nodes-base.code",
      "name": "Classify Urgency",
      "position": [680, 300]
    },
    {
      "parameters": {
        "rules": {"values": [
          {"conditions": {"options": {"leftValue": "={{ $json.urgency }}", "operation": "equals", "rightValue": "OVERDUE"}}, "renameOutput": true, "outputKey": "overdue"},
          {"conditions": {"options": {"leftValue": "={{ $json.urgency }}", "operation": "equals", "rightValue": "CRITICAL"}}, "renameOutput": true, "outputKey": "critical"}
        ]}
      },
      "type": "n8n-nodes-base.switch",
      "name": "Route by Urgency",
      "position": [900, 300]
    },
    {
      "parameters": {
        "select": "channel", "channelId": {"__rl": true, "value": "#regulatory-emergency", "mode": "name"},
        "text": ":rotating_light: OVERDUE: {{ $json.deadline_type }} was due {{ Math.abs($json.daysLeft) }} day(s) ago. Owner: {{ $json.owner }}. Action required NOW."
      },
      "type": "n8n-nodes-base.slack",
      "name": "Slack OVERDUE Alert",
      "position": [1120, 200]
    },
    {
      "parameters": {
        "sendTo": "={{ $json.owner_email }}",
        "subject": "OVERDUE: {{ $json.deadline_type }} — Action Required",
        "message": "This regulatory deadline is OVERDUE by {{ Math.abs($json.daysLeft) }} day(s).\n\nDeadline: {{ $json.deadline_type }}\nDue date: {{ $json.due_date }}\nRegulation: {{ $json.regulation }}\n\nImmediate action required."
      },
      "type": "n8n-nodes-base.gmail",
      "name": "Email Owner OVERDUE",
      "position": [1340, 200]
    }
  ],
  "connections": {
    "Weekday 8AM": {"main": [[{"node": "Read Regulatory Calendar", "type": "main", "index": 0}]]},
    "Read Regulatory Calendar": {"main": [[{"node": "Classify Urgency", "type": "main", "index": 0}]]},
    "Classify Urgency": {"main": [[{"node": "Route by Urgency", "type": "main", "index": 0}]]},
    "Route by Urgency": {"overdue": [{"node": "Slack OVERDUE Alert", "type": "main", "index": 0}], "critical": [{"node": "Slack OVERDUE Alert", "type": "main", "index": 0}]},
    "Slack OVERDUE Alert": {"main": [[{"node": "Email Owner OVERDUE", "type": "main", "index": 0}]]}
  },
  "active": false,
  "settings": {"executionOrder": "v1"}
}
Enter fullscreen mode Exit fullscreen mode

Regulatory note: FDA 21 CFR Part 803.17 requires MDR reporting procedures with documented timelines. A missed MDR = FDA warning letter. This workflow makes it impossible to miss — every deadline has an automated escalation path.


Workflow 3: Adverse Event & Medical Device Reporting (MDR) Alert Pipeline

What it does: Receives adverse event webhooks (from your platform's complaint form or device monitoring system), classifies them by FDA/EU MDR reportability, routes to regulatory ops, and logs to a MAUDE-ready audit trail.

Trigger: Webhook (POST from your complaint management system or IoT device health API)

Severity classifications:

  • FDA_MDR_REPORTABLE_5DAY — malfunction likely to cause serious injury if recurs (21 CFR 803.50)
  • FDA_MDR_REPORTABLE_30DAY — malfunction, no immediate serious risk
  • EU_MDR_VIGILANCE_IMMEDIATE — death or unanticipated serious deterioration (Art.87: 2-day window)
  • EU_MDR_VIGILANCE_15DAY — serious incident (Art.87: 15-day window)
  • NON_REPORTABLE_WATCH — track internally, does not meet reportability threshold
{
  "name": "Adverse Event & MDR Alert Pipeline",
  "nodes": [
    {
      "parameters": {"httpMethod": "POST", "path": "adverse-event", "responseMode": "responseNode"},
      "type": "n8n-nodes-base.webhook",
      "name": "Adverse Event Webhook",
      "position": [240, 300]
    },
    {
      "parameters": {
        "jsCode": "const e = $json.body || $json;\nconst severity = (e.severity || '').toUpperCase();\nconst death = e.patient_outcome === 'DEATH';\nconst seriousInjury = ['SERIOUS_INJURY', 'HOSPITALIZATION', 'LIFE_THREATENING'].includes(e.patient_outcome);\nconst malfunction = e.event_type === 'MALFUNCTION';\nlet classification;\nif (death || (seriousInjury && e.market === 'EU')) classification = 'EU_MDR_VIGILANCE_IMMEDIATE';\nelse if (seriousInjury && e.market === 'EU') classification = 'EU_MDR_VIGILANCE_15DAY';\nelse if (death || seriousInjury) classification = 'FDA_MDR_REPORTABLE_5DAY';\nelse if (malfunction) classification = 'FDA_MDR_REPORTABLE_30DAY';\nelse classification = 'NON_REPORTABLE_WATCH';\nconst reportDeadline = {\n  'EU_MDR_VIGILANCE_IMMEDIATE': '2 days',\n  'EU_MDR_VIGILANCE_15DAY': '15 days',\n  'FDA_MDR_REPORTABLE_5DAY': '5 days',\n  'FDA_MDR_REPORTABLE_30DAY': '30 days',\n  'NON_REPORTABLE_WATCH': 'N/A'\n}[classification];\nreturn [{json: {...e, classification, reportDeadline, receivedAt: new Date().toISOString()}}];"
      },
      "type": "n8n-nodes-base.code",
      "name": "Classify Reportability",
      "position": [460, 300]
    },
    {
      "parameters": {
        "conditions": {"options": [{"leftValue": "={{ $json.classification }}", "operation": "contains", "rightValue": "REPORTABLE"}]}
      },
      "type": "n8n-nodes-base.if",
      "name": "Is Reportable?",
      "position": [680, 300]
    },
    {
      "parameters": {
        "select": "channel", "channelId": {"__rl": true, "value": "#regulatory-emergency", "mode": "name"},
        "text": ":rotating_light: MDR ALERT: {{ $json.classification }}\nDevice: {{ $json.device_model }} | Patient outcome: {{ $json.patient_outcome }}\nReport deadline: {{ $json.reportDeadline }} | Event ID: {{ $json.event_id }}"
      },
      "type": "n8n-nodes-base.slack",
      "name": "Slack Regulatory Emergency",
      "position": [900, 200]
    },
    {
      "parameters": {
        "operation": "insert",
        "schema": {"__rl": true, "value": "public", "mode": "name"},
        "table": {"__rl": true, "value": "maude_audit_log", "mode": "name"},
        "columns": {"mappingMode": "autoMapInputData"}
      },
      "type": "n8n-nodes-base.postgres",
      "name": "MAUDE Audit Log",
      "position": [1120, 300]
    },
    {
      "parameters": {"respondWith": "json", "responseBody": "={{ JSON.stringify({received: true, classification: $json.classification, deadline: $json.reportDeadline}) }}"},
      "type": "n8n-nodes-base.respondToWebhook",
      "name": "ACK 200",
      "position": [1340, 300]
    }
  ],
  "connections": {
    "Adverse Event Webhook": {"main": [[{"node": "Classify Reportability", "type": "main", "index": 0}]]},
    "Classify Reportability": {"main": [[{"node": "Is Reportable?", "type": "main", "index": 0}]]},
    "Is Reportable?": {"main": [
      [{"node": "Slack Regulatory Emergency", "type": "main", "index": 0}],
      [{"node": "MAUDE Audit Log", "type": "main", "index": 0}]
    ]},
    "Slack Regulatory Emergency": {"main": [[{"node": "MAUDE Audit Log", "type": "main", "index": 0}]]},
    "MAUDE Audit Log": {"main": [[{"node": "ACK 200", "type": "main", "index": 0}]]}
  },
  "active": false,
  "settings": {"executionOrder": "v1"}
}
Enter fullscreen mode Exit fullscreen mode

Why this matters: FDA Warning Letters for late MDR reporting are public record and destroy device company relationships. Automation removes human memory from a legally mandated process.


Workflow 4: Device Clinical Trial & Post-Market Surveillance Monitor

What it does: Runs daily, queries a Postgres database of device clinical trial sites and PMS complaint data, flags RED/AMBER sites based on protocol deviations and enrollment shortfalls, and alerts clinical ops.

Trigger: Schedule (weekdays 9 AM)

Required Postgres tables:

CREATE TABLE trial_sites (
  site_id TEXT PRIMARY KEY,
  site_name TEXT,
  pi_email TEXT,
  target_enrollment INT,
  actual_enrollment INT,
  open_deviations INT,
  critical_deviations INT,
  last_updated TIMESTAMPTZ
);

CREATE TABLE pms_complaints (
  complaint_id TEXT PRIMARY KEY,
  device_model TEXT,
  complaint_type TEXT,
  week_of DATE,
  complaint_count INT
);
Enter fullscreen mode Exit fullscreen mode
{
  "name": "Device Trial & PMS Monitor",
  "nodes": [
    {
      "parameters": {"rule": {"interval": [{"field": "cronExpression", "expression": "0 9 * * 1-5"}]}},
      "type": "n8n-nodes-base.scheduleTrigger",
      "name": "Weekday 9AM",
      "position": [240, 300]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT site_id, site_name, pi_email, target_enrollment, actual_enrollment, open_deviations, critical_deviations, ROUND(actual_enrollment::numeric/NULLIF(target_enrollment,0)*100,1) as enrollment_pct FROM trial_sites WHERE last_updated > NOW() - INTERVAL '7 days'"
      },
      "type": "n8n-nodes-base.postgres",
      "name": "Query Trial Sites",
      "position": [460, 300]
    },
    {
      "parameters": {
        "jsCode": "return $input.all().map(item => {\n  const d = item.json;\n  let risk = 'GREEN';\n  if (d.critical_deviations >= 2 || d.enrollment_pct < 60) risk = 'RED';\n  else if (d.critical_deviations >= 1 || d.open_deviations >= 5 || d.enrollment_pct < 80) risk = 'AMBER';\n  return {json: {...d, risk}};\n});"
      },
      "type": "n8n-nodes-base.code",
      "name": "Flag Risk Level",
      "position": [680, 300]
    },
    {
      "parameters": {
        "conditions": {"options": [{"leftValue": "={{ $json.risk }}", "operation": "equals", "rightValue": "RED"}]}
      },
      "type": "n8n-nodes-base.if",
      "name": "RED Sites?",
      "position": [900, 300]
    },
    {
      "parameters": {
        "select": "channel", "channelId": {"__rl": true, "value": "#clinical-ops", "mode": "name"},
        "text": ":red_circle: RED SITE: {{ $json.site_name }}\nEnrollment: {{ $json.enrollment_pct }}% | Critical deviations: {{ $json.critical_deviations }}\nPI: {{ $json.pi_email }}"
      },
      "type": "n8n-nodes-base.slack",
      "name": "Alert Clinical Ops",
      "position": [1120, 200]
    }
  ],
  "connections": {
    "Weekday 9AM": {"main": [[{"node": "Query Trial Sites", "type": "main", "index": 0}]]},
    "Query Trial Sites": {"main": [[{"node": "Flag Risk Level", "type": "main", "index": 0}]]},
    "Flag Risk Level": {"main": [[{"node": "RED Sites?", "type": "main", "index": 0}]]},
    "RED Sites?": {"main": [[{"node": "Alert Clinical Ops", "type": "main", "index": 0}], []]}
  },
  "active": false,
  "settings": {"executionOrder": "v1"}
}
Enter fullscreen mode Exit fullscreen mode

Extension: Add a second Postgres query on PMS complaint rate per device model and flag any model where complaint rate increased >0.5% WoW — that's an EU MDR Art.83 PMCF signal that requires documented review.


Workflow 5: Weekly MedTech Platform KPI Dashboard

What it does: Every Monday at 8 AM, pulls platform metrics and account health data in parallel from Postgres, builds a color-coded HTML report, and emails it to leadership with a Slack summary.

Trigger: Schedule (Monday 8 AM)

{
  "name": "Weekly MedTech KPI Dashboard",
  "nodes": [
    {
      "parameters": {"rule": {"interval": [{"field": "cronExpression", "expression": "0 8 * * 1"}]}},
      "type": "n8n-nodes-base.scheduleTrigger",
      "name": "Monday 8AM",
      "position": [240, 300]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(DISTINCT client_id) as active_clients, COUNT(DISTINCT CASE WHEN created_at > NOW()-INTERVAL'7d' THEN client_id END) as new_this_week, SUM(mrr_usd) as total_mrr, AVG(api_calls_7d) as avg_api_calls FROM platform_metrics WHERE status='active'"
      },
      "type": "n8n-nodes-base.postgres",
      "name": "Platform Metrics",
      "position": [460, 200]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(*) FILTER (WHERE health_score < 40) as at_risk, COUNT(*) FILTER (WHERE health_score >= 40 AND health_score < 70) as needs_attention, COUNT(*) FILTER (WHERE health_score >= 70) as healthy FROM account_health"
      },
      "type": "n8n-nodes-base.postgres",
      "name": "Account Health",
      "position": [460, 400]
    },
    {
      "parameters": {"mode": "passThrough", "output": "all"},
      "type": "n8n-nodes-base.merge",
      "name": "Merge Data",
      "position": [680, 300]
    },
    {
      "parameters": {
        "jsCode": "const prev = $getWorkflowStaticData('global');\nconst platform = $input.first().json;\nconst health = $input.last().json;\nconst mrrWoW = prev.mrr ? ((platform.total_mrr - prev.mrr)/prev.mrr*100).toFixed(1) : 'N/A';\nprev.mrr = platform.total_mrr;\n$setWorkflowStaticData('global', prev);\nconst html = [\n  '<h2>MedTech Platform KPI — Week of ' + new Date().toDateString() + '</h2>',\n  '<table border=1 cellpadding=6>',\n  '<tr><th>Metric</th><th>Value</th><th>WoW</th></tr>',\n  `<tr><td>Active Clients</td><td>${platform.active_clients}</td><td>—</td></tr>`,\n  `<tr><td>New This Week</td><td>${platform.new_this_week}</td><td>—</td></tr>`,\n  `<tr><td>Total MRR</td><td>$${Number(platform.total_mrr).toLocaleString()}</td><td>${mrrWoW}%</td></tr>`,\n  `<tr style='background:#ffe0e0'><td>At-Risk Accounts</td><td>${health.at_risk}</td><td>—</td></tr>`,\n  `<tr style='background:#fff3cd'><td>Needs Attention</td><td>${health.needs_attention}</td><td>—</td></tr>`,\n  `<tr style='background:#d4edda'><td>Healthy Accounts</td><td>${health.healthy}</td><td>—</td></tr>`,\n  '</table>'\n].join('\\n');\nreturn [{json: {html, platform, health, mrrWoW}}];"
      },
      "type": "n8n-nodes-base.code",
      "name": "Build KPI Report",
      "position": [900, 300]
    },
    {
      "parameters": {
        "sendTo": "ceo@yourcompany.com",
        "bcc": "cto@yourcompany.com,vp-regulatory@yourcompany.com",
        "subject": "Weekly MedTech Platform KPI — {{ $now.format('yyyy-MM-dd') }}",
        "message": "={{ $json.html }}",
        "options": {"appendAttribution": false}
      },
      "type": "n8n-nodes-base.gmail",
      "name": "Email Leadership",
      "position": [1120, 300]
    },
    {
      "parameters": {
        "select": "channel", "channelId": {"__rl": true, "value": "#exec-kpis", "mode": "name"},
        "text": "MedTech Weekly: {{ $json.platform.active_clients }} clients | MRR ${{ $json.platform.total_mrr }} ({{ $json.mrrWoW }}% WoW) | At-risk: {{ $json.health.at_risk }}"
      },
      "type": "n8n-nodes-base.slack",
      "name": "Slack KPI Summary",
      "position": [1340, 300]
    }
  ],
  "connections": {
    "Monday 8AM": {"main": [[{"node": "Platform Metrics", "type": "main", "index": 0}, {"node": "Account Health", "type": "main", "index": 0}]]},
    "Platform Metrics": {"main": [[{"node": "Merge Data", "type": "main", "index": 0}]]},
    "Account Health": {"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 Leadership", "type": "main", "index": 0}]]},
    "Email Leadership": {"main": [[{"node": "Slack KPI Summary", "type": "main", "index": 0}]]}
  },
  "active": false,
  "settings": {"executionOrder": "v1"}
}
Enter fullscreen mode Exit fullscreen mode

Extension: Add a third Postgres query pulling adverse event counts from your MAUDE audit log and include a regulatory risk section in the email — flagging any device model with MDR filings this week.


How to set up n8n for MedTech SaaS

# Self-hosted on your own VPS (DigitalOcean, AWS, GCP)
docker run -it --rm \
  -p 5678:5678 \
  -v ~/.n8n:/home/node/.n8n \
  n8nio/n8n

# Cloud option (no self-hosting)
# https://n8n.io/cloud
Enter fullscreen mode Exit fullscreen mode

Import any workflow above: Settings → Import from JSON → paste the JSON.


Ready-to-use automation templates for MedTech SaaS

If you want production-ready templates — pre-built, pre-tested, with setup guides — FlowKit has a full library at stripeai.gumroad.com.

The bundle covers all 5 workflows above plus 10 more for SaaS ops, CRM onboarding, compliance tracking, and KPI reporting — $97 one-time, no subscription.


Which of these workflows would save your team the most time? Drop a comment — I read every one.

Top comments (0)