Public sector IT teams face a unique paradox: massive operational complexity, chronic understaffing, and zero tolerance for errors. A missed permit deadline means a constituent waits months. A late emergency alert can have serious consequences.
n8n — the open-source workflow automation platform — is increasingly adopted by government agencies and municipalities because it can be self-hosted (data never leaves your infrastructure), it's auditable (JSON-based workflows are human-readable), and it costs $0 on existing government servers.
Here are 5 automations that address real public-sector pain points, with complete workflow JSON you can import directly.
1. Citizen Inquiry Auto-Triage
The problem: City hall inboxes receive hundreds of mixed inquiries daily — permit questions, service requests, complaints, general info. Staff manually read and route each one.
The solution: Automatically classify and route emails by department.
{
"name": "Citizen Inquiry Auto-Triage",
"nodes": [
{
"parameters": {
"operation": "getAll",
"filters": {"readStatus": "unread"},
"options": {"maxResults": 25}
},
"name": "Gmail Trigger",
"type": "n8n-nodes-base.gmailTrigger",
"typeVersion": 1,
"position": [240, 300]
},
{
"parameters": {
"jsCode": "const email = $json;\nconst combined = ((email.subject || '') + ' ' + (email.textPlain || '')).toLowerCase();\n\nlet department = 'general';\nlet priority = 'normal';\n\nif (/permit|build|construct|zoning/.test(combined)) department = 'permits';\nelse if (/road|pothole|streetlight|sidewalk/.test(combined)) department = 'public_works';\nelse if (/water|sewage|waste|trash/.test(combined)) department = 'utilities';\nelse if (/emergency|urgent|danger|hazard/.test(combined)) { department = 'emergency'; priority = 'high'; }\nelse if (/complain|feedback|suggest/.test(combined)) department = 'constituent_services';\n\nif (/urgent|immediate|asap|critical/.test(combined)) priority = 'high';\n\nreturn { ...email, department, priority, routedAt: new Date().toISOString() };"
},
"name": "Classify Inquiry",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [460, 300]
},
{
"parameters": {
"operation": "appendOrUpdate",
"documentId": "YOUR_SHEETS_ID",
"sheetName": "Inquiries",
"columns": {"mappingMode": "autoMapInputData"}
},
"name": "Log to Sheets",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [680, 300]
},
{
"parameters": {
"webhookUri": "https://hooks.slack.com/services/YOUR/WEBHOOK/HERE",
"text": "=📧 New {{$json.priority === 'high' ? '🚨 HIGH PRIORITY ' : ''}}Inquiry → *{{$json.department.toUpperCase().replace('_',' ')}}*\nFrom: {{$json.from}}\nSubject: {{$json.subject}}\nTime: {{$json.routedAt}}"
},
"name": "Slack Department Alert",
"type": "n8n-nodes-base.slack",
"typeVersion": 1,
"position": [900, 300]
}
],
"connections": {
"Gmail Trigger": {"main": [[{"node": "Classify Inquiry", "type": "main", "index": 0}]]},
"Classify Inquiry": {"main": [[{"node": "Log to Sheets", "type": "main", "index": 0}]]},
"Log to Sheets": {"main": [[{"node": "Slack Department Alert", "type": "main", "index": 0}]]}
}
}
Setup (5 min): Connect Gmail Trigger to your public inquiry inbox. Replace YOUR_SHEETS_ID and Slack webhook. Activate.
Pro tips:
- Add AI classification (Claude Haiku or GPT-4o-mini) for nuanced routing beyond keywords
- Add an auto-acknowledgment email: "Your inquiry has been received and routed. Expected response: 3–5 business days."
- Route to a Jira Service Management or Freshdesk ticket instead of Slack for formal tracking
2. FOIA / Open Records Request Deadline Tracker
The problem: FOIA and open records requests have legal deadlines (often 10–20 business days). Missing them triggers mandatory fee waivers, legal complaints, and political fallout.
The solution: Automated deadline tracking with escalating alerts at 7, 3, and 1 days.
{
"name": "FOIA Request Deadline Tracker",
"nodes": [
{
"parameters": {
"rule": {"interval": [{"field": "cronExpression", "expression": "0 8 * * 1-5"}]}
},
"name": "Weekdays 8AM",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [240, 300]
},
{
"parameters": {
"operation": "getAll",
"documentId": "YOUR_SHEETS_ID",
"sheetName": "FOIA_Requests"
},
"name": "Fetch Open Requests",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [460, 300]
},
{
"parameters": {
"jsCode": "const today = new Date();\nconst requests = $input.all().map(i => i.json);\nconst alerts = [];\n\nfor (const req of requests) {\n if (req.status === 'COMPLETED') continue;\n const deadline = new Date(req.deadline_date);\n const daysLeft = Math.ceil((deadline - today) / (1000 * 60 * 60 * 24));\n \n let alertLevel = null;\n if (daysLeft <= 1) alertLevel = 'DUE TODAY / OVERDUE';\n else if (daysLeft <= 3) alertLevel = 'CRITICAL — 3 days';\n else if (daysLeft <= 7) alertLevel = 'WARNING — 7 days';\n \n if (alertLevel) {\n alerts.push({ ...req, days_left: daysLeft, alert_level: alertLevel });\n }\n}\n\nreturn alerts.map(a => ({ json: a }));"
},
"name": "Check Deadlines",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [680, 300]
},
{
"parameters": {
"toEmail": "={{$json.assigned_to_email}}",
"subject": "=⚠️ FOIA {{$json.request_id}} — {{$json.alert_level}}",
"message": "=FOIA/Open Records Request Deadline Alert\n\nRequest ID: {{$json.request_id}}\nRequester: {{$json.requester_name}}\nDescription: {{$json.description}}\nDeadline: {{$json.deadline_date}}\nDays Remaining: {{$json.days_left}}\nStatus: {{$json.alert_level}}\n\nPlease update the tracking sheet or escalate immediately if unable to fulfill by deadline.",
"options": {}
},
"name": "Alert Assigned Staff",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2,
"position": [900, 300]
}
],
"connections": {
"Weekdays 8AM": {"main": [[{"node": "Fetch Open Requests", "type": "main", "index": 0}]]},
"Fetch Open Requests": {"main": [[{"node": "Check Deadlines", "type": "main", "index": 0}]]},
"Check Deadlines": {"main": [[{"node": "Alert Assigned Staff", "type": "main", "index": 0}]]}
}
}
Sheets columns needed: request_id, requester_name, description, assigned_to, assigned_to_email, deadline_date, status
3. Permit & Business License Renewal Alert System
The problem: Thousands of business licenses and contractor certifications expire each year. Manual tracking leads to complaints and fines when licenses lapse unnoticed.
The solution: Automated 60/30/14/7-day renewal reminders sent directly to license holders.
{
"name": "Permit & License Renewal Alert",
"nodes": [
{
"parameters": {
"rule": {"interval": [{"field": "cronExpression", "expression": "0 7 * * *"}]}
},
"name": "Daily 7AM",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [240, 300]
},
{
"parameters": {
"operation": "getAll",
"documentId": "YOUR_SHEETS_ID",
"sheetName": "Licenses"
},
"name": "Fetch Active Licenses",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [460, 300]
},
{
"parameters": {
"jsCode": "const today = new Date();\nconst licenses = $input.all().map(i => i.json);\nconst alerts = [];\n\nfor (const lic of licenses) {\n if (lic.status !== 'ACTIVE') continue;\n const exp = new Date(lic.expiration_date);\n const days = Math.ceil((exp - today) / (1000 * 60 * 60 * 24));\n \n if ([60, 30, 14, 7].includes(days)) {\n alerts.push({\n ...lic,\n days_remaining: days,\n renewal_link: lic.renewal_url || 'https://permits.yourcity.gov/renew'\n });\n }\n}\n\nreturn alerts.map(a => ({ json: a }));"
},
"name": "Find Expiring Licenses",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [680, 300]
},
{
"parameters": {
"toEmail": "={{$json.holder_email}}",
"subject": "=Action Required: {{$json.license_type}} Renewal — {{$json.days_remaining}} Days Remaining",
"message": "=Dear {{$json.business_name}},\n\nYour {{$json.license_type}} (License #{{$json.license_id}}) expires on {{$json.expiration_date}}.\n\nYou have {{$json.days_remaining}} days remaining to renew.\n\nRenew online: {{$json.renewal_link}}\n\nCity of [Your City] — Department of Licensing & Inspections",
"options": {}
},
"name": "Send Renewal Notice",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2,
"position": [900, 300]
}
],
"connections": {
"Daily 7AM": {"main": [[{"node": "Fetch Active Licenses", "type": "main", "index": 0}]]},
"Fetch Active Licenses": {"main": [[{"node": "Find Expiring Licenses", "type": "main", "index": 0}]]},
"Find Expiring Licenses": {"main": [[{"node": "Send Renewal Notice", "type": "main", "index": 0}]]}
}
}
Sheets columns needed: license_id, business_name, license_type, holder_email, expiration_date, renewal_url, status
4. Emergency & Public Safety Alert Escalation
The problem: When an incident is reported — road hazard, utility outage, public safety event — the right people need to know in minutes, not after a chain of phone calls.
The solution: Webhook-triggered severity classification and instant escalation.
{
"name": "Emergency Alert Escalation",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "incident-report",
"responseMode": "responseNode"
},
"name": "Incident Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [240, 300]
},
{
"parameters": {
"jsCode": "const report = $json.body;\nconst desc = (report.description || '').toLowerCase();\n\nlet severity = 'LOW';\nif (/injury|accident|collapse|fire|gas.?leak|flood/.test(desc)) severity = 'CRITICAL';\nelse if (/road.?closed|power.?out|water.?main|traffic.?signal/.test(desc)) severity = 'HIGH';\nelse if (/pothole|streetlight|graffiti/.test(desc)) severity = 'MEDIUM';\n\nreturn { ...report, severity, incident_id: 'INC-' + Date.now(), received_at: new Date().toISOString() };"
},
"name": "Classify Severity",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [460, 300]
},
{
"parameters": {
"conditions": {"conditions": [{"leftValue": "={{$json.severity}}", "rightValue": "CRITICAL", "operator": {"type": "string", "operation": "equals"}}]}
},
"name": "IF Critical",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [680, 300]
},
{
"parameters": {
"webhookUri": "https://hooks.slack.com/services/YOUR/EMERGENCY/WEBHOOK",
"text": "=🚨 *CRITICAL INCIDENT* — ID: {{$json.incident_id}}\nType: {{$json.incident_type}}\nLocation: {{$json.location}}\nDetails: {{$json.description}}\n@channel — Immediate response required"
},
"name": "Slack Emergency Channel",
"type": "n8n-nodes-base.slack",
"typeVersion": 1,
"position": [900, 200]
},
{
"parameters": {
"webhookUri": "https://hooks.slack.com/services/YOUR/OPS/WEBHOOK",
"text": "=⚠️ Incident [{{$json.severity}}] — ID: {{$json.incident_id}}\nLocation: {{$json.location}}\nDetails: {{$json.description}}"
},
"name": "Slack Ops Channel",
"type": "n8n-nodes-base.slack",
"typeVersion": 1,
"position": [900, 400]
}
],
"connections": {
"Incident Webhook": {"main": [[{"node": "Classify Severity", "type": "main", "index": 0}]]},
"Classify Severity": {"main": [[{"node": "IF Critical", "type": "main", "index": 0}]]},
"IF Critical": {"main": [[{"node": "Slack Emergency Channel", "type": "main", "index": 0}], [{"node": "Slack Ops Channel", "type": "main", "index": 0}]]}
}
}
Add an auto-acknowledgment Respond to Webhook node (HTTP 200 with an incident ID) so the reporting person knows it was received.
5. Weekly Government Operations Dashboard
The problem: Department heads need a unified operations view, but data lives in different systems. Compiling a weekly report manually takes hours.
The solution: Automated Monday morning briefing assembled from your tracking sheets.
{
"name": "Weekly Government Ops Dashboard",
"nodes": [
{
"parameters": {
"rule": {"interval": [{"field": "cronExpression", "expression": "0 8 * * 1"}]}
},
"name": "Monday 8AM",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [240, 300]
},
{
"parameters": {
"operation": "getAll",
"documentId": "YOUR_SHEETS_ID",
"sheetName": "Weekly_Metrics"
},
"name": "Fetch KPIs",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4,
"position": [460, 300]
},
{
"parameters": {
"jsCode": "const rows = $input.all().map(i => i.json);\nconst latest = rows[rows.length - 1];\nconst prev = rows[rows.length - 2] || {};\n\nconst pctChange = (curr, prev) => {\n if (!prev || !curr) return 'N/A';\n const p = ((curr - prev) / prev * 100).toFixed(1);\n return p > 0 ? `▲${p}%` : `▼${Math.abs(p)}%`;\n};\n\nreturn [{ json: {\n permits_issued: latest.permits_issued,\n permits_change: pctChange(+latest.permits_issued, +prev.permits_issued),\n service_requests: latest.service_requests,\n requests_resolved: latest.requests_resolved,\n avg_resolution_days: latest.avg_resolution_days,\n foia_pending: latest.foia_pending,\n report_week: latest.week_ending\n} }];"
},
"name": "Compute KPIs",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [680, 300]
},
{
"parameters": {
"toEmail": "city-manager@yourcity.gov",
"subject": "=Weekly Ops Dashboard — Week Ending {{$json.report_week}}",
"message": "=<html><body style='font-family:sans-serif;max-width:600px'><h2>Weekly Operations Dashboard</h2><p><strong>Week Ending:</strong> {{$json.report_week}}</p><table style='width:100%;border-collapse:collapse'><tr style='background:#f0f4f8'><th style='padding:8px;text-align:left'>Metric</th><th style='padding:8px'>Value</th><th style='padding:8px'>WoW</th></tr><tr><td style='padding:8px'>Permits Issued</td><td style='padding:8px;text-align:center'>{{$json.permits_issued}}</td><td style='padding:8px;text-align:center'>{{$json.permits_change}}</td></tr><tr style='background:#f9f9f9'><td style='padding:8px'>Service Requests</td><td style='padding:8px;text-align:center'>{{$json.service_requests}}</td><td></td></tr><tr><td style='padding:8px'>Requests Resolved</td><td style='padding:8px;text-align:center'>{{$json.requests_resolved}}</td><td></td></tr><tr style='background:#f9f9f9'><td style='padding:8px'>Avg Resolution Time</td><td style='padding:8px;text-align:center'>{{$json.avg_resolution_days}} days</td><td></td></tr><tr><td style='padding:8px'>FOIA Requests Pending</td><td style='padding:8px;text-align:center'>{{$json.foia_pending}}</td><td></td></tr></table><p style='color:#666;font-size:12px'>Automated by n8n — self-hosted on government infrastructure.</p></body></html>",
"options": {"appendAttribution": false}
},
"name": "Email Weekly Report",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2,
"position": [900, 300]
}
],
"connections": {
"Monday 8AM": {"main": [[{"node": "Fetch KPIs", "type": "main", "index": 0}]]},
"Fetch KPIs": {"main": [[{"node": "Compute KPIs", "type": "main", "index": 0}]]},
"Compute KPIs": {"main": [[{"node": "Email Weekly Report", "type": "main", "index": 0}]]}
}
}
Sheets columns: week_ending, permits_issued, service_requests, requests_resolved, avg_resolution_days, foia_pending
Why n8n for Government?
Data sovereignty is non-negotiable. Most government data policies require that citizen data, FOIA details, and permit records stay within agency infrastructure. n8n self-hosted on your servers means zero data leaves your network — unlike Zapier or Make.com.
Audit trail by default. Every n8n workflow is stored as a human-readable JSON file. Every execution logs to an internal database. Exactly what government auditors require.
No per-workflow pricing. Government agencies need hundreds of automations across departments. n8n's server-based pricing (or free self-hosted) means no per-task charges that scale with volume.
Getting Started
-
Self-hosted:
npx n8nor Docker on your government server - Import any workflow: Copy JSON → n8n dashboard → Import from JSON
-
Replace placeholders:
YOUR_SHEETS_ID, Slack webhooks, email addresses - Activate
FlowKit n8n Template Store
Want pre-built, tested workflows for your team? FlowKit at stripeai.gumroad.com has 10+ ready-to-import n8n templates:
- 📋 Lead Capture to CRM ($19) — constituent inquiry routing with Google Sheets
- 📨 Email Auto-Responder ($15) — automated acknowledgment with smart routing
- 📅 Appointment Reminder ($15) — multi-channel reminder sequences
- 📊 Daily Report Generator ($19) — automated KPI briefings for managers
Each includes full workflow JSON + setup guide, works on any n8n instance.
Drop a comment with your use case — happy to help customize these for your specific government workflow.
Top comments (0)