DEV Community

Alex Kane
Alex Kane

Posted on

n8n for GovTech SaaS: 5 Automations That Scale Civic Ops and Keep Government Data Compliant (Free Workflow JSON)

If you're building software for government — permitting platforms, grant management SaaS, civic portals, public records systems — you know the compliance requirements go beyond GDPR and SOC 2.

FISMA. FedRAMP. StateRAMP. CJIS. FOIA exemptions. The moment a government agency discovers that their permit data, FOIA requests, or criminal justice workflows are routing through a third-party cloud platform like Zapier, you have an ATO (Authority to Operate) problem.

n8n's self-hosted architecture eliminates that problem entirely. Your data never leaves the government's network perimeter. Workflows run inside the agency's VPC, on-premises, or in a FedRAMP-authorized cloud region. And the JSON workflow format gives you the git-versioned audit trail that FISMA requires.

Here are 5 real automations GovTech SaaS teams build with n8n — with full import-ready workflow JSON for each.


Why GovTech SaaS Teams Are Moving to n8n

Challenge Zapier / Make n8n (self-hosted)
FedRAMP ATO Adds to ATO scope Runs inside authorized boundary
CJIS compliance FBI CJIS §5.10 violation Air-gapped or on-prem deployment
FOIA Exemption 5/3 data Third-party disclosure risk Never leaves government network
StateRAMP data residency Multi-tenant cloud State-controlled VPC
Workflow audit trail 30-day log limit Permanent git-versioned JSON
High-volume permit/app processing Per-task cost explodes Flat VPS cost

1. Permit Application Auto-Routing & Status Tracker

Use case: When a citizen submits a permit application through your platform, automatically classify it, route it to the right reviewer queue, log it, and send confirmation — with SLA tracking.

{
  "name": "Permit Application Auto-Router",
  "nodes": [
    {"name": "Webhook", "type": "n8n-nodes-base.webhook", "parameters": {"path": "permit-submission", "responseMode": "onReceived", "responseData": "allEntries"}},
    {"name": "Classify Permit Type", "type": "n8n-nodes-base.code", "parameters": {"jsCode": "const p = $json.body; const type = p.permit_type?.toUpperCase() || ''; const amount = parseFloat(p.estimated_value) || 0; let tier = 'STANDARD'; if (type.includes('RESIDENTIAL') && amount < 50000) tier = 'RESIDENTIAL_MINOR'; else if (type.includes('COMMERCIAL')) tier = 'COMMERCIAL'; else if (type.includes('INDUSTRIAL')) tier = 'INDUSTRIAL'; else if (type.includes('UTILITY')) tier = 'UTILITY'; const autoApprove = (tier === 'RESIDENTIAL_MINOR' && amount < 5000); return [{json: {permit_id: p.permit_id, applicant: p.applicant_name, email: p.email, address: p.address, permit_type: type, tier, estimated_value: amount, auto_approve: autoApprove, submitted_at: new Date().toISOString()}}];"}},
    {"name": "Route by Tier", "type": "n8n-nodes-base.switch", "parameters": {"dataType": "string", "value1": "={{$json.tier}}", "rules": {"rules": [{"value2": "RESIDENTIAL_MINOR", "output": 0}, {"value2": "COMMERCIAL", "output": 1}, {"value2": "INDUSTRIAL", "output": 2}, {"value2": "UTILITY", "output": 3}]}, "fallbackOutput": 0}},
    {"name": "Log to Postgres", "type": "n8n-nodes-base.postgres", "parameters": {"operation": "insert", "table": "permit_applications", "columns": "permit_id,applicant,email,address,permit_type,tier,estimated_value,auto_approve,submitted_at,status", "values": "={{$json.permit_id}},={{$json.applicant}},={{$json.email}},={{$json.address}},={{$json.permit_type}},={{$json.tier}},={{$json.estimated_value}},={{$json.auto_approve}},={{$json.submitted_at}},'RECEIVED'"}},
    {"name": "Send Applicant Confirmation", "type": "n8n-nodes-base.gmail", "parameters": {"toList": "={{$json.email}}", "subject": "Permit Application Received — Ref: {{$json.permit_id}}", "message": "<p>Dear {{$json.applicant}},</p><p>Your permit application (Ref: <b>{{$json.permit_id}}</b>) has been received and is under review. You will be notified within 3-5 business days.</p>"}},
    {"name": "Alert Permits Team", "type": "n8n-nodes-base.slack", "parameters": {"channel": "#permits-ops", "text": "New {{$json.tier}} permit: {{$json.permit_id}} | Applicant: {{$json.applicant}} | Value: ${{$json.estimated_value}} | Auto-approve: {{$json.auto_approve}}"}}
  ]
}
Enter fullscreen mode Exit fullscreen mode

What it does: Webhook receives permit submission → classifies tier (RESIDENTIAL_MINOR/COMMERCIAL/INDUSTRIAL/UTILITY) and flags auto-approvable small residential permits → logs to Postgres → sends applicant confirmation with reference number → alerts the permits team on Slack with routing context.


2. Grant Application Processing Pipeline

Use case: When a grant application arrives through your platform, parse it, tier it by award size, route it to the appropriate review committee, send acknowledgment, and track SLA compliance.

{
  "name": "Grant Application Pipeline",
  "nodes": [
    {"name": "Webhook", "type": "n8n-nodes-base.webhook", "parameters": {"path": "grant-application", "responseMode": "onReceived"}},
    {"name": "Parse & Tier", "type": "n8n-nodes-base.code", "parameters": {"jsCode": "const g = $json.body; const amount = parseFloat(g.requested_amount) || 0; let tier = 'MICRO'; if (amount >= 500000) tier = 'LARGE'; else if (amount >= 100000) tier = 'MID'; else if (amount >= 10000) tier = 'SMALL'; const sla_deadline = new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(); return [{json: {grant_id: g.grant_id, org: g.organization, email: g.contact_email, program: g.program_name, requested_amount: amount, tier, category: g.category, sla_deadline, submitted_at: new Date().toISOString()}}];"}},
    {"name": "Log to Grants DB", "type": "n8n-nodes-base.postgres", "parameters": {"operation": "insert", "table": "grant_applications", "columns": "grant_id,org,email,program,requested_amount,tier,category,sla_deadline,submitted_at,status", "values": "={{$json.grant_id}},={{$json.org}},={{$json.email}},={{$json.program}},={{$json.requested_amount}},={{$json.tier}},={{$json.category}},={{$json.sla_deadline}},={{$json.submitted_at}},'RECEIVED'"}},
    {"name": "Send Acknowledgment", "type": "n8n-nodes-base.gmail", "parameters": {"toList": "={{$json.email}}", "subject": "Grant Application Received — {{$json.grant_id}}", "message": "<p>Thank you, {{$json.org}}. Your application for the {{$json.program}} program (Ref: {{$json.grant_id}}, Tier: {{$json.tier}}, Amount: ${{$json.requested_amount}}) has been received. Initial review will be completed within 3 business days.</p>"}},
    {"name": "Route to Review Committee", "type": "n8n-nodes-base.slack", "parameters": {"channel": "#grants-review", "text": "New {{$json.tier}} grant application: {{$json.grant_id}} | Org: {{$json.org}} | Program: {{$json.program}} | Amount: ${{$json.requested_amount}} | SLA: {{$json.sla_deadline}}"}}
  ]
}
Enter fullscreen mode Exit fullscreen mode

What it does: Classifies grants by tier (MICRO <$10K / SMALL / MID / LARGE ≥$500K) → logs to Postgres with SLA deadline → sends applicant acknowledgment with reference number → routes to #grants-review Slack with tier context for prioritization.


3. Public Records Request (FOIA) Manager

Use case: Track FOIA/public records requests from intake through deadline, with escalating alerts for requests approaching statutory response windows.

{
  "name": "FOIA Request Manager",
  "nodes": [
    {"name": "Schedule", "type": "n8n-nodes-base.scheduleTrigger", "parameters": {"rule": {"interval": [{"field": "cronExpression", "expression": "0 9 * * 1-5"}]}}},
    {"name": "Query Open Requests", "type": "n8n-nodes-base.postgres", "parameters": {"operation": "executeQuery", "query": "SELECT request_id, requester_email, subject, submitted_at, statutory_deadline, exemption_category, assigned_officer, EXTRACT(days FROM statutory_deadline - NOW()) AS days_remaining FROM foia_requests WHERE status = 'OPEN' ORDER BY statutory_deadline ASC;"}},
    {"name": "Classify Urgency", "type": "n8n-nodes-base.code", "parameters": {"jsCode": "return $input.all().map(item => { const d = item.json; const days = parseFloat(d.days_remaining); let urgency = 'NOTICE'; if (days < 0) urgency = 'OVERDUE'; else if (days <= 1) urgency = 'DUE_TODAY'; else if (days <= 3) urgency = 'CRITICAL'; else if (days <= 7) urgency = 'URGENT'; return {...item, json: {...d, urgency, days_remaining: days}}; }).filter(i => i.json.urgency !== 'NOTICE');"}},
    {"name": "Alert Records Office", "type": "n8n-nodes-base.slack", "parameters": {"channel": "#records-office", "text": "={{$json.urgency}} FOIA {{$json.request_id}}: \"{{$json.subject}}\" — {{$json.days_remaining}} days remaining. Officer: {{$json.assigned_officer}}"}},
    {"name": "Email Officer on OVERDUE", "type": "n8n-nodes-base.gmail", "parameters": {"toList": "={{$json.assigned_officer_email}}", "subject": "OVERDUE FOIA Request — {{$json.request_id}}", "message": "<p>FOIA request {{$json.request_id}} (\"{{$json.subject}}\") is past its statutory deadline. Immediate action required. Exemption category: {{$json.exemption_category}}.</p>"}}
  ]
}
Enter fullscreen mode Exit fullscreen mode

What it does: Runs weekday mornings → queries all open FOIA requests from Postgres → classifies urgency (OVERDUE/DUE_TODAY/CRITICAL ≤3d/URGENT ≤7d) → alerts #records-office on Slack → escalates overdue requests directly to assigned officer by email.

Key compliance note: FOIA Exemption 5 (deliberative process privilege) and Exemption 3 (statutory protections) data cannot transit commercial cloud platforms without creating a potential waiver issue. n8n runs inside the agency's network — FOIA metadata and request content never leave.


4. Government API Health & Uptime Monitor

Use case: Your platform depends on external government APIs (USASpending.gov, SAM.gov, Census Bureau, state portals). Monitor them continuously and alert your team the moment one degrades.

{
  "name": "GovTech API Monitor",
  "nodes": [
    {"name": "Every 5 Minutes", "type": "n8n-nodes-base.scheduleTrigger", "parameters": {"rule": {"interval": [{"field": "cronExpression", "expression": "*/5 * * * *"}]}}},
    {"name": "Load Endpoints", "type": "n8n-nodes-base.googleSheets", "parameters": {"operation": "read", "spreadsheetId": "YOUR_SHEET_ID", "range": "Endpoints!A:C"}},
    {"name": "Ping Each API", "type": "n8n-nodes-base.httpRequest", "parameters": {"url": "={{$json.endpoint_url}}", "method": "GET", "timeout": 5000, "continueOnFail": true}},
    {"name": "Evaluate Status", "type": "n8n-nodes-base.code", "parameters": {"jsCode": "return $input.all().map(item => { const r = item.json; const status = r.error ? 'DOWN' : (r.$response?.statusCode !== 200 ? 'DEGRADED' : 'OK'); return {...item, json: {...r, api_status: status, checked_at: new Date().toISOString()}}; }).filter(i => i.json.api_status !== 'OK');"}},
    {"name": "Dedup Alerts", "type": "n8n-nodes-base.code", "parameters": {"jsCode": "const store = $getWorkflowStaticData('global'); const now = Date.now(); return $input.all().filter(item => { const key = item.json.api_name; const lastAlert = store[key] || 0; if (now - lastAlert > 30 * 60 * 1000) { store[key] = now; return true; } return false; });"}},
    {"name": "Slack Alert", "type": "n8n-nodes-base.slack", "parameters": {"channel": "#platform-ops", "text": "={{$json.api_status}} — {{$json.api_name}} ({{$json.endpoint_url}}) at {{$json.checked_at}}"}},
    {"name": "Log Incident", "type": "n8n-nodes-base.postgres", "parameters": {"operation": "insert", "table": "api_incidents", "columns": "api_name,endpoint_url,api_status,checked_at", "values": "={{$json.api_name}},={{$json.endpoint_url}},={{$json.api_status}},={{$json.checked_at}}"}}
  ]
}
Enter fullscreen mode Exit fullscreen mode

What it does: Pings each government API every 5 minutes → classifies DOWN/DEGRADED/OK → deduplicates alerts per endpoint (30-minute window using $getWorkflowStaticData) → Slack alert to #platform-ops → logs all incidents to Postgres for SLA reporting.


5. Weekly GovTech Platform KPI Dashboard

Use case: Every Monday morning, your leadership team receives an automated HTML email with this week's platform KPIs — applications processed, revenue, SLA compliance — with week-over-week comparisons.

{
  "name": "Weekly GovTech KPI Dashboard",
  "nodes": [
    {"name": "Monday 8 AM", "type": "n8n-nodes-base.scheduleTrigger", "parameters": {"rule": {"interval": [{"field": "cronExpression", "expression": "0 8 * * 1"}]}}},
    {"name": "Query Applications", "type": "n8n-nodes-base.postgres", "parameters": {"operation": "executeQuery", "query": "SELECT COUNT(*) AS total_applications, COUNT(*) FILTER (WHERE status = 'APPROVED') AS approved, COUNT(*) FILTER (WHERE status = 'REJECTED') AS rejected, COUNT(*) FILTER (WHERE sla_breached = true) AS sla_breaches, SUM(platform_fee) AS platform_revenue FROM permit_applications WHERE submitted_at >= NOW() - INTERVAL '7 days';"}},
    {"name": "Query API Uptime", "type": "n8n-nodes-base.postgres", "parameters": {"operation": "executeQuery", "query": "SELECT api_name, COUNT(*) FILTER (WHERE api_status = 'OK') AS ok_checks, COUNT(*) AS total_checks, ROUND(100.0 * COUNT(*) FILTER (WHERE api_status = 'OK') / NULLIF(COUNT(*), 0), 1) AS uptime_pct FROM api_incidents WHERE checked_at >= NOW() - INTERVAL '7 days' GROUP BY api_name ORDER BY uptime_pct ASC;"}},
    {"name": "Build HTML Report", "type": "n8n-nodes-base.code", "parameters": {"jsCode": "const store = $getWorkflowStaticData('global'); const apps = $('Query Applications').first().json; const prevApps = store.prev_apps || 0; const wow = prevApps > 0 ? ((apps.total_applications - prevApps) / prevApps * 100).toFixed(1) : 'N/A'; store.prev_apps = apps.total_applications; const slaRate = apps.total_applications > 0 ? (100 - (apps.sla_breaches / apps.total_applications * 100)).toFixed(1) : '100'; const uptimeRows = $('Query API Uptime').all().map(i => `<tr><td>${i.json.api_name}</td><td style='color:${i.json.uptime_pct >= 99 ? 'green' : 'red'}'>${i.json.uptime_pct}%</td></tr>`).join(''); const html = `<h2>GovTech Platform — Weekly KPIs</h2><table border='1' cellpadding='5'><tr><th>Metric</th><th>This Week</th><th>WoW</th></tr><tr><td>Applications</td><td>${apps.total_applications}</td><td>${wow}%</td></tr><tr><td>Approved</td><td>${apps.approved}</td><td></td></tr><tr><td>SLA Compliance</td><td style='color:${slaRate >= 95 ? 'green' : 'red'}'>${slaRate}%</td><td></td></tr><tr><td>Platform Revenue</td><td>$${(parseFloat(apps.platform_revenue) || 0).toLocaleString()}</td><td></td></tr></table><br><h3>API Uptime</h3><table border='1' cellpadding='5'><tr><th>API</th><th>Uptime</th></tr>${uptimeRows}</table>`; return [{json: {html, sla_rate: slaRate, total: apps.total_applications}}];"}},
    {"name": "Email Leadership", "type": "n8n-nodes-base.gmail", "parameters": {"toList": "ceo@yourgovtech.com", "bccList": "cto@yourgovtech.com,cro@yourgovtech.com", "subject": "GovTech Weekly KPIs — SLA: {{$json.sla_rate}}% | Apps: {{$json.total}}", "message": "={{$json.html}}"}},
    {"name": "Slack Summary", "type": "n8n-nodes-base.slack", "parameters": {"channel": "#exec-kpis", "text": "Weekly KPIs: {{$json.total}} applications | SLA compliance: {{$json.sla_rate}}% | See email for full report."}}
  ]
}
Enter fullscreen mode Exit fullscreen mode

What it does: Every Monday 8 AM → parallel Postgres queries (applications + API uptime) → builds HTML email with week-over-week comparison using $getWorkflowStaticData for baseline tracking → emails leadership with BCC → posts summary to #exec-kpis Slack.


The GovTech Compliance Case for Self-Hosting

GovTech SaaS companies face compliance requirements that commercial automation platforms fundamentally cannot meet:

FISMA / FedRAMP: Federal agencies using your platform require an ATO (Authority to Operate). Any third-party service that touches federal data must be within the ATO boundary. Adding Zapier or Make to your integration stack means adding them to your ATO scope — a multi-year, multi-million dollar process. n8n runs inside the existing ATO boundary.

CJIS Security Policy §5.10: Criminal Justice Information Services data has FBI-mandated network controls. Routing workflow triggers through cloud automation platforms violates the non-disclosure and access control requirements. n8n can run air-gapped.

StateRAMP: The state equivalent of FedRAMP requires data residency within the state's authorized infrastructure. Self-hosted n8n on a state-controlled VPC satisfies StateRAMP data residency requirements by design.

FOIA Exemptions: Exemption 5 (deliberative process privilege) and Exemption 3 (statutory protections) information may be waived by disclosure to third parties. Cloud automation platforms that receive FOIA request metadata create disclosure risk that government legal teams cannot accept.

Audit Trail Requirements: FISMA requires a permanent, tamper-evident audit trail. Zapier's 30-day execution history and Make's limited history don't satisfy this requirement. n8n's JSON workflows stored in git provide a permanent, versioned record of every workflow change.

Cost at Scale: A city permitting platform processing 100K applications/year with 5 automation steps each = 500K tasks/month. Zapier Professional caps at 2K tasks, then goes to Enterprise pricing ($600+/month). n8n on a $30/month VPS handles the same load at 1/20th the cost.


Deploy These Workflows

All 5 workflows are available as import-ready JSON at stripeai.gumroad.com — or grab the complete GovTech SaaS automation bundle.

Built with n8n? Questions about the workflows? Drop a comment below.


FlowKit builds ready-to-use n8n automation templates for SaaS teams. These workflows are starting points — customize field names, Postgres schemas, and Slack channels for your stack.

Top comments (0)