DEV Community

Alex Kane
Alex Kane

Posted on

n8n for GovTech/Public Safety SaaS Vendors: 5 Automations for CJIS, NG911, and StateRAMP Compliance (Free JSON)

n8n for GovTech/Public Safety SaaS Vendors: 5 Automations for CJIS, NG911, and StateRAMP Compliance

If your SaaS platform serves PSAPs, law enforcement agencies, fire/EMS operations, corrections facilities, or emergency management teams, you're operating in one of the most heavily regulated technology environments in the US. The FBI CJIS Security Policy v5.9, CALEA lawful intercept requirements, NG911/i3 interoperability mandates, StateRAMP authorization requirements, and IPAWS authentication obligations create a compliance matrix that touches every layer of your platform.

The automation patterns below address the compliance deadlines, integration health signals, and regulatory reporting obligations that GovTech/Public Safety SaaS vendors routinely miss — not from negligence, but from the sheer number of moving parts across dozens of agency customers, each with their own ATO conditions and CJIS Security Addendum terms.

Who This Applies To

Customer Tier Example Platforms Primary Compliance Obligations
PSAP_DISPATCH_SAAS 911 CAD/dispatch, ESInet routing CJIS v5.9, NG911 i3, CALEA, IPAWS
LAW_ENFORCEMENT_RECORDS_SAAS RMS, body camera, investigative CJIS v5.9 §5.2.1.1 Advanced Auth, NCIC access rules
FIRE_EMS_OPERATIONS_SAAS Fire/EMS CAD, asset tracking NG911, FirstNet Band 14, HIPAA PHI in patient data
EMERGENCY_MANAGEMENT_SAAS EOC management, FEMA coordination IPAWS, FEMA BPAS, StateRAMP
CORRECTIONS_FACILITY_SAAS Jail/prison management, inmate tracking CJIS v5.9, PREA, ADA Title II
COURT_CASE_MANAGEMENT_SAAS Judiciary SaaS, case/docket tracking CJIS v5.9, HIPAA (mental health records), ADA §508
PUBLIC_SAFETY_ANALYTICS_SAAS Crime analytics, predictive oversight CJIS v5.9, CCPA, Title VI disparate impact

The Compliance Clock Problem

CJIS Security Policy v5.9 §5.4.7 mandates quarterly log reviews — but the clock starts when the audit period ends, not when someone manually runs the report. CALEA technical capability orders require compliance within 90 days of receipt. StateRAMP continuous monitoring requires annual POA&M updates. NG911 ESInet availability standards (FCC recommends 99.999%) mean every minute of downtime is a documented gap in your ATO evidence package.

The standard response — spreadsheets, calendar reminders, manual quarterly sweeps — doesn't scale past 10 agency customers. At 50+ agencies, each with their own CJIS Security Addendum terms and StateRAMP ATO conditions, the compliance matrix becomes unmanageable without automation.

Why Self-Hosted n8n Matters Here

CJIS Security Policy v5.9 §5.3.1 requires AES 256-bit encryption for CJI at rest and in transit, with the cloud service provider (CSP) explicitly listed in the agency's CJIS Security Addendum. Routing criminal history records, warrant data, CAD incident reports, or inmate records through Zapier/Make requires CJIS to review and approve that CSP — a process that takes months and often fails.

Self-hosted n8n running in your FedRAMP/StateRAMP-authorized cloud environment keeps CJI in the authorized enclave. The workflow JSON lives in Git — creating the auditable evidence trail that CJIS auditors require under §5.4.1.


Workflow 1: CJIS v5.9 Compliance Deadline Tracker

Who needs it: Any SaaS platform where agencies access CJI (criminal justice information).

What it catches:

  • CJIS §5.4.7 quarterly log reviews (clock starts at end of each quarter)
  • §5.12.1 personnel security screening (30-day window for new hires with CJI access)
  • §5.2.1.1 advanced authentication renewals (MFA credentials expire)
  • Annual CJIS Security Addendum renewals per subscribing agency
  • CALEA annual technical capability demonstration deadlines

Your Sheets schema (cjis_compliance_items):
| requirement_id | agency_name | saas_module | requirement_type | due_date | responsible_officer | responsible_officer_email | tier |
|---|---|---|---|---|---|---|---|
| CJIS-001 | Metro PD | Records Module | CJIS_ANNUAL_ADDENDUM | 2026-06-15 | J. Smith | j.smith@metropd.gov | LAW_ENFORCEMENT_RECORDS_SAAS |

Urgency tiers: OVERDUE → CRITICAL (≤14d) → URGENT (≤30d) → WARNING (≤60d) → NOTICE (≤90d)

{
  "name": "CJIS v5.9 Compliance Deadline Tracker",
  "nodes": [
    {
      "id": "1",
      "name": "Every Weekday 7AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        240,
        300
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 7 * * 1-5"
            }
          ]
        }
      }
    },
    {
      "id": "2",
      "name": "Load CJIS Items",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        460,
        300
      ],
      "parameters": {
        "operation": "readRows",
        "documentId": {
          "__rl": true,
          "value": "{{YOUR_SPREADSHEET_ID}}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "cjis_compliance_items",
          "mode": "name"
        },
        "options": {}
      }
    },
    {
      "id": "3",
      "name": "Classify Urgency",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ],
      "parameters": {
        "jsCode": "const today = new Date();\nconst items = $input.all().map(item => {\n  const row = item.json;\n  const dueDate = new Date(row.due_date);\n  const daysUntil = Math.floor((dueDate - today) / (1000 * 60 * 60 * 24));\n  let urgency = 'OK';\n  if (daysUntil < 0) urgency = 'OVERDUE';\n  else if (daysUntil <= 14) urgency = 'CRITICAL';\n  else if (daysUntil <= 30) urgency = 'URGENT';\n  else if (daysUntil <= 60) urgency = 'WARNING';\n  else if (daysUntil <= 90) urgency = 'NOTICE';\n  return { json: { ...row, daysUntil, urgency } };\n});\nreturn items.filter(i => i.json.urgency !== 'OK');"
      }
    },
    {
      "id": "4",
      "name": "IF Actionable",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        900,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "combinator": "and"
          },
          "conditions": [
            {
              "leftValue": "={{$json.urgency}}",
              "rightValue": "OK",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ]
        }
      }
    },
    {
      "id": "5",
      "name": "Slack Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1120,
        200
      ],
      "parameters": {
        "channel": "#compliance-team",
        "text": "={{$json.urgency}}: {{$json.requirement_type}} for {{$json.agency_name}} \u2014 due {{$json.due_date}} ({{$json.daysUntil}} days). Module: {{$json.saas_module}}. Tier: {{$json.tier}}. Owner: {{$json.responsible_officer}}",
        "otherOptions": {}
      }
    },
    {
      "id": "6",
      "name": "Email Officer",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1120,
        400
      ],
      "parameters": {
        "operation": "send",
        "toList": "={{$json.responsible_officer_email}}",
        "subject": "=[CJIS {{$json.urgency}}] {{$json.requirement_type}} \u2014 {{$json.daysUntil < 0 ? 'OVERDUE' : $json.daysUntil + ' days remaining'}}",
        "message": "=CJIS v5.9 Compliance Alert for {{$json.agency_name}}\\n\\nRequirement: {{$json.requirement_type}}\\nModule: {{$json.saas_module}}\\nDue Date: {{$json.due_date}}\\nDays Until Due: {{$json.daysUntil}}\\nUrgency: {{$json.urgency}}\\nTier: {{$json.tier}}\\n\\nCJIS \u00a75.4.7 requires quarterly log reviews. \u00a75.12.1 requires personnel screening within 30 days. Advanced authentication under \u00a75.2.1.1 must be active for all remote access. Non-compliance risks Criminal Justice Information Services (CJIS) Security Addendum termination \u2014 which terminates your agency's access to the FBI NCIC and state criminal repositories.\\n\\nAction required by {{$json.due_date}}.\\n\\n\u2014 FlowKit Compliance Monitor",
        "options": {}
      }
    }
  ],
  "connections": {
    "Every Weekday 7AM": {
      "main": [
        [
          {
            "node": "Load CJIS Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load CJIS Items": {
      "main": [
        [
          {
            "node": "Classify Urgency",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Urgency": {
      "main": [
        [
          {
            "node": "IF Actionable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Actionable": {
      "main": [
        [
          {
            "node": "Slack Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Email Officer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 2: NG911/i3 ESInet Integration Health Monitor

Who needs it: CAD vendors, ESInet operators, TextTo911 platforms, PSAP software vendors.

What it catches:

  • ESInet endpoint failures (NG911 i3 §5.1 availability requirements)
  • TextTo911 service gaps (FCC §20.18(n) accessibility mandate — civil penalty for downtime)
  • ECRF/LVF location routing degradation (mis-routed 911 calls = liability)
  • ESInet response time violations (FCC recommends <100ms for emergency call routing)

Your Sheets schema (ng911_integrations):
| psap_id | psap_name | endpoint_url | integration_type | tier |
|---|---|---|---|---|
| PSAP-001 | County 911 Center | https://esinet.county.gov/health | ESInet | PSAP_DISPATCH_SAAS |

Integration types: ESInet, TextTo911, ECRF, LVF, CallHandling, PSAP_Gateway

{
  "name": "NG911/i3 ESInet Integration Health Monitor",
  "nodes": [
    {
      "id": "1",
      "name": "Every 5 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        240,
        300
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 5
            }
          ]
        }
      }
    },
    {
      "id": "2",
      "name": "Load NG911 Endpoints",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        460,
        300
      ],
      "parameters": {
        "operation": "readRows",
        "documentId": {
          "__rl": true,
          "value": "{{YOUR_SPREADSHEET_ID}}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "ng911_integrations",
          "mode": "name"
        },
        "options": {}
      }
    },
    {
      "id": "3",
      "name": "Batch 3",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        680,
        300
      ],
      "parameters": {
        "batchSize": 3,
        "options": {}
      }
    },
    {
      "id": "4",
      "name": "Ping Endpoint",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        900,
        300
      ],
      "parameters": {
        "method": "GET",
        "url": "={{$json.endpoint_url}}",
        "options": {
          "timeout": 5000,
          "response": {
            "response": {
              "neverError": true
            }
          }
        }
      }
    },
    {
      "id": "5",
      "name": "Classify Health",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        300
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\nconst statusCode = item.statusCode || 0;\nconst responseTime = item.responseTime || 9999;\nlet health = 'OK';\nlet risk = '';\nif (statusCode === 0 || statusCode >= 500) {\n  health = 'DOWN';\n  risk = item.integration_type === 'ESInet'\n    ? 'CRITICAL: NG911 i3 interoperability gap \u2014 911 calls may not route to correct PSAP'\n    : item.integration_type === 'TextTo911'\n    ? 'HIGH: Text-to-911 service unavailable \u2014 ADA Title II disability accommodation gap'\n    : 'HIGH: ' + item.integration_type + ' offline';\n} else if (responseTime > 2000 || (statusCode >= 400 && statusCode < 500)) {\n  health = 'DEGRADED';\n  risk = 'NG911 i3 standard recommends <100ms ESInet response \u2014 ' + responseTime + 'ms degrades call routing';\n}\nreturn [{ json: { ...item, health, risk, statusCode, responseTime, checked_at: new Date().toISOString() } }];"
      }
    },
    {
      "id": "6",
      "name": "IF Not OK",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        1340,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "combinator": "and"
          },
          "conditions": [
            {
              "leftValue": "={{$json.health}}",
              "rightValue": "OK",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ]
        }
      }
    },
    {
      "id": "7",
      "name": "Slack NOC Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1560,
        200
      ],
      "parameters": {
        "channel": "#noc-public-safety",
        "text": "=\ud83d\udea8 {{$json.health}}: {{$json.psap_name}} \u2014 {{$json.integration_type}} at {{$json.endpoint_url}}\\nRisk: {{$json.risk}}\\nHTTP: {{$json.statusCode}} | Response: {{$json.responseTime}}ms\\nTier: {{$json.tier}} | Checked: {{$json.checked_at}}",
        "otherOptions": {}
      }
    }
  ],
  "connections": {
    "Every 5 Minutes": {
      "main": [
        [
          {
            "node": "Load NG911 Endpoints",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load NG911 Endpoints": {
      "main": [
        [
          {
            "node": "Batch 3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Batch 3": {
      "main": [
        [
          {
            "node": "Ping Endpoint",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ping Endpoint": {
      "main": [
        [
          {
            "node": "Classify Health",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Health": {
      "main": [
        [
          {
            "node": "IF Not OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Not OK": {
      "main": [
        [
          {
            "node": "Slack NOC Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 3: CALEA/CJIS Breach & Audit Event Pipeline

Who needs it: Any GovTech SaaS vendor with CALEA lawful intercept capability or CJIS data access.

The deadline structure:
| Event Type | Reporting Body | Citation | Deadline |
|---|---|---|---|
| CJIS_UNAUTHORIZED_ACCESS | FBI CJIS Division | CJIS Policy v5.9 §5.4.7 | 1 hour |
| CALEA_INTERCEPT_UNAUTHORIZED | FBI/DOJ | 47 USC §1002 — $10K/day civil penalty | 1 hour |
| NG911_DATA_BREACH | FCC/CISA | NIST SP 800-187 + HIPAA if PHI | 24 hours |
| CJIS_POLICY_VIOLATION | State CSO | CJIS Policy §5.4.1 | 24 hours |
| STATERAMP_FINDING | StateRAMP PMO | ATO conditions — open POA&M item | 72 hours |
| FIRSTNET_CUI_EXPOSURE | FirstNet Authority | Band 14 security requirements | 72 hours |
| IPAWS_AUTH_FAILURE | FEMA IPAWS PMO | 47 CFR §11.61 EAS test failure | 24 hours |

{
  "name": "CALEA/CJIS Breach & Audit Event Pipeline",
  "nodes": [
    {
      "id": "1",
      "name": "Audit Event Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        300
      ],
      "parameters": {
        "path": "public-safety-audit",
        "responseMode": "onReceived",
        "options": {}
      }
    },
    {
      "id": "2",
      "name": "Classify Event",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ],
      "parameters": {
        "jsCode": "const event = $input.first().json;\nconst typeMap = {\n  CJIS_UNAUTHORIZED_ACCESS: { severity: 'CRITICAL', deadline_hours: 1, body: 'FBI CJIS', citation: 'CJIS Security Policy v5.9 \u00a75.4.7 \u2014 mandatory incident reporting' },\n  CALEA_INTERCEPT_UNAUTHORIZED: { severity: 'CRITICAL', deadline_hours: 1, body: 'FBI/DOJ', citation: '47 USC \u00a71002 CALEA \u2014 unauthorized intercept capability = $10K/day civil penalty' },\n  NG911_DATA_BREACH: { severity: 'CRITICAL', deadline_hours: 24, body: 'FCC/CISA', citation: 'NIST SP 800-187 NG911 Security Framework + HIPAA 45 CFR \u00a7164.408 if PHI involved' },\n  CJIS_POLICY_VIOLATION: { severity: 'HIGH', deadline_hours: 24, body: 'State CSO', citation: 'CJIS Security Policy v5.9 \u00a75.4.1 \u2014 violation must be reported to appointing authority' },\n  STATERAMP_FINDING: { severity: 'HIGH', deadline_hours: 72, body: 'StateRAMP PMO', citation: 'StateRAMP Continuous Monitoring \u2014 open POA&M item deadline per ATO conditions' },\n  FIRSTNET_CUI_EXPOSURE: { severity: 'HIGH', deadline_hours: 72, body: 'FirstNet Authority', citation: 'FirstNet Band 14 security requirements + DFARS 252.204-7012 if DoD overlap' },\n  IPAWS_AUTH_FAILURE: { severity: 'MEDIUM', deadline_hours: 24, body: 'FEMA IPAWS PMO', citation: '47 CFR \u00a711.61 \u2014 EAS monthly test failure must be reported' }\n};\nconst meta = typeMap[event.event_type] || { severity: 'LOW', deadline_hours: 72, body: 'Security Team', citation: 'Internal security policy' };\nconst deadline = new Date(Date.now() + meta.deadline_hours * 3600000).toISOString();\nreturn [{ json: { ...event, ...meta, report_deadline: deadline, logged_at: new Date().toISOString() } }];"
      }
    },
    {
      "id": "3",
      "name": "Slack Emergency",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        680,
        200
      ],
      "parameters": {
        "channel": "#security-emergency",
        "text": "=\ud83d\udea8 {{$json.severity}} PUBLIC SAFETY INCIDENT: {{$json.event_type}}\\nAgency: {{$json.agency_name}} | Module: {{$json.saas_module}}\\nCitation: {{$json.citation}}\\nReport to: {{$json.body}} by {{$json.report_deadline}}\\nEvent: {{$json.description}}",
        "otherOptions": {}
      }
    },
    {
      "id": "4",
      "name": "Log Incident",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        680,
        400
      ],
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "{{YOUR_SPREADSHEET_ID}}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "cjis_incident_log",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "autoMapInputData",
          "value": {}
        },
        "options": {}
      }
    }
  ],
  "connections": {
    "Audit Event Webhook": {
      "main": [
        [
          {
            "node": "Classify Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Event": {
      "main": [
        [
          {
            "node": "Slack Emergency",
            "type": "main",
            "index": 0
          },
          {
            "node": "Log Incident",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 4: StateRAMP/FirstNet Certification Tracker

Who needs it: GovTech vendors seeking or maintaining state/local government authorizations.

What it tracks:

  • StateRAMP High/Moderate ATO renewals (typically 2-3 year cycles with annual ConMon)
  • FirstNet Band 14 authentication credential renewals
  • CJIS Security Addendum renewals per subscribing agency
  • CALEA annual technical capability test deadlines

The business impact: Expired StateRAMP ATO = immediate suspension of new agency seat provisioning. Lapsed CJIS Security Addendum = loss of NCIC/state repository access for all agency users on that module.

Your Sheets schema (agency_certifications):
| agency_id | agency_name | certification_type | status | expiry_date | account_manager | account_manager_email | tier |
|---|---|---|---|---|---|---|---|
| AG-001 | State Police | STATERAMP_HIGH | ACTIVE | 2026-07-01 | K. Lee | k.lee@yourco.com | LAW_ENFORCEMENT_RECORDS_SAAS |

{
  "name": "StateRAMP/FirstNet Certification Tracker",
  "nodes": [
    {
      "id": "1",
      "name": "Daily 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        240,
        300
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * *"
            }
          ]
        }
      }
    },
    {
      "id": "2",
      "name": "Load Certifications",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        460,
        300
      ],
      "parameters": {
        "operation": "readRows",
        "documentId": {
          "__rl": true,
          "value": "{{YOUR_SPREADSHEET_ID}}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "agency_certifications",
          "mode": "name"
        },
        "options": {}
      }
    },
    {
      "id": "3",
      "name": "Classify Expiry",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ],
      "parameters": {
        "jsCode": "const today = new Date();\nconst items = $input.all().map(item => {\n  const row = item.json;\n  const expiry = new Date(row.expiry_date);\n  const daysUntil = Math.floor((expiry - today) / (1000 * 60 * 60 * 24));\n  let urgency = 'OK';\n  if (daysUntil < 0) urgency = 'EXPIRED';\n  else if (daysUntil <= 30) urgency = 'CRITICAL';\n  else if (daysUntil <= 60) urgency = 'URGENT';\n  else if (daysUntil <= 90) urgency = 'WARNING';\n  else if (daysUntil <= 120) urgency = 'NOTICE';\n  const dedup_key = row.agency_id + '_' + row.certification_type + '_' + new Date().toISOString().slice(0, 10);\n  return { json: { ...row, daysUntil, urgency, dedup_key } };\n});\nreturn items.filter(i => i.json.urgency !== 'OK');"
      }
    },
    {
      "id": "4",
      "name": "Slack CS Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        900,
        200
      ],
      "parameters": {
        "channel": "#customer-success-gov",
        "text": "={{$json.urgency}}: {{$json.agency_name}} \u2014 {{$json.certification_type}} expires {{$json.expiry_date}} ({{$json.daysUntil}} days). Account Manager: {{$json.account_manager}}. Tier: {{$json.tier}}",
        "otherOptions": {}
      }
    },
    {
      "id": "5",
      "name": "Email Account Manager",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        900,
        400
      ],
      "parameters": {
        "operation": "send",
        "toList": "={{$json.account_manager_email}}",
        "subject": "=[{{$json.urgency}}] {{$json.agency_name}} \u2014 {{$json.certification_type}} Renewal Required",
        "message": "=Certification Renewal Alert\\n\\nAgency: {{$json.agency_name}}\\nCertification: {{$json.certification_type}}\\nExpiry: {{$json.expiry_date}} ({{$json.daysUntil}} days)\\nStatus: {{$json.urgency}}\\n\\nAction Required:\\n- STATERAMP_HIGH/MODERATE: Initiate StateRAMP annual continuous monitoring submission via StateRAMP PMO portal\\n- FIRSTNET_BAND14: Renew FirstNet Authority authentication credentials\\n- CJIS_SECURITY_ADDENDUM: Obtain updated signed Security Addendum from agency head\\n- CALEA_CAPABILITY_TEST: Schedule annual CALEA technical capability demonstration with FBI/DOJ\\n\\nExpired certifications block new agency seat provisioning and may trigger ATO suspension.\\n\\n\u2014 FlowKit Compliance Monitor",
        "options": {}
      }
    }
  ],
  "connections": {
    "Daily 8AM": {
      "main": [
        [
          {
            "node": "Load Certifications",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Certifications": {
      "main": [
        [
          {
            "node": "Classify Expiry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Expiry": {
      "main": [
        [
          {
            "node": "Slack CS Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email Account Manager",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Workflow 5: Weekly Public Safety Platform KPI Dashboard

Who needs it: GovTech SaaS CEOs and CISOs tracking revenue, compliance, and operational health in one view.

KPIs in the report:

  • MRR + WoW% change (PSAP seat revenue)
  • Active PSAPs on platform
  • NG911/ESInet aggregate uptime percentage
  • CJIS incidents in last 30 days
  • Certification renewals due in ≤30 days
  • StateRAMP ATO status

The governance angle: StateRAMP Continuous Monitoring requires a designated Authorizing Official (AO) to review monthly security metrics. This report BCC'd to your StateRAMP ATO officer closes that evidence gap automatically — every Monday, documented, timestamped, in your ATO evidence package.

{
  "name": "Weekly Public Safety Platform KPI Dashboard",
  "nodes": [
    {
      "id": "1",
      "name": "Monday 8AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        240,
        300
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1"
            }
          ]
        }
      }
    },
    {
      "id": "2",
      "name": "Load Platform Metrics",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        460,
        200
      ],
      "parameters": {
        "operation": "readRows",
        "documentId": {
          "__rl": true,
          "value": "{{YOUR_SPREADSHEET_ID}}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "platform_metrics",
          "mode": "name"
        },
        "options": {}
      }
    },
    {
      "id": "3",
      "name": "Load Compliance Events",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        460,
        400
      ],
      "parameters": {
        "operation": "readRows",
        "documentId": {
          "__rl": true,
          "value": "{{YOUR_SPREADSHEET_ID}}",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "cjis_incident_log",
          "mode": "name"
        },
        "options": {}
      }
    },
    {
      "id": "4",
      "name": "Merge Datasets",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        680,
        300
      ],
      "parameters": {
        "mode": "combine",
        "combineBy": "combineAll",
        "options": {}
      }
    },
    {
      "id": "5",
      "name": "Compute KPIs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        300
      ],
      "parameters": {
        "jsCode": "const allItems = $input.all().map(i => i.json);\nconst metrics = allItems.find(i => i.mrr_usd !== undefined) || {};\nconst prevMetrics = allItems.find(i => i.prev_mrr_usd !== undefined) || {};\nconst events = allItems.filter(i => i.event_type !== undefined);\nconst mrrCurrent = parseFloat(metrics.mrr_usd || 0);\nconst mrrPrev = parseFloat(metrics.prev_mrr_usd || mrrCurrent);\nconst mrrWoW = mrrPrev > 0 ? (((mrrCurrent - mrrPrev) / mrrPrev) * 100).toFixed(1) : '0.0';\nconst cjisIncidents30d = events.filter(e => {\n  const d = new Date(e.logged_at || e.event_date || '2000-01-01');\n  return (Date.now() - d.getTime()) < 30 * 24 * 3600000 && e.event_type && e.event_type.includes('CJIS');\n}).length;\nconst activePsaps = parseInt(metrics.active_psaps || 0);\nconst ng911Uptime = parseFloat(metrics.ng911_uptime_pct || 99.9).toFixed(3);\nconst certRenewals30d = parseInt(metrics.certs_expiring_30d || 0);\nconst stateRampStatus = metrics.stateramp_ato_status || 'ACTIVE';\nconst flag = pct => parseFloat(pct) < 0 ? '\ud83d\udd34' : parseFloat(pct) > 5 ? '\ud83d\udfe2' : '\ud83d\udfe1';\nconst html = '<h2>FlowKit Public Safety Platform \u2014 Weekly KPI Report</h2>' +\n  '<table border=1 cellpadding=6 style=\"border-collapse:collapse\">' +\n  '<tr><th>Metric</th><th>Value</th><th>WoW</th></tr>' +\n  '<tr><td>MRR</td><td>$' + mrrCurrent.toLocaleString() + '</td><td>' + flag(mrrWoW) + ' ' + mrrWoW + '%</td></tr>' +\n  '<tr><td>Active PSAPs</td><td>' + activePsaps + '</td><td>\u2014</td></tr>' +\n  '<tr><td>NG911/ESInet Uptime</td><td>' + ng911Uptime + '%</td><td>' + (parseFloat(ng911Uptime) >= 99.999 ? '\ud83d\udfe2' : parseFloat(ng911Uptime) >= 99.9 ? '\ud83d\udfe1' : '\ud83d\udd34') + '</td></tr>' +\n  '<tr><td>CJIS Incidents (30d)</td><td>' + cjisIncidents30d + '</td><td>' + (cjisIncidents30d > 0 ? '\ud83d\udd34' : '\ud83d\udfe2') + '</td></tr>' +\n  '<tr><td>Cert Renewals \u226430d</td><td>' + certRenewals30d + '</td><td>' + (certRenewals30d > 0 ? '\ud83d\udfe1' : '\ud83d\udfe2') + '</td></tr>' +\n  '<tr><td>StateRAMP ATO Status</td><td>' + stateRampStatus + '</td><td>' + (stateRampStatus === 'ACTIVE' ? '\ud83d\udfe2' : '\ud83d\udd34') + '</td></tr>' +\n  '</table>';\nreturn [{ json: { html, mrrCurrent, mrrWoW, activePsaps, ng911Uptime, cjisIncidents30d, certRenewals30d, stateRampStatus } }];"
      }
    },
    {
      "id": "6",
      "name": "Email Executive Team",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1120,
        300
      ],
      "parameters": {
        "operation": "send",
        "toList": "ceo@yourcompany.com",
        "ccList": "ciso@yourcompany.com, stateramp-ato@yourcompany.com",
        "subject": "=[Weekly KPI] Public Safety Platform \u2014 MRR {{$json.mrrCurrent}} | NG911 Uptime {{$json.ng911Uptime}}% | CJIS Incidents {{$json.cjisIncidents30d}}",
        "message": "={{$json.html}}",
        "options": {
          "appendAttribution": false
        }
      }
    }
  ],
  "connections": {
    "Monday 8AM": {
      "main": [
        [
          {
            "node": "Load Platform Metrics",
            "type": "main",
            "index": 0
          },
          {
            "node": "Load Compliance Events",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Platform Metrics": {
      "main": [
        [
          {
            "node": "Merge Datasets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Compliance Events": {
      "main": [
        [
          {
            "node": "Merge Datasets",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Datasets": {
      "main": [
        [
          {
            "node": "Compute KPIs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute KPIs": {
      "main": [
        [
          {
            "node": "Email Executive Team",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Implementation Notes

CJIS cloud deployment: CJIS Policy v5.9 §5.13 requires your cloud service provider to be listed on the CJIS Cloud Provider List or have a completed Cloud Provider Audit. Self-hosted n8n in AWS GovCloud (US), Azure Government, or your own FedRAMP/StateRAMP-authorized infrastructure satisfies this requirement without a separate CSP audit.

NG911 data sensitivity: ESInet call records, CAD incident data, and location routing tables are CJI in most state implementations. Any workflow touching this data must run within the CJIS-authorized boundary — not in a multi-tenant SaaS automation platform.

StateRAMP vs FedRAMP: StateRAMP uses FedRAMP baselines but adds state-specific controls and a separate PMO. If you have FedRAMP Moderate ATO, you can pursue StateRAMP Authorized status via the equivalency pathway — but your automation workflows touching state agency data must still stay within the StateRAMP-authorized boundary.

n8n vs Zapier/Make for GovTech

Requirement n8n Self-Hosted Zapier/Make
CJIS CSP compliance Run in your authorized boundary Requires separate CJIS CSP audit
StateRAMP scope Stays in-scope by design External CSP = expanded ATO scope
CALEA audit trail Git-versioned workflow JSON = evidence No audit trail for workflow logic
NG911 data sovereignty ESInet data never leaves enclave Passes through cloud iPaaS
FISMA/NIST evidence Git history = evidence artifact No NIST 800-53 controls documentation

All five workflow JSONs above are import-ready — paste them into Settings → Import workflow in your n8n instance. Replace the Sheets document IDs, Slack channel names, and email addresses with your configuration.

For the full FlowKit n8n template library (including pre-built variants for each tier), visit stripeai.gumroad.com.

Top comments (0)