DEV Community

Alex Kane
Alex Kane

Posted on

n8n for FleetTech & TransportTech SaaS: 5 Automations for FMCSA ELD, HOS Compliance, and DOT Audit Readiness (Free JSON)

If your fleet management SaaS handles driver ELD data, HOS logs, or DQ files — and you're routing any of that through Zapier or Make.com — you may have a compliance problem that your legal team doesn't know about yet.

FMCSA ELD data is driver PII. Hours of Service logs track exact movements, rest locations, and work patterns. Drug test results are medical records. Driver Qualification File contents include Social Security numbers, employment history, and MVR data. Under CCPA and various state commercial vehicle privacy regulations, routing any of this through a third-party multi-tenant iPaaS creates unauthorized data exposure — the kind that triggers both regulatory scrutiny and carrier liability.

Self-hosted n8n keeps all of this on your infrastructure. Here are five fleet compliance workflows your SaaS platform should be running.


Workflow 1: FMCSA HOS Violation & ELD Alert Pipeline

Every ELD device generates violation events when a driver goes over-hours. The question is what happens next.

This workflow receives ELD webhook events, classifies violations by severity (CRITICAL for §395.3 11-hour and 70-hour limits, HIGH for sleeper berth and adverse conditions, CRITICAL for log falsification), routes CRITICAL violations to #fleet-safety-critical in Slack with penalty exposure shown ($16,000/violation for most CRITICAL types), emails the safety director with the FMCSA rule citation and required action, and logs everything to Postgres with the incident ID — which you'll need if FMCSA requests records under §390.31.

The key angle: FMCSA ELD data belongs to the carrier and must be producible for roadside inspection under §395.8(k). If it's flowing through Zapier, your audit trail may have gaps — and Zapier's standard ToS doesn't include the data retention guarantees §390.31 requires (3 years minimum).

{
  "name": "FMCSA HOS Violation & ELD Alert Pipeline",
  "nodes": [
    {
      "id": "1",
      "name": "ELD Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "parameters": {
        "path": "eld-hos-violation",
        "responseMode": "onReceived"
      },
      "position": [
        250,
        300
      ]
    },
    {
      "id": "2",
      "name": "Classify HOS Violation",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "\nconst data = $json.body || $json;\nconst violationType = data.violation_type || 'UNKNOWN';\nconst driverId = data.driver_id || 'UNKNOWN';\nconst vehicleId = data.vehicle_id || 'UNKNOWN';\nconst hosMinutesOver = data.minutes_over_limit || 0;\nconst carrierDotNumber = data.carrier_dot_number || '';\nconst eldDeviceId = data.eld_device_id || '';\nconst locationState = data.location_state || '';\nconst timestamp = new Date().toISOString();\n\nconst severityMap = {\n  'DRIVING_OVER_11H_LIMIT': { severity: 'CRITICAL', rule: '49 CFR \u00a7395.3(a)(3)', penalty: '$16,000/violation', smsPts: 2, action: 'Out of service \u2014 driver must stop immediately' },\n  'DRIVING_OVER_70H_8DAY': { severity: 'CRITICAL', rule: '49 CFR \u00a7395.3(b)', penalty: '$16,000/violation', smsPts: 2, action: 'Driver out of service until hours reset' },\n  'SLEEPER_BERTH_VIOLATION': { severity: 'HIGH', rule: '49 CFR \u00a7395.1(g)', penalty: '$11,000/violation', smsPts: 1, action: 'Notify safety director + document in DQ file' },\n  'ADVERSE_CONDITIONS_MISUSE': { severity: 'HIGH', rule: '49 CFR \u00a7395.1(b)', penalty: '$11,000/violation', smsPts: 1, action: 'Review dispatch logs for coaching opportunity' },\n  'ELD_MALFUNCTION_UNREPORTED': { severity: 'HIGH', rule: '49 CFR \u00a7395.8(d)', penalty: '$16,000/violation', smsPts: 2, action: '24h to notify FMCSA + use paper RODS' },\n  'FORM_MANNER_ELD_DEFICIENCY': { severity: 'MEDIUM', rule: '49 CFR \u00a7395.8(a)', penalty: '$1,270/violation', smsPts: 1, action: 'CSM coaching + 15-day correction window' },\n  'LOG_FALSIFICATION': { severity: 'CRITICAL', rule: '49 CFR \u00a7395.8(e)', penalty: '$16,000 + criminal referral', smsPts: 4, action: 'Immediate legal review \u2014 CDL suspension risk' },\n  'MISSING_REQUIRED_INFO': { severity: 'LOW', rule: '49 CFR \u00a7395.8(f)', penalty: '$1,270/violation', smsPts: 1, action: 'Driver coaching \u2014 administrative correction' }\n};\n\nconst info = severityMap[violationType] || { severity: 'UNKNOWN', rule: 'Unknown regulation', penalty: 'Unknown', smsPts: 0, action: 'Manual review required' };\n\nreturn [{\n  json: {\n    driver_id: driverId,\n    vehicle_id: vehicleId,\n    carrier_dot_number: carrierDotNumber,\n    eld_device_id: eldDeviceId,\n    location_state: locationState,\n    violation_type: violationType,\n    minutes_over: hosMinutesOver,\n    severity: info.severity,\n    fmcsa_rule: info.rule,\n    max_penalty: info.penalty,\n    sms_points: info.smsPts,\n    required_action: info.action,\n    incident_id: `HOS-${driverId}-${Date.now()}`,\n    timestamp: timestamp\n  }\n}];\n"
      },
      "position": [
        450,
        300
      ]
    },
    {
      "id": "3",
      "name": "Route by Severity",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.severity }}",
              "operation": "equals",
              "value2": "CRITICAL"
            }
          ]
        }
      },
      "position": [
        650,
        300
      ]
    },
    {
      "id": "4",
      "name": "Slack Critical Alert",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#fleet-safety-critical",
        "text": "={{ '\ud83d\udea8 CRITICAL HOS VIOLATION\\n' + 'Driver: ' + $json.driver_id + ' | Vehicle: ' + $json.vehicle_id + '\\n' + 'Violation: ' + $json.violation_type + ' (' + $json.minutes_over + 'min over)\\n' + 'Rule: ' + $json.fmcsa_rule + ' | Penalty: ' + $json.max_penalty + '\\n' + 'SMS Points: +' + $json.sms_points + ' | Action: ' + $json.required_action + '\\n' + 'Incident ID: ' + $json.incident_id }}"
      },
      "position": [
        850,
        200
      ]
    },
    {
      "id": "5",
      "name": "Email Safety Director",
      "type": "n8n-nodes-base.emailSend",
      "parameters": {
        "toEmail": "safety@fleetops.com",
        "subject": "={{ '[' + $json.severity + '] HOS Violation \u2014 Driver ' + $json.driver_id + ' \u2014 ' + $json.fmcsa_rule }}",
        "text": "={{ 'FMCSA HOS Violation Alert\\n\\nSeverity: ' + $json.severity + '\\nDriver ID: ' + $json.driver_id + '\\nVehicle: ' + $json.vehicle_id + '\\nCarrier DOT#: ' + $json.carrier_dot_number + '\\nELD Device: ' + $json.eld_device_id + '\\n\\nViolation: ' + $json.violation_type + '\\nMinutes Over Limit: ' + $json.minutes_over + '\\nFMCSA Rule: ' + $json.fmcsa_rule + '\\nMax Penalty: ' + $json.max_penalty + '\\nCSA SMS Points: +' + $json.sms_points + '\\n\\nRequired Action: ' + $json.required_action + '\\n\\nIncident ID: ' + $json.incident_id + ' (retain per 49 CFR Part 390.31 \u2014 3 years minimum)\\n\\nThis alert generated by self-hosted n8n. All ELD data stays in-enclave \u2014 no third-party cloud exposure.' }}"
      },
      "position": [
        850,
        400
      ]
    },
    {
      "id": "6",
      "name": "Log to Postgres",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "insert",
        "table": "hos_violations",
        "columns": "driver_id,vehicle_id,carrier_dot_number,violation_type,severity,fmcsa_rule,sms_points,incident_id,created_at",
        "additionalFields": {}
      },
      "position": [
        1050,
        300
      ]
    }
  ],
  "connections": {
    "ELD Webhook Trigger": {
      "main": [
        [
          {
            "node": "Classify HOS Violation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify HOS Violation": {
      "main": [
        [
          {
            "node": "Route by Severity",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Severity": {
      "main": [
        [
          {
            "node": "Slack Critical Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Email Safety Director",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack Critical Alert": {
      "main": [
        [
          {
            "node": "Log to Postgres",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email Safety Director": {
      "main": [
        [
          {
            "node": "Log to Postgres",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 2: Driver Qualification File Expiry Monitor

A lapsed CDL or expired medical examiner certificate makes a driver legally prohibited from operating a CMV. An MVR check that slips past its annual window creates carrier liability for undisclosed violations. A missing background investigation is a DQ file deficiency in an FMCSA compliance review.

This workflow runs every weekday morning, reads your DQ file register from Google Sheets, classifies expiry urgency for 10 document types (CDL license, medical examiner certificate, MVR annual check, road test certificate, background investigation, employment application, HAZMAT endorsement, ELDT certificate, drug test pre-employment clearance), and sends EXPIRED/CRITICAL/URGENT/WARNING alerts to Slack and the safety director — with the FMCSA rule, legal consequence, and a deduplification check so the same driver doesn't get five alerts in one week.

Retention note embedded in the email: 49 CFR §390.31 requires 3 years from date of employment, plus 3 years after departure. If you're storing DQ files in Zapier task logs, you're not meeting this requirement.

{
  "name": "Driver Qualification File Expiry Monitor",
  "nodes": [
    {
      "id": "1",
      "name": "Daily Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 7 * * 1-5"
            }
          ]
        }
      },
      "position": [
        250,
        300
      ]
    },
    {
      "id": "2",
      "name": "Read DQ File Register",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "readRows",
        "documentId": "YOUR_SPREADSHEET_ID",
        "sheetName": "DQ_Files"
      },
      "position": [
        450,
        300
      ]
    },
    {
      "id": "3",
      "name": "Classify DQ Expiry",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "\nconst today = new Date();\nconst alerts = [];\n\nfor (const row of $input.all()) {\n  const driverId = row.json.driver_id;\n  const driverName = row.json.driver_name;\n  const docType = row.json.document_type;\n  const expiryDateStr = row.json.expiry_date;\n  const carrierDot = row.json.carrier_dot_number || '';\n  const alertSentDate = row.json.alert_sent_date || '';\n\n  if (!expiryDateStr) continue;\n  const expiryDate = new Date(expiryDateStr);\n  const daysLeft = Math.floor((expiryDate - today) / (1000 * 60 * 60 * 24));\n\n  // Dedup: skip if alerted today\n  const todayStr = today.toISOString().split('T')[0];\n  if (alertSentDate === todayStr) continue;\n\n  const regulationMap = {\n    'CDL_LICENSE': { rule: '49 CFR \u00a7391.21', consequence: 'Driver legally prohibited from operating CMV', window: 0 },\n    'MEDICAL_EXAMINER_CERTIFICATE': { rule: '49 CFR \u00a7391.41(a)', consequence: 'Immediate disqualification \u2014 federal OOS violation', window: 0 },\n    'MEDICAL_EXAMINER_CERT_WAIVER': { rule: '49 CFR \u00a7391.64', consequence: 'Loses waiver \u2014 may need full DOT physical', window: 0 },\n    'MVR_ANNUAL_CHECK': { rule: '49 CFR \u00a7391.25', consequence: 'Carrier liability for undisclosed violations', window: 30 },\n    'ROAD_TEST_CERTIFICATE': { rule: '49 CFR \u00a7391.31', consequence: 'Required before solo CMV operation', window: 0 },\n    'BACKGROUND_INVESTIGATION': { rule: '49 CFR \u00a7391.23', consequence: 'Incomplete DQ file \u2014 audit deficiency', window: 30 },\n    'EMPLOYMENT_APPLICATION': { rule: '49 CFR \u00a7391.21', consequence: 'Missing required DQ file element', window: 30 },\n    'HAZMAT_ENDORSEMENT': { rule: '49 CFR \u00a7391.11(b)', consequence: 'Cannot transport HAZMAT \u2014 PHMSA violation risk', window: 30 },\n    'ENTRY_LEVEL_DRIVER_TRAINING': { rule: '49 CFR Part 380', consequence: 'ELDT required before Class A/B/Hazmat/Passenger', window: 30 },\n    'DRUG_TEST_PRE_EMPLOYMENT': { rule: '49 CFR \u00a7382.301', consequence: 'Cannot operate CMV without negative pre-employment test', window: 0 }\n  };\n\n  const reg = regulationMap[docType] || { rule: '49 CFR Part 391', consequence: 'DQ file deficiency', window: 30 };\n\n  let urgency = null;\n  if (daysLeft < 0) urgency = 'EXPIRED';\n  else if (daysLeft <= 7) urgency = 'CRITICAL';\n  else if (daysLeft <= 21) urgency = 'URGENT';\n  else if (daysLeft <= reg.window) urgency = 'WARNING';\n\n  if (urgency) {\n    alerts.push({\n      json: {\n        driver_id: driverId,\n        driver_name: driverName,\n        document_type: docType,\n        expiry_date: expiryDateStr,\n        days_left: daysLeft,\n        urgency: urgency,\n        fmcsa_rule: reg.rule,\n        consequence: reg.consequence,\n        carrier_dot: carrierDot\n      }\n    });\n  }\n}\n\nreturn alerts.length > 0 ? alerts : [{ json: { no_alerts: true } }];\n"
      },
      "position": [
        650,
        300
      ]
    },
    {
      "id": "4",
      "name": "Has Alerts?",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.no_alerts }}",
              "operation": "notEqual",
              "value2": true
            }
          ]
        }
      },
      "position": [
        850,
        300
      ]
    },
    {
      "id": "5",
      "name": "Slack DQ Alert",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#fleet-compliance",
        "text": "={{ '\ud83d\udccb DQ File Expiry Alert \u2014 ' + $json.urgency + '\\n' + 'Driver: ' + $json.driver_name + ' (' + $json.driver_id + ')\\n' + 'Document: ' + $json.document_type + '\\n' + 'Expires: ' + $json.expiry_date + ' (' + $json.days_left + ' days)\\n' + 'Rule: ' + $json.fmcsa_rule + '\\n' + 'Consequence: ' + $json.consequence }}"
      },
      "position": [
        1050,
        200
      ]
    },
    {
      "id": "6",
      "name": "Email Safety Director",
      "type": "n8n-nodes-base.emailSend",
      "parameters": {
        "toEmail": "safety@fleetops.com",
        "subject": "={{ '[DQ ' + $json.urgency + '] ' + $json.document_type + ' \u2014 ' + $json.driver_name + ' \u2014 ' + $json.days_left + 'd remaining' }}",
        "text": "={{ 'Driver Qualification File Alert\\n\\n' + $json.urgency + ': ' + $json.document_type + '\\nDriver: ' + $json.driver_name + ' (' + $json.driver_id + ')\\nCarrier DOT#: ' + $json.carrier_dot + '\\nExpiry: ' + $json.expiry_date + ' (' + $json.days_left + ' days remaining)\\nFMCSA Rule: ' + $json.fmcsa_rule + '\\nConsequence if missed: ' + $json.consequence + '\\n\\nAction required: Renew document before expiry to maintain compliant DQ file per 49 CFR Part 391.\\nRetention: 3 years from date of employment + 3 years after departure (49 CFR \u00a7390.31).\\n\\nGenerated by self-hosted n8n \u2014 DQ file data stays on-premises.' }}"
      },
      "position": [
        1050,
        400
      ]
    }
  ],
  "connections": {
    "Daily Schedule": {
      "main": [
        [
          {
            "node": "Read DQ File Register",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read DQ File Register": {
      "main": [
        [
          {
            "node": "Classify DQ Expiry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify DQ Expiry": {
      "main": [
        [
          {
            "node": "Has Alerts?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Alerts?": {
      "main": [
        [
          {
            "node": "Slack DQ Alert",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 3: DOT Drug & Alcohol Testing Compliance Tracker

Drug and alcohol testing under 49 CFR Part 382 has some of the tightest compliance windows in fleet operations. Post-accident drug testing has a 32-hour window (alcohol is 8 hours). A driver who misses a random selection is treated the same as a positive test result. A return-to-duty test must be cleared before the driver touches another CMV.

This workflow tracks pre-employment, random, post-accident, reasonable suspicion, return-to-duty, follow-up, and annual random pool enrollment deadlines — classifying OVERDUE/CRITICAL (≤3 days)/URGENT (≤7 days)/WARNING (≤14 days) with the rule citation and consequence shown in the Slack alert.

Drug test results are the most sensitive data a fleet company handles. They're medical records, and unauthorized disclosure creates liability under both FMCSA regulations and state employment law. They should never touch a multi-tenant cloud workflow platform.

{
  "name": "DOT Drug & Alcohol Testing Compliance Tracker",
  "nodes": [
    {
      "id": "1",
      "name": "Weekday Schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1-5"
            }
          ]
        }
      },
      "position": [
        250,
        300
      ]
    },
    {
      "id": "2",
      "name": "Read Drug Test Register",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "readRows",
        "documentId": "YOUR_SPREADSHEET_ID",
        "sheetName": "Drug_Test_Log"
      },
      "position": [
        450,
        300
      ]
    },
    {
      "id": "3",
      "name": "Classify Testing Compliance",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "\nconst today = new Date();\nconst todayStr = today.toISOString().split('T')[0];\nconst alerts = [];\n\nfor (const row of $input.all()) {\n  const driverId = row.json.driver_id;\n  const driverName = row.json.driver_name;\n  const testType = row.json.test_type;\n  const dueDateStr = row.json.due_date;\n  const status = row.json.status || 'PENDING';\n  const alertSentDate = row.json.alert_sent_date || '';\n\n  if (!dueDateStr || status === 'COMPLETED') continue;\n  if (alertSentDate === todayStr) continue;\n\n  const dueDate = new Date(dueDateStr);\n  const daysLeft = Math.floor((dueDate - today) / (1000 * 60 * 60 * 24));\n\n  const testRules = {\n    'PRE_EMPLOYMENT': { rule: '49 CFR \u00a7382.301', consequence: 'Cannot operate CMV \u2014 immediate OOS risk', timing: 'Before first CMV operation' },\n    'RANDOM': { rule: '49 CFR \u00a7382.305', consequence: 'Failure to test = equivalent to positive result', timing: 'FMCSA minimum 50% annual rate (drug) / 10% (alcohol)' },\n    'POST_ACCIDENT': { rule: '49 CFR \u00a7382.303', consequence: 'Required within 32h (drug) / 8h (alcohol) of qualifying accident', timing: 'Time-critical: 8-32h window' },\n    'REASONABLE_SUSPICION': { rule: '49 CFR \u00a7382.307', consequence: 'Driver must be removed from service immediately', timing: 'Within 24h of supervisor observation' },\n    'RETURN_TO_DUTY': { rule: '49 CFR \u00a7382.309', consequence: 'Cannot return to safety-sensitive function without negative test', timing: 'Before resuming CMV operation' },\n    'FOLLOW_UP': { rule: '49 CFR \u00a7382.311', consequence: 'Minimum 6 tests in 12 months post-RTD', timing: 'Per SAP follow-up plan' },\n    'ANNUAL_RANDOM_POOL': { rule: '49 CFR \u00a7382.305(b)', consequence: 'Annual pool enrollment verification required', timing: 'Annual calendar year' }\n  };\n\n  const testInfo = testRules[testType] || { rule: '49 CFR Part 382', consequence: 'DOT testing non-compliance', timing: 'Per testing program' };\n\n  let urgency = null;\n  if (daysLeft < 0) urgency = 'OVERDUE';\n  else if (daysLeft <= 3) urgency = 'CRITICAL';\n  else if (daysLeft <= 7) urgency = 'URGENT';\n  else if (daysLeft <= 14) urgency = 'WARNING';\n\n  if (urgency) {\n    alerts.push({\n      json: {\n        driver_id: driverId,\n        driver_name: driverName,\n        test_type: testType,\n        due_date: dueDateStr,\n        days_left: daysLeft,\n        urgency: urgency,\n        fmcsa_rule: testInfo.rule,\n        consequence: testInfo.consequence,\n        timing_note: testInfo.timing,\n        status: status\n      }\n    });\n  }\n}\n\nreturn alerts.length > 0 ? alerts : [{ json: { no_alerts: true } }];\n"
      },
      "position": [
        650,
        300
      ]
    },
    {
      "id": "4",
      "name": "Filter Alerts",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.no_alerts }}",
              "operation": "notEqual",
              "value2": true
            }
          ]
        }
      },
      "position": [
        850,
        300
      ]
    },
    {
      "id": "5",
      "name": "Slack Drug Test Alert",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#fleet-drug-testing",
        "text": "={{ '\ud83e\uddea DOT Drug Test Due \u2014 ' + $json.urgency + '\\n' + 'Driver: ' + $json.driver_name + ' | Test Type: ' + $json.test_type + '\\n' + 'Due: ' + $json.due_date + ' (' + $json.days_left + ' days)\\n' + 'Rule: ' + $json.fmcsa_rule + '\\n' + '\u26a0\ufe0f Consequence: ' + $json.consequence }}"
      },
      "position": [
        1050,
        300
      ]
    }
  ],
  "connections": {
    "Weekday Schedule": {
      "main": [
        [
          {
            "node": "Read Drug Test Register",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Drug Test Register": {
      "main": [
        [
          {
            "node": "Classify Testing Compliance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Testing Compliance": {
      "main": [
        [
          {
            "node": "Filter Alerts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Alerts": {
      "main": [
        [
          {
            "node": "Slack Drug Test Alert",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 4: FMCSA & DOT Compliance Deadline Tracker

Beyond driver-level compliance, carriers and fleet SaaS vendors face a constant stream of regulatory filing deadlines: UCR annual renewal, IFTA quarterly returns, IRP plate renewal, PHMSA HAZMAT registration ($75,906/violation/day for HAZMAT operators who let it lapse), MCS-150 biennial update, drug testing MIS annual report (due March 15), annual vehicle inspections, and CSA intervention response windows.

This workflow checks 12 deadline types against your compliance register every weekday morning, classifies OVERDUE/CRITICAL/URGENT/WARNING/NOTICE with the regulation, penalty exposure, and required action, then routes urgent items to Slack and emails the deadline owner — all with a dedup check so a deadline that's been alerted doesn't repeat until the date changes.

{
  "name": "FMCSA & DOT Compliance Deadline Tracker",
  "nodes": [
    {
      "id": "1",
      "name": "Weekday Morning",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1-5"
            }
          ]
        }
      },
      "position": [
        250,
        300
      ]
    },
    {
      "id": "2",
      "name": "Read Compliance Deadlines",
      "type": "n8n-nodes-base.googleSheets",
      "parameters": {
        "operation": "readRows",
        "documentId": "YOUR_SPREADSHEET_ID",
        "sheetName": "FMCSA_Deadlines"
      },
      "position": [
        450,
        300
      ]
    },
    {
      "id": "3",
      "name": "Classify Urgency",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "\nconst today = new Date();\nconst todayStr = today.toISOString().split('T')[0];\nconst alerts = [];\n\nconst actionMap = {\n  'FMCSA_UCR_ANNUAL': { rule: '49 USC \u00a714504a', portal: 'ucr.gov', penalty: '$1M+/year unpaid fees', action: 'Renew Unified Carrier Registration at ucr.gov' },\n  'IFTA_QUARTERLY': { rule: 'IFTA Agreement', portal: 'iftach.org', penalty: '$50+/return + interest', action: 'File IFTA fuel tax return for previous quarter' },\n  'IRP_RENEWAL': { rule: 'IRP Plan', portal: 'State DMV', penalty: 'OOS vehicle', action: 'Renew International Registration Plan apportioned license plates' },\n  'DOT_HAZMAT_REGISTRATION': { rule: '49 CFR Part 107 Subpart G', portal: 'phmsa.dot.gov/hazmat-program-management', penalty: '$75,906/violation/day', action: 'Renew PHMSA Hazardous Materials Registration \u2014 carriers offering interstate HAZMAT' },\n  'FMCSA_BIENNIAL_UPDATE': { rule: '49 CFR \u00a7390.19', portal: 'fmcsa.dot.gov', penalty: '$1,270-$12,695/violation', action: 'File MCS-150 biennial update within 30 days of anniversary month' },\n  'BOC_3_PROCESS_AGENT': { rule: '49 CFR \u00a7366', portal: 'fmcsa.dot.gov', penalty: 'Operating authority revocation', action: 'Verify BOC-3 blanket process agent filing is current' },\n  'DRUG_TESTING_MIS_ANNUAL': { rule: '49 CFR \u00a7382.403', portal: 'FMCSA MCMIS', penalty: '$10,000/violation', action: 'Submit Annual MIS Drug & Alcohol Testing Summary by March 15' },\n  'VEHICLE_ANNUAL_INSPECTION': { rule: '49 CFR \u00a7396.17', portal: 'Internal', penalty: '$16,000 OOS fine', action: 'Complete FMCSA annual vehicle inspection + retain certificate 14 months' },\n  'DRIVER_ANNUAL_REVIEW': { rule: '49 CFR \u00a7391.25', portal: 'Internal', penalty: '$10,933+/violation', action: 'Complete annual driver record review + MVR check per \u00a7391.25' },\n  'ROADSIDE_INSPECTION_FOLLOW_UP': { rule: '49 CFR \u00a7386', portal: 'FMCSA DataQs', penalty: 'Negative CSA SMS impact', action: 'Challenge erroneous roadside inspection data in DataQs within 90 days' },\n  'ELD_CALIBRATION_AUDIT': { rule: '49 CFR \u00a7395.22(g)', portal: 'Internal', penalty: '$16,000/violation', action: 'Verify ELD device calibration + data transfer audit' },\n  'CSA_INTERVENTION_RESPONSE': { rule: '49 CFR \u00a7386.81', portal: 'fmcsa.dot.gov/safety/carrier-safety', penalty: 'Compliance review / OOS order', action: 'Respond to FMCSA CSA intervention within deadline' }\n};\n\nfor (const row of $input.all()) {\n  const deadlineType = row.json.deadline_type;\n  const dueDateStr = row.json.due_date;\n  const owner = row.json.owner_email || '';\n  const alertSentDate = row.json.alert_sent_date || '';\n\n  if (!dueDateStr) continue;\n  if (alertSentDate === todayStr) continue;\n\n  const dueDate = new Date(dueDateStr);\n  const daysLeft = Math.floor((dueDate - today) / (1000 * 60 * 60 * 24));\n\n  const info = actionMap[deadlineType] || { rule: '49 CFR', portal: 'fmcsa.dot.gov', penalty: 'Regulatory penalty', action: 'Complete required FMCSA/DOT filing' };\n\n  let urgency = null;\n  if (daysLeft < 0) urgency = 'OVERDUE';\n  else if (daysLeft <= 7) urgency = 'CRITICAL';\n  else if (daysLeft <= 21) urgency = 'URGENT';\n  else if (daysLeft <= 45) urgency = 'WARNING';\n  else if (daysLeft <= 90) urgency = 'NOTICE';\n\n  if (urgency) {\n    alerts.push({\n      json: {\n        deadline_type: deadlineType,\n        due_date: dueDateStr,\n        days_left: daysLeft,\n        urgency: urgency,\n        fmcsa_rule: info.rule,\n        portal: info.portal,\n        penalty: info.penalty,\n        required_action: info.action,\n        owner_email: owner\n      }\n    });\n  }\n}\n\nreturn alerts.length > 0 ? alerts : [{ json: { no_alerts: true } }];\n"
      },
      "position": [
        650,
        300
      ]
    },
    {
      "id": "4",
      "name": "Has Deadlines?",
      "type": "n8n-nodes-base.if",
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.no_alerts }}",
              "operation": "notEqual",
              "value2": true
            }
          ]
        }
      },
      "position": [
        850,
        300
      ]
    },
    {
      "id": "5",
      "name": "Route by Urgency",
      "type": "n8n-nodes-base.switch",
      "parameters": {
        "value": "={{ $json.urgency }}",
        "rules": {
          "values": [
            {
              "value": "OVERDUE",
              "output": 0
            },
            {
              "value": "CRITICAL",
              "output": 0
            },
            {
              "value": "URGENT",
              "output": 1
            },
            {
              "value": "WARNING",
              "output": 2
            },
            {
              "value": "NOTICE",
              "output": 2
            }
          ]
        }
      },
      "position": [
        1050,
        250
      ]
    },
    {
      "id": "6",
      "name": "Slack Urgent Alert",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#fleet-compliance",
        "text": "={{ '\u26a0\ufe0f FMCSA DEADLINE \u2014 ' + $json.urgency + '\\n' + $json.deadline_type + ' due in ' + $json.days_left + ' days (' + $json.due_date + ')\\n' + 'Rule: ' + $json.fmcsa_rule + '\\n' + 'Penalty: ' + $json.penalty + '\\n' + 'Action: ' + $json.required_action + '\\n' + 'Portal: ' + $json.portal }}"
      },
      "position": [
        1250,
        200
      ]
    },
    {
      "id": "7",
      "name": "Email Owner",
      "type": "n8n-nodes-base.emailSend",
      "parameters": {
        "toEmail": "={{ $json.owner_email }}",
        "subject": "={{ '[FMCSA ' + $json.urgency + '] ' + $json.deadline_type + ' \u2014 ' + $json.days_left + ' days remaining' }}",
        "text": "={{ 'FMCSA/DOT Compliance Deadline Alert\\n\\n' + $json.urgency + ': ' + $json.deadline_type + '\\nDue: ' + $json.due_date + ' (' + $json.days_left + ' days remaining)\\nRegulation: ' + $json.fmcsa_rule + '\\nPenalty if missed: ' + $json.penalty + '\\n\\nRequired action: ' + $json.required_action + '\\nPortal/resource: ' + $json.portal + '\\n\\nThis deadline is tracked in your self-hosted n8n compliance system. All FMCSA filing data stays on-premises.' }}"
      },
      "position": [
        1250,
        400
      ]
    }
  ],
  "connections": {
    "Weekday Morning": {
      "main": [
        [
          {
            "node": "Read Compliance Deadlines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Compliance Deadlines": {
      "main": [
        [
          {
            "node": "Classify Urgency",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Urgency": {
      "main": [
        [
          {
            "node": "Has Deadlines?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Deadlines?": {
      "main": [
        [
          {
            "node": "Route by Urgency",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Route by Urgency": {
      "main": [
        [
          {
            "node": "Slack Urgent Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Email Owner",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Email Owner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 5: Weekly Fleet Safety KPI Dashboard

Every Monday morning, this workflow queries your Postgres fleet operations database for two parallel metrics sets: HOS violations (7-day count, critical violations, SMS points accrued in 30 days, drivers with violations) and compliance posture (expired DQ files, critical DQ files, pending drug tests due in 14 days, critical FMCSA deadlines).

It generates an HTML dashboard email color-coded by threshold severity and sends it to the CEO with the safety director CC'd. If there are expired DQ files, critical HOS violations, or overdue FMCSA deadlines, those appear as flags in the subject line: [DQ EXPIRED: 2] [HOS CRITICAL: 1] Weekly Fleet KPI.

The safety director BCC is intentional — it closes the governance gap where fleet compliance issues are visible to operations but never reach the C-suite until there's an FMCSA compliance review.

{
  "name": "Weekly Fleet Safety KPI Dashboard",
  "nodes": [
    {
      "id": "1",
      "name": "Monday 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      },
      "position": [
        250,
        300
      ]
    },
    {
      "id": "2",
      "name": "Query Fleet Metrics",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(*) FILTER (WHERE violation_date >= NOW()-INTERVAL '7 days') as hos_violations_7d, COUNT(*) FILTER (WHERE severity='CRITICAL' AND violation_date >= NOW()-INTERVAL '7 days') as critical_violations, SUM(sms_points) FILTER (WHERE violation_date >= NOW()-INTERVAL '30 days') as sms_points_30d, COUNT(DISTINCT driver_id) FILTER (WHERE violation_date >= NOW()-INTERVAL '7 days') as drivers_with_violations FROM hos_violations"
      },
      "position": [
        450,
        200
      ]
    },
    {
      "id": "3",
      "name": "Query Compliance Status",
      "type": "n8n-nodes-base.postgres",
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT COUNT(*) FILTER (WHERE status='EXPIRED') as dq_expired, COUNT(*) FILTER (WHERE status='CRITICAL' AND urgency='CRITICAL') as dq_critical, COUNT(*) FILTER (WHERE test_type='RANDOM' AND status='PENDING' AND due_date <= NOW()+INTERVAL '14 days') as drug_tests_pending, COUNT(*) FILTER (WHERE urgency IN ('OVERDUE','CRITICAL') AND deadline_type IN ('FMCSA_UCR_ANNUAL','DOT_HAZMAT_REGISTRATION','VEHICLE_ANNUAL_INSPECTION')) as critical_fmcsa_deadlines FROM fleet_compliance_register"
      },
      "position": [
        450,
        400
      ]
    },
    {
      "id": "4",
      "name": "Merge Metrics",
      "type": "n8n-nodes-base.merge",
      "parameters": {
        "mode": "mergeByPosition"
      },
      "position": [
        650,
        300
      ]
    },
    {
      "id": "5",
      "name": "Build KPI Report",
      "type": "n8n-nodes-base.code",
      "parameters": {
        "jsCode": "\nconst fleet = $input.first().json;\nconst compliance = $input.last().json;\n\nconst hosViolations = parseInt(fleet.hos_violations_7d || 0);\nconst criticalViolations = parseInt(fleet.critical_violations || 0);\nconst smsPoints = parseInt(fleet.sms_points_30d || 0);\nconst driversViolated = parseInt(fleet.drivers_with_violations || 0);\n\nconst dqExpired = parseInt(compliance.dq_expired || 0);\nconst dqCritical = parseInt(compliance.dq_critical || 0);\nconst drugTestsPending = parseInt(compliance.drug_tests_pending || 0);\nconst criticalDeadlines = parseInt(compliance.critical_fmcsa_deadlines || 0);\n\nconst hasUrgentIssues = criticalViolations > 0 || dqExpired > 0 || criticalDeadlines > 0;\nconst subjectFlags = [];\nif (dqExpired > 0) subjectFlags.push(`[DQ EXPIRED: ${dqExpired}]`);\nif (criticalViolations > 0) subjectFlags.push(`[HOS CRITICAL: ${criticalViolations}]`);\nif (criticalDeadlines > 0) subjectFlags.push(`[FMCSA OVERDUE: ${criticalDeadlines}]`);\n\nconst getColor = (val, warn, critical) => val >= critical ? '#d32f2f' : val >= warn ? '#f57c00' : '#388e3c';\n\nconst html = `\n<h2 style=\"font-family:sans-serif\">Weekly Fleet Safety & Compliance KPI Dashboard</h2>\n<p style=\"font-family:sans-serif;color:#666\">Week ending ${new Date().toISOString().split('T')[0]} | Self-hosted n8n</p>\n<h3 style=\"font-family:sans-serif\">HOS / ELD (Last 7 Days)</h3>\n<table style=\"border-collapse:collapse;font-family:sans-serif;width:100%\">\n<tr style=\"background:#f5f5f5\"><th style=\"padding:8px;text-align:left\">Metric</th><th style=\"padding:8px;text-align:right\">Value</th></tr>\n<tr><td style=\"padding:8px;border-top:1px solid #ddd\">HOS Violations (7d)</td><td style=\"padding:8px;border-top:1px solid #ddd;text-align:right;color:${getColor(hosViolations,3,8)}\">${hosViolations}</td></tr>\n<tr><td style=\"padding:8px;border-top:1px solid #ddd\">Critical Violations (7d)</td><td style=\"padding:8px;border-top:1px solid #ddd;text-align:right;color:${getColor(criticalViolations,1,3)}\">${criticalViolations}</td></tr>\n<tr><td style=\"padding:8px;border-top:1px solid #ddd\">SMS Points Accrued (30d)</td><td style=\"padding:8px;border-top:1px solid #ddd;text-align:right;color:${getColor(smsPoints,5,15)}\">${smsPoints}</td></tr>\n<tr><td style=\"padding:8px;border-top:1px solid #ddd\">Drivers w/ Violations (7d)</td><td style=\"padding:8px;border-top:1px solid #ddd;text-align:right\">${driversViolated}</td></tr>\n</table>\n<h3 style=\"font-family:sans-serif\">Compliance Posture</h3>\n<table style=\"border-collapse:collapse;font-family:sans-serif;width:100%\">\n<tr style=\"background:#f5f5f5\"><th style=\"padding:8px;text-align:left\">Metric</th><th style=\"padding:8px;text-align:right\">Value</th></tr>\n<tr><td style=\"padding:8px;border-top:1px solid #ddd\">DQ Files Expired</td><td style=\"padding:8px;border-top:1px solid #ddd;text-align:right;color:${getColor(dqExpired,1,3)}\">${dqExpired}</td></tr>\n<tr><td style=\"padding:8px;border-top:1px solid #ddd\">DQ Files Critical (&lt;7d)</td><td style=\"padding:8px;border-top:1px solid #ddd;text-align:right;color:${getColor(dqCritical,1,2)}\">${dqCritical}</td></tr>\n<tr><td style=\"padding:8px;border-top:1px solid #ddd\">Drug Tests Pending (&lt;14d)</td><td style=\"padding:8px;border-top:1px solid #ddd;text-align:right;color:${getColor(drugTestsPending,2,5)}\">${drugTestsPending}</td></tr>\n<tr><td style=\"padding:8px;border-top:1px solid #ddd\">Critical FMCSA Deadlines</td><td style=\"padding:8px;border-top:1px solid #ddd;text-align:right;color:${getColor(criticalDeadlines,1,2)}\">${criticalDeadlines}</td></tr>\n</table>\n<p style=\"font-family:sans-serif;font-size:12px;color:#999\">Generated by self-hosted n8n. ELD data, DQ files, and drug test records stay on-premises \u2014 no FMCSA data egress to third-party cloud.</p>\n`;\n\nconst subject = subjectFlags.length > 0\n  ? subjectFlags.join(' ') + ' Weekly Fleet KPI'\n  : 'Weekly Fleet Safety & Compliance KPI Dashboard';\n\nreturn [{ json: { html, subject } }];\n"
      },
      "position": [
        850,
        300
      ]
    },
    {
      "id": "6",
      "name": "Email CEO + Safety Director",
      "type": "n8n-nodes-base.emailSend",
      "parameters": {
        "toEmail": "ceo@fleetops.com",
        "ccEmail": "safety@fleetops.com",
        "subject": "={{ $json.subject }}",
        "html": "={{ $json.html }}"
      },
      "position": [
        1050,
        300
      ]
    },
    {
      "id": "7",
      "name": "Slack Weekly Summary",
      "type": "n8n-nodes-base.slack",
      "parameters": {
        "channel": "#exec-fleet-kpis",
        "text": "Weekly Fleet KPI posted. See email for full report."
      },
      "position": [
        1050,
        450
      ]
    }
  ],
  "connections": {
    "Monday 8AM": {
      "main": [
        [
          {
            "node": "Query Fleet Metrics",
            "type": "main",
            "index": 0
          },
          {
            "node": "Query Compliance Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Fleet Metrics": {
      "main": [
        [
          {
            "node": "Merge Metrics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Query Compliance Status": {
      "main": [
        [
          {
            "node": "Merge Metrics",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Metrics": {
      "main": [
        [
          {
            "node": "Build KPI Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build KPI Report": {
      "main": [
        [
          {
            "node": "Email CEO + Safety Director",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Email CEO + Safety Director": {
      "main": [
        [
          {
            "node": "Slack Weekly Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Why self-hosted n8n is the right architecture for fleet compliance SaaS

Requirement Zapier/Make Self-hosted n8n
§390.31 records retention (3yr+) 30-day task log Postgres — configurable retention
§395.8(k) ELD producibility to FMCSA Shared cloud storage On-premises, audit-ready
Drug test result confidentiality Third-party cloud Stays on your infrastructure
DQ file PII protection Shared multi-tenant VPC/on-prem isolation
PHMSA HAZMAT incident audit trail 30-day expiry Full history retained
Git-versioned workflow audit trail No versioning Git-JSON per workflow

The FMCSA compliance review process is document-intensive. When an investigator asks for records, the carrier needs to produce them within a defined window. If those records lived in Zapier's task history and the 30-day retention window has passed, you don't have them.

Self-hosted n8n runs on your infrastructure, keeps all data in your database, and produces workflow JSON that is itself a versioned audit artifact — showing exactly what logic ran, when, and on what data.


Download all 5 workflows

All five workflows — ELD HOS alert pipeline, DQ file expiry monitor, drug testing compliance tracker, FMCSA deadline tracker, weekly fleet KPI dashboard — are available as import-ready n8n JSON at FlowKit on Gumroad.

Import → connect your ELD webhook, Google Sheets, and Postgres database → done.

Questions about adapting these to your specific ELD provider or fleet management platform? Drop a comment below.

Top comments (0)