DEV Community

Alex Kane
Alex Kane

Posted on

n8n for PharmaTech SaaS: 5 Automations for FDA 21 CFR Part 11, GxP Compliance, and Drug Safety Monitoring (Free JSON)

If you're building PharmaTech or biotech software, your customers operate under some of the most demanding compliance regimes on earth: FDA 21 CFR Part 11 (electronic records and signatures), GxP (GMP/GLP/GCP across manufacturing, labs, and clinical trials), and ICH Q10 (Pharmaceutical Quality System). One audit trail gap, one missed deadline, one data integrity finding — and your customer is facing a Warning Letter, a consent decree, or a clinical hold.

Here's the problem with Zapier and Make.com in this space:

  • 21 CFR §11.10(e) requires audit trails that cannot be modified or deleted. Zapier auto-deletes execution logs after 30 days. FDA inspections routinely look back 2–5 years. Logs are gone before the investigator opens their laptop.
  • GxP Computer System Validation (CSV): Zapier is a closed-source black box — writing a compliant IQ/OQ/PQ validation protocol for it is legally indefensible. Self-hosted n8n gives you source code, git-tagged releases, and a full change control audit trail.
  • EMA Annex 11 §7.1 requires audit trails to be available for the lifetime of the GxP computer system. Zapier's 30-day rolling window is non-compliant for EU GMP-regulated systems.
  • GDPR Article 44+: Batch record fragments, clinical data, and patient-adjacent information routed through Zapier may require Standard Contractual Clauses for US→EU transfers. Self-hosted n8n eliminates the transfer entirely.
  • ICH Q10 §3.2.3 CAPA: The Pharmaceutical Quality System requires documented evidence of corrective action effectiveness. Zapier's task history doesn't generate the audit evidence regulators expect.

n8n self-hosted changes the compliance calculus entirely. Here are 5 production-grade workflows every PharmaTech SaaS company should be running.


Workflow 1: PharmaTech Customer GxP Onboarding Drip

New customers come in as BIG_PHARMA, MID_MARKET_PHARMA, CDMO, CRO, or BIOTECH — each with different compliance flags active. This workflow routes them through the right onboarding cadence automatically.

What it does:

  • Classifies incoming customer by revenue tier and company type
  • Activates compliance flags: CFR_PART_11_APPLICABLE, GMP_REQUIRED, GCP_APPLICABLE, GLP_APPLICABLE, HIPAA_BAA_REQUIRED, GDPR_DPA_REQUIRED, ICH_Q10_ALIGNED
  • Sends tier-appropriate Day 0 welcome email
  • Fires CSM Slack alert with Part 11 scope flag for systems requiring CSV validation
  • Day 3: checks integration status — if LIMS/ERP not connected, sends reminder and escalates Part 11 scope accounts
  • Day 7: sends activation check-in with GMP/Part 11 status

Import-ready JSON:

{
  "name": "PharmaTech Customer GxP Onboarding Drip",
  "nodes": [
    {
      "id": "1",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        250,
        300
      ],
      "parameters": {
        "path": "pharma-customer-onboarding",
        "method": "POST",
        "responseMode": "responseNode"
      }
    },
    {
      "id": "2",
      "name": "Classify Customer Tier",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        470,
        300
      ],
      "parameters": {
        "jsCode": "const d=items[0].json;\nconst tier=d.annual_revenue_usd>500e6?'BIG_PHARMA':d.annual_revenue_usd>50e6?'MID_MARKET_PHARMA':d.company_type==='CDMO'?'CDMO':d.company_type==='CRO'?'CRO':'BIOTECH';\nconst flags=[];\nif(d.has_electronic_batch_records||d.lims_integration)flags.push('CFR_PART_11_APPLICABLE');\nif(d.manufactures_drug_product||d.cmo_client)flags.push('GMP_REQUIRED');\nif(d.runs_clinical_trials)flags.push('GCP_APPLICABLE');\nif(d.has_lab_operations)flags.push('GLP_APPLICABLE');\nif(d.processes_patient_data)flags.push('HIPAA_BAA_REQUIRED');\nif(d.eu_customers||d.eu_sites)flags.push('GDPR_DPA_REQUIRED');\nif(d.formal_pqs_deployed)flags.push('ICH_Q10_ALIGNED');\nreturn[{json:{...d,customer_tier:tier,compliance_flags:flags,onboarding_ts:new Date().toISOString()}}];"
      }
    },
    {
      "id": "3",
      "name": "Log to Postgres",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2,
      "position": [
        690,
        300
      ],
      "parameters": {
        "operation": "insert",
        "table": "pharma_customer_onboarding",
        "columns": "customer_id,company_name,customer_tier,compliance_flags,onboarding_ts",
        "values": "={{$json.customer_id}},={{$json.company_name}},={{$json.customer_tier}},={{JSON.stringify($json.compliance_flags)}},={{$json.onboarding_ts}}"
      }
    },
    {
      "id": "4",
      "name": "Day 0 Welcome Email",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2,
      "position": [
        910,
        200
      ],
      "parameters": {
        "operation": "send",
        "to": "={{$json.contact_email}}",
        "subject": "Welcome to FlowKit \u2014 Your GxP Automation Platform is Ready",
        "message": "={{$json.customer_tier==='BIG_PHARMA'?'Your dedicated CSM will contact you within 2 business hours.':$json.customer_tier==='CDMO'?'Your CDMO workflow templates are pre-configured.':'Your onboarding checklist is attached.'}} Compliance flags active: {{$json.compliance_flags.join(', ')}}"
      }
    },
    {
      "id": "5",
      "name": "Day 0 Slack CSM Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2,
      "position": [
        910,
        350
      ],
      "parameters": {
        "channel": "#csm-pharma-onboarding",
        "text": "New {{$json.customer_tier}} customer: *{{$json.company_name}}*. Flags: {{$json.compliance_flags.join(' | ')}}. {{$json.compliance_flags.includes('CFR_PART_11_APPLICABLE')?'\u26a0\ufe0f Part 11 scope \u2014 CSV validation required':''}}"
      }
    },
    {
      "id": "6",
      "name": "Wait 3 Days",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1,
      "position": [
        1130,
        300
      ],
      "parameters": {
        "amount": 3,
        "unit": "days"
      }
    },
    {
      "id": "7",
      "name": "Day 3 Integration Check",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2,
      "position": [
        1350,
        300
      ],
      "parameters": {
        "operation": "select",
        "table": "pharma_integration_status",
        "where": "customer_id = '{{$json.customer_id}}'"
      }
    },
    {
      "id": "8",
      "name": "Day 3 \u2014 Integration Not Confirmed?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1570,
        300
      ],
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{$json.integration_status}}",
              "operation": "notEqual",
              "value2": "CONNECTED"
            }
          ]
        }
      }
    },
    {
      "id": "9",
      "name": "Day 3 Reminder Email",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2,
      "position": [
        1790,
        200
      ],
      "parameters": {
        "operation": "send",
        "to": "={{$json.contact_email}}",
        "subject": "Your LIMS/ERP integration \u2014 5-minute setup",
        "message": "Hi {{$json.contact_name}}, your integration has not been confirmed. {{$json.compliance_flags.includes('CFR_PART_11_APPLICABLE')?'Part 11 audit trails require confirmed integration before go-live.':''}} Book a 15-min setup call: calendly.com/flowkit-pharma"
      }
    },
    {
      "id": "10",
      "name": "Day 3 CSM Escalation Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2,
      "position": [
        1790,
        380
      ],
      "parameters": {
        "channel": "#csm-pharma-onboarding",
        "text": "\u26a0\ufe0f *{{$json.company_name}}* Day 3 \u2014 no integration. {{$json.customer_tier==='BIG_PHARMA'||$json.customer_tier==='CDMO'?'PRIORITY: assign CSM now.':'Monitor.'}}"
      }
    },
    {
      "id": "11",
      "name": "Wait 4 More Days",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1,
      "position": [
        2010,
        300
      ],
      "parameters": {
        "amount": 4,
        "unit": "days"
      }
    },
    {
      "id": "12",
      "name": "Day 7 Activation Email",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2,
      "position": [
        2230,
        300
      ],
      "parameters": {
        "operation": "send",
        "to": "={{$json.contact_email}}",
        "subject": "Week 1 Check-In \u2014 Are your GxP workflows running?",
        "message": "Hi {{$json.contact_name}}, it has been 7 days. {{$json.compliance_flags.includes('GMP_REQUIRED')?'Your batch record automation should be live.':''}} {{$json.compliance_flags.includes('CFR_PART_11_APPLICABLE')?'Part 11 audit trail is active from Day 1.':''}} Anything blocking you? Reply to this email."
      }
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Classify Customer Tier",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Customer Tier": {
      "main": [
        [
          {
            "node": "Log to Postgres",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log to Postgres": {
      "main": [
        [
          {
            "node": "Day 0 Welcome Email",
            "type": "main",
            "index": 0
          },
          {
            "node": "Day 0 Slack CSM Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Day 0 Welcome Email": {
      "main": [
        [
          {
            "node": "Wait 3 Days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 3 Days": {
      "main": [
        [
          {
            "node": "Day 3 Integration Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Day 3 Integration Check": {
      "main": [
        [
          {
            "node": "Day 3 \u2014 Integration Not Confirmed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Day 3 \u2014 Integration Not Confirmed?": {
      "main": [
        [
          {
            "node": "Day 3 Reminder Email",
            "type": "main",
            "index": 0
          },
          {
            "node": "Day 3 CSM Escalation Slack",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait 4 More Days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Day 3 Reminder Email": {
      "main": [
        [
          {
            "node": "Wait 4 More Days",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 4 More Days": {
      "main": [
        [
          {
            "node": "Day 7 Activation Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 2: FDA 21 CFR Part 11 Audit Trail & System Validation Monitor

Runs every 15 minutes. Polls your Part 11 system registry, checks audit trail continuity, validation status, and e-signature binding. One gap → immediate Slack alert.

What it monitors:

  • AUDIT_TRAIL_DISABLED → CRITICAL (§11.10(e) violation in progress)
  • Audit trail stale >60min → CRITICAL (§11.10(e) gap — document immediately)
  • VALIDATION_EXPIRED → CRITICAL (Part 11 use of system blocked until revalidation)
  • ERES_SIGNATURE_BINDING_FAILURE → CRITICAL (§11.50 violation)
  • Open deviations >5 → HIGH (ICH Q10 quality system alert)
{
  "name": "FDA 21 CFR Part 11 Audit Trail Monitor",
  "nodes": [
    {
      "id": "1",
      "name": "Schedule 15min",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        250,
        300
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      }
    },
    {
      "id": "2",
      "name": "Fetch Audit Trail Status",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2,
      "position": [
        470,
        300
      ],
      "parameters": {
        "operation": "select",
        "query": "SELECT system_name, last_audit_entry_ts, audit_trail_enabled, validation_status, eres_status, open_deviations FROM part11_system_registry WHERE active = true"
      }
    },
    {
      "id": "3",
      "name": "Evaluate Compliance",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        690,
        300
      ],
      "parameters": {
        "jsCode": "const alerts=[];\nconst now=Date.now();\nfor(const s of items){\n  const d=s.json;\n  const minsSince=(now-new Date(d.last_audit_entry_ts).getTime())/60000;\n  if(!d.audit_trail_enabled){alerts.push({...d,alert_type:'AUDIT_TRAIL_DISABLED',severity:'CRITICAL',message:`CRITICAL: ${d.system_name} audit trail disabled \u2014 21 CFR \u00a711.10(e) violation`});}\n  else if(minsSince>60){alerts.push({...d,alert_type:'AUDIT_TRAIL_STALE',severity:'CRITICAL',message:`CRITICAL: ${d.system_name} no audit entries for ${Math.round(minsSince)}min \u2014 potential \u00a711.10(e) gap`});}\n  if(d.validation_status==='OUT_OF_VALIDATION'){alerts.push({...d,alert_type:'VALIDATION_EXPIRED',severity:'CRITICAL',message:`CRITICAL: ${d.system_name} validation expired \u2014 Part 11 use blocked until revalidation`});}\n  if(d.eres_status==='SIGNATURE_BINDING_FAILURE'){alerts.push({...d,alert_type:'ERES_FAILURE',severity:'CRITICAL',message:`CRITICAL: ${d.system_name} e-signature binding failed \u2014 \u00a711.50 violation`});}\n  if(d.open_deviations>5){alerts.push({...d,alert_type:'DEVIATION_BACKLOG',severity:'HIGH',message:`HIGH: ${d.system_name} has ${d.open_deviations} open deviations`});}\n}\nreturn alerts.length?alerts:[{json:{no_alerts:true,checked_at:new Date().toISOString()}}];"
      }
    },
    {
      "id": "4",
      "name": "Has Alerts?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        910,
        300
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{$json.no_alerts}}",
              "operation": "notEqual",
              "value2": true
            }
          ]
        }
      }
    },
    {
      "id": "5",
      "name": "Log Alert to Postgres",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2,
      "position": [
        1130,
        200
      ],
      "parameters": {
        "operation": "insert",
        "table": "part11_audit_alerts",
        "columns": "system_name,alert_type,severity,message,detected_at",
        "values": "={{$json.system_name}},={{$json.alert_type}},={{$json.severity}},={{$json.message}},={{new Date().toISOString()}}"
      }
    },
    {
      "id": "6",
      "name": "Slack #quality-systems-critical",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2,
      "position": [
        1350,
        200
      ],
      "parameters": {
        "channel": "#quality-systems-critical",
        "text": "{{$json.message}} | System: {{$json.system_name}} | {{new Date().toISOString()}}"
      }
    }
  ],
  "connections": {
    "Schedule 15min": {
      "main": [
        [
          {
            "node": "Fetch Audit Trail Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Audit Trail Status": {
      "main": [
        [
          {
            "node": "Evaluate Compliance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evaluate Compliance": {
      "main": [
        [
          {
            "node": "Has Alerts?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Alerts?": {
      "main": [
        [
          {
            "node": "Log Alert to Postgres",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Log Alert to Postgres": {
      "main": [
        [
          {
            "node": "Slack #quality-systems-critical",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 3: GxP & FDA Regulatory Filing Deadline Tracker

Runs weekdays at 7AM. Pulls from Google Sheets. Covers 12 deadline types across FDA, EMA, WHO, and DEA — per customer.

Deadline types covered:
| Deadline | Regulatory Cite | Penalty |
|---|---|---|
| FDA Annual Product Review (APR) | 21 CFR §211.180(e) | FDA 483 → Warning Letter |
| IND Annual Report | 21 CFR §312.33 | IND placed on clinical hold |
| NDA/BLA PDUFA Date | PDUFA commitment | CRL → 12+ month delay |
| BIMO Inspection Prep | FDA BIMO Program | Clinical data rejected |
| ICH Q10 Management Review | ICH Q10 §3.4.1 | Inspection finding |
| ICH Q9 Risk Assessment | ICH Q9 | GMP deviation — recall risk |
| EMA Annual GMP Compliance | EMA/189422/2014 | Certificate suspension |
| EU GMP Annual Product Review | Annex 16 | QP certification blocked |
| WHO GMP Certificate Renewal | WHO TRS 986 | Export to WHO markets blocked |
| DEA Schedule Registration | 21 CFR Part 1301 | Controlled substance halt |

{
  "name": "GxP & FDA Regulatory Filing Deadline Tracker",
  "nodes": [
    {
      "id": "1",
      "name": "Schedule Weekdays 7AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        250,
        300
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 7 * * 1-5"
            }
          ]
        }
      }
    },
    {
      "id": "2",
      "name": "Fetch Deadlines",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        470,
        300
      ],
      "parameters": {
        "operation": "read",
        "sheetId": "YOUR_SHEET_ID",
        "range": "A:H",
        "keyRow": 1
      }
    },
    {
      "id": "3",
      "name": "Classify Urgency",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        690,
        300
      ],
      "parameters": {
        "jsCode": "const today=new Date();\nconst results=[];\nconst deadlineTypes={\n  'FDA_ANNUAL_PRODUCT_REVIEW':{'desc':'Annual Product Review (APR/PQR)','penalty':'FDA 483 observation \u2192 Warning Letter','days_notice':60},\n  'IND_ANNUAL_REPORT':{'desc':'IND Annual Report (21 CFR \u00a7312.33)','penalty':'IND placed on clinical hold','days_notice':30},\n  'NDA_PDUFA_DATE':{'desc':'NDA/BLA PDUFA Action Date','penalty':'Complete Response Letter \u2192 12+ month delay','days_notice':90},\n  'FDA_BIORESEARCH_MONITORING':{'desc':'Bioresearch Monitoring (BIMO) Inspection Prep','penalty':'Clinical data rejected \u2192 NDA refusal','days_notice':30},\n  'ICH_Q10_MGMT_REVIEW':{'desc':'ICH Q10 Management Review (annual)','penalty':'Regulatory inspection finding','days_notice':30},\n  'ICH_Q9_RISK_ASSESSMENT':{'desc':'ICH Q9 Risk Assessment Review','penalty':'GMP deviation \u2014 recall risk','days_notice':21},\n  'EMA_ANNUAL_COMPLIANCE':{'desc':'EMA Annual GMP Compliance Report','penalty':'EMA GMP certificate suspension','days_notice':45},\n  'EU_GMP_ANNUAL_PRODUCT_REVIEW':{'desc':'EU GMP Annual Product Review (Annex 16)','penalty':'Qualified Person (QP) certification blocked','days_notice':60},\n  'WHO_GMP_RENEWAL':{'desc':'WHO GMP Certificate Renewal','penalty':'Export to WHO markets blocked','days_notice':90},\n  'DEA_SCHEDULE_RENEWAL':{'desc':'DEA Schedule I-V Registration Renewal','penalty':'Controlled substance manufacturing halted','days_notice':60},\n  'USDA_BIOLOGICAL_LICENSE':{'desc':'USDA Biological License Renewal','penalty':'Veterinary biologics manufacturing halted','days_notice':90},\n  'CLIA_CERTIFICATE_RENEWAL':{'desc':'CLIA Certificate of Compliance Renewal','penalty':'Clinical lab testing operations halted','days_notice':60}\n};\nfor(const row of items){\n  const d=row.json;\n  if(!d.due_date||!d.deadline_type)continue;\n  const due=new Date(d.due_date);\n  const daysLeft=Math.round((due-today)/(86400000));\n  const meta=deadlineTypes[d.deadline_type]||{};\n  const urgency=daysLeft<0?'OVERDUE':daysLeft<=7?'CRITICAL':daysLeft<=14?'URGENT':daysLeft<=30?'WARNING':'NOTICE';\n  results.push({json:{...d,...meta,days_left:daysLeft,urgency,penalty_risk:meta.penalty,due_date_fmt:d.due_date}});\n}\nreturn results.filter(r=>r.json.urgency!=='NOTICE');"
      }
    },
    {
      "id": "4",
      "name": "Route by Urgency",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [
        910,
        300
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "critical",
              "conditions": {
                "string": [
                  {
                    "value1": "={{$json.urgency}}",
                    "operation": "equals",
                    "value2": "CRITICAL"
                  }
                ]
              }
            },
            {
              "outputKey": "overdue",
              "conditions": {
                "string": [
                  {
                    "value1": "={{$json.urgency}}",
                    "operation": "equals",
                    "value2": "OVERDUE"
                  }
                ]
              }
            },
            {
              "outputKey": "urgent",
              "conditions": {
                "string": [
                  {
                    "value1": "={{$json.urgency}}",
                    "operation": "equals",
                    "value2": "URGENT"
                  }
                ]
              }
            }
          ]
        }
      }
    },
    {
      "id": "5",
      "name": "Slack #regulatory-critical",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2,
      "position": [
        1130,
        150
      ],
      "parameters": {
        "channel": "#regulatory-critical",
        "text": "\ud83d\udea8 {{$json.urgency}} ({{$json.days_left}}d): *{{$json.desc}}* | Customer: {{$json.customer_name}} | Risk: {{$json.penalty_risk}}"
      }
    },
    {
      "id": "6",
      "name": "Gmail Regulatory Team",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2,
      "position": [
        1130,
        300
      ],
      "parameters": {
        "operation": "send",
        "to": "={{$json.owner_email}}",
        "subject": "[{{$json.urgency}}] {{$json.desc}} \u2014 {{$json.days_left}} days remaining",
        "message": "Regulatory deadline {{$json.urgency}}: {{$json.desc}} for {{$json.customer_name}}. Due: {{$json.due_date_fmt}}. Penalty: {{$json.penalty_risk}}."
      }
    },
    {
      "id": "7",
      "name": "Log to Regulatory Register",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2,
      "position": [
        1130,
        450
      ],
      "parameters": {
        "operation": "insert",
        "table": "regulatory_deadline_log",
        "columns": "customer_id,deadline_type,urgency,days_left,notified_at",
        "values": "={{$json.customer_id}},={{$json.deadline_type}},={{$json.urgency}},={{$json.days_left}},={{new Date().toISOString()}}"
      }
    }
  ],
  "connections": {
    "Schedule Weekdays 7AM": {
      "main": [
        [
          {
            "node": "Fetch Deadlines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Deadlines": {
      "main": [
        [
          {
            "node": "Classify Urgency",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Urgency": {
      "main": [
        [
          {
            "node": "Route by Urgency",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Urgency": {
      "critical": [
        [
          {
            "node": "Slack #regulatory-critical",
            "type": "main",
            "index": 0
          },
          {
            "node": "Gmail Regulatory Team",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log to Regulatory Register",
            "type": "main",
            "index": 0
          }
        ]
      ],
      "overdue": [
        [
          {
            "node": "Slack #regulatory-critical",
            "type": "main",
            "index": 0
          },
          {
            "node": "Gmail Regulatory Team",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log to Regulatory Register",
            "type": "main",
            "index": 0
          }
        ]
      ],
      "urgent": [
        [
          {
            "node": "Gmail Regulatory Team",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log to Regulatory Register",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 4: FDA Inspection & Warning Letter Alert Pipeline

Webhook-triggered. Covers 7 event types with 30-minute deduplication and a Postgres inspection log.

Event types handled:

  • FDA_WARNING_LETTER → CRITICAL, 15 working days to respond (FD&C Act §305)
  • FDA_483_OBSERVATION → HIGH, 15 business days CAPA response
  • DATA_INTEGRITY_FINDING → CRITICAL, 72h initial CAPA plan (FDA Data Integrity Guidance 2018)
  • EMA_GMP_STATEMENT_CONCERN → HIGH, 30 days EMA response
  • OOS_RESULT → CRITICAL, full investigation required (21 CFR §211.192)
  • CAPA_OVERDUE → HIGH, immediate escalation (ICH Q10 §3.2.3)
  • DEVIATION_CORRECTIVE_ACTION → MEDIUM, 30-day CAPA closure target
{
  "name": "FDA Inspection & Warning Letter Alert Pipeline",
  "nodes": [
    {
      "id": "1",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        250,
        300
      ],
      "parameters": {
        "path": "pharma-regulatory-event",
        "method": "POST",
        "responseMode": "responseNode"
      }
    },
    {
      "id": "2",
      "name": "Deduplicate 30min",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        470,
        300
      ],
      "parameters": {
        "jsCode": "const d=items[0].json;\nconst key=`${d.event_type}_${d.customer_id}_${d.system_name||'global'}`;\nconst state=$getWorkflowStaticData('global');\nconst now=Date.now();\nif(state[key]&&(now-state[key])<30*60*1000){return[];}\nstate[key]=now;\nconst classmap={\n  'FDA_WARNING_LETTER':{'severity':'CRITICAL','channel':'#quality-systems-critical','deadline':'15 working days to respond (21 CFR \u00a7558.8)','cite':'FD&C Act \u00a7305'},\n  'FDA_483_OBSERVATION':{'severity':'HIGH','channel':'#quality-systems','deadline':'15 business days for CAPA response','cite':'21 CFR Part 20'},\n  'DATA_INTEGRITY_FINDING':{'severity':'CRITICAL','channel':'#quality-systems-critical','deadline':'72h for initial CAPA plan','cite':'FDA Data Integrity Guidance 2018'},\n  'EMA_GMP_STATEMENT_CONCERN':{'severity':'HIGH','channel':'#quality-systems','deadline':'30 days EMA response','cite':'EMA/189422/2014'},\n  'OOS_RESULT':{'severity':'CRITICAL','channel':'#quality-systems','deadline':'21 CFR \u00a7211.192 full investigation required','cite':'FDA OOS Guidance 2006'},\n  'CAPA_OVERDUE':{'severity':'HIGH','channel':'#capa-management','deadline':'Immediate escalation','cite':'ICH Q10 \u00a73.2.3'},\n  'DEVIATION_CORRECTIVE_ACTION':{'severity':'MEDIUM','channel':'#quality-systems','deadline':'30-day CAPA closure target','cite':'ICH Q10'}\n};\nconst meta=classmap[d.event_type]||{severity:'MEDIUM',channel:'#quality-systems',deadline:'Assess within 24h',cite:'SOP-QS-001'};\nreturn[{json:{...d,...meta,detected_at:new Date().toISOString()}}];"
      }
    },
    {
      "id": "3",
      "name": "Respond 200",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        470,
        450
      ],
      "parameters": {
        "responseCode": 200,
        "responseBody": "{\"status\":\"received\"}"
      }
    },
    {
      "id": "4",
      "name": "Log Inspection Event",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2,
      "position": [
        690,
        300
      ],
      "parameters": {
        "operation": "insert",
        "table": "fda_inspection_log",
        "columns": "customer_id,event_type,severity,deadline,detected_at,cite",
        "values": "={{$json.customer_id}},={{$json.event_type}},={{$json.severity}},={{$json.deadline}},={{$json.detected_at}},={{$json.cite}}"
      }
    },
    {
      "id": "5",
      "name": "Slack Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2,
      "position": [
        910,
        250
      ],
      "parameters": {
        "channel": "={{$json.channel}}",
        "text": "\ud83d\udea8 *{{$json.severity}}* \u2014 {{$json.event_type}} | Customer: {{$json.customer_name}} | Deadline: {{$json.deadline}} | Cite: {{$json.cite}} | {{$json.detected_at}}"
      }
    },
    {
      "id": "6",
      "name": "Gmail QA Director",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2,
      "position": [
        910,
        400
      ],
      "parameters": {
        "operation": "send",
        "to": "={{$json.qa_director_email}}",
        "subject": "[{{$json.severity}}] {{$json.event_type}} \u2014 {{$json.customer_name}} \u2014 Action Required",
        "message": "Regulatory event: {{$json.event_type}}\nCustomer: {{$json.customer_name}}\nSeverity: {{$json.severity}}\nDeadline: {{$json.deadline}}\nRegulatory cite: {{$json.cite}}\nDetected: {{$json.detected_at}}"
      }
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Deduplicate 30min",
            "type": "main",
            "index": 0
          },
          {
            "node": "Respond 200",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Deduplicate 30min": {
      "main": [
        [
          {
            "node": "Log Inspection Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Inspection Event": {
      "main": [
        [
          {
            "node": "Slack Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Gmail QA Director",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 5: Weekly PharmaTech Platform KPI Dashboard

Runs Monday 8AM. Pulls dual Postgres views. Builds HTML table with WoW% deltas. Flags CAPA rate <90%, audit trail completeness <99%, and high open deviations. BCC CEO, VP Quality, CMO.

Metrics tracked:

  • MRR (with WoW%)
  • Active customers (with WoW%)
  • Part 11 coverage (customers with active audit trails / total)
  • Average audit trail completeness %
  • Open deviations total
  • CAPA on-time rate %
  • New trials this week / trial conversion
  • Average time to validation (days)
{
  "name": "Weekly PharmaTech Platform KPI Dashboard",
  "nodes": [
    {
      "id": "1",
      "name": "Schedule Monday 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        250,
        300
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      }
    },
    {
      "id": "2",
      "name": "Fetch Platform Metrics",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2,
      "position": [
        470,
        200
      ],
      "parameters": {
        "operation": "select",
        "query": "SELECT active_customers, customers_with_part11_active, avg_audit_trail_completeness_pct, open_deviations_total, capa_on_time_rate_pct, mrr_usd, new_trials_this_week, trials_converted_this_week, avg_time_to_validation_days FROM platform_metrics_weekly WHERE week = current_date - interval '7 days'"
      }
    },
    {
      "id": "3",
      "name": "Fetch Prior Week",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2,
      "position": [
        470,
        400
      ],
      "parameters": {
        "operation": "select",
        "query": "SELECT mrr_usd as prior_mrr, active_customers as prior_customers FROM platform_metrics_weekly WHERE week = current_date - interval '14 days'"
      }
    },
    {
      "id": "4",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        690,
        300
      ],
      "parameters": {
        "mode": "combine",
        "combineBy": "position"
      }
    },
    {
      "id": "5",
      "name": "Build KPI Report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        910,
        300
      ],
      "parameters": {
        "jsCode": "const d=items[0].json;\nconst mrrGrowth=d.prior_mrr?((d.mrr_usd-d.prior_mrm)/d.prior_mrr*100).toFixed(1):0;\nconst custGrowth=d.prior_customers?((d.active_customers-d.prior_customers)/d.prior_customers*100).toFixed(1):0;\nconst capaFlag=d.capa_on_time_rate_pct<90?'\u26a0\ufe0f CAPA rate below 90% \u2014 ICH Q10 \u00a73.2.3 risk':'';\nconst devFlag=d.open_deviations_total>20?'\u26a0\ufe0f High open deviations \u2014 quality risk':'';\nconst part11Flag=d.avg_audit_trail_completeness_pct<99?'\u26a0\ufe0f Audit trail completeness below 99% \u2014 Part 11 gap':'';\nconst html=`<h2>FlowKit PharmaTech Platform \u2014 Weekly KPI</h2>\n<table border='1' cellpadding='6'>\n<tr><th>Metric</th><th>This Week</th><th>WoW</th></tr>\n<tr><td>MRR</td><td>$${Number(d.mrr_usd).toLocaleString()}</td><td>${mrrGrowth>0?'+':''}${mrrGrowth}%</td></tr>\n<tr><td>Active Customers</td><td>${d.active_customers}</td><td>${custGrowth>0?'+':''}${custGrowth}%</td></tr>\n<tr><td>Part 11 Coverage</td><td>${d.customers_with_part11_active}/${d.active_customers}</td><td>\u2014</td></tr>\n<tr><td>Audit Trail Completeness</td><td>${d.avg_audit_trail_completeness_pct}%</td><td>\u2014</td></tr>\n<tr><td>Open Deviations</td><td>${d.open_deviations_total}</td><td>\u2014</td></tr>\n<tr><td>CAPA On-Time Rate</td><td>${d.capa_on_time_rate_pct}%</td><td>\u2014</td></tr>\n<tr><td>New Trials</td><td>${d.new_trials_this_week}</td><td>\u2014</td></tr>\n<tr><td>Trial Conversion</td><td>${d.trials_converted_this_week}</td><td>\u2014</td></tr>\n<tr><td>Avg Time to Validation</td><td>${d.avg_time_to_validation_days}d</td><td>\u2014</td></tr>\n</table>\n${capaFlag?'<p>'+capaFlag+'</p>':''}\n${devFlag?'<p>'+devFlag+'</p>':''}\n${part11Flag?'<p>'+part11Flag+'</p>':''}`;\nconst flags=[capaFlag,devFlag,part11Flag].filter(Boolean);\nconst subject=flags.length?`[ACTION: ${flags.length} quality flags] FlowKit PharmaTech KPI \u2014 Week of ${new Date().toISOString().slice(0,10)}`:`FlowKit PharmaTech KPI \u2014 Week of ${new Date().toISOString().slice(0,10)}`;\nreturn[{json:{...d,html_body:html,email_subject:subject,slack_summary:`MRR $${Number(d.mrr_usd).toLocaleString()} (${mrrGrowth>0?'+':''}${mrrGrowth}%) | Customers: ${d.active_customers} | Part 11: ${d.customers_with_part11_active} active | CAPA: ${d.capa_on_time_rate_pct}% on-time | Deviations open: ${d.open_deviations_total}`}}];"
      }
    },
    {
      "id": "6",
      "name": "Gmail CEO + QA",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2,
      "position": [
        1130,
        250
      ],
      "parameters": {
        "operation": "send",
        "to": "ceo@yourcompany.com",
        "bcc": "vp-quality@yourcompany.com,cmo@yourcompany.com",
        "subject": "={{$json.email_subject}}",
        "message": "={{$json.html_body}}",
        "options": {
          "contentType": "html"
        }
      }
    },
    {
      "id": "7",
      "name": "Slack #exec-pharma-kpis",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2,
      "position": [
        1130,
        400
      ],
      "parameters": {
        "channel": "#exec-pharma-kpis",
        "text": "={{$json.slack_summary}}"
      }
    }
  ],
  "connections": {
    "Schedule Monday 8AM": {
      "main": [
        [
          {
            "node": "Fetch Platform Metrics",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Prior Week",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Platform Metrics": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Prior Week": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Build KPI Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build KPI Report": {
      "main": [
        [
          {
            "node": "Gmail CEO + QA",
            "type": "main",
            "index": 0
          },
          {
            "node": "Slack #exec-pharma-kpis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why PharmaTech SaaS Teams Are Replacing Zapier with n8n

Issue Zapier/Make n8n Self-Hosted
FDA audit trail retention 30-day auto-delete Unlimited — git-versioned
GxP Computer System Validation Black-box, no IQ/OQ/PQ evidence Full source, reproducible validation
EMA Annex 11 audit availability 30 days only System lifetime
GDPR Art.44 data transfer US→EU SCCs required No transfer — stays on-prem
ICH Q10 CAPA evidence Task log only Full execution trace + Postgres
NERC CIP / 21 CFR Part 11 air-gap Cannot self-host Docker on-prem, air-gapped
Cost at 1M tasks/month $500+/mo $0 + $20/mo VPS

The FDA's 2023 Data Integrity Guidance specifically calls out automated systems that don't retain complete audit trails. If your PharmaTech SaaS is routing GxP-adjacent data through Zapier and a regulatory agency asks for 18 months of workflow audit history, the answer is: it doesn't exist.


Get the Pre-Built Templates

All 5 workflows are available as import-ready JSON on the FlowKit store.

The AI Customer Support Bot and Customer Feedback Analyzer templates are particularly useful for PharmaTech SaaS teams managing high-volume regulatory inquiries and customer QA tickets.

Questions or want the full Postgres schema for the part11_system_registry and pharma_customer_onboarding tables? Drop a comment below.

Top comments (0)