Plant managers and operations directors deal with a brutal reality: equipment failures cost thousands per hour, quality issues mean costly rework, and maintenance teams rely on paper schedules. Most of this is preventable with the right automation.
n8n is the open-source workflow automation tool that connects your MES, ERP, SCADA systems, and communication tools without custom code. Self-hosted means your operational data stays on your infrastructure — no cloud vendor gets access to your production metrics.
Here are 5 production-ready n8n automations for manufacturing and industrial operations. Full workflow JSON included — import directly into n8n.
1. Production Quality Alert System
Problem: Quality issues discovered hours after the defect occurred — entire batches scrapped.
Solution: Webhook-triggered real-time quality monitoring that alerts the right people the moment defect rates exceed thresholds.
{
"name": "Production Quality Alert",
"nodes": [
{
"parameters": { "httpMethod": "POST", "path": "quality-event", "responseMode": "lastNode", "options": {} },
"name": "Quality Event Webhook",
"type": "n8n-nodes-base.webhook",
"position": [250, 300]
},
{
"parameters": {
"jsCode": "const d = $json;\nconst defectRate = (d.scrap_count / d.units_produced) * 100;\nconst severity = defectRate > 5 ? 'CRITICAL' : defectRate > 2 ? 'WARNING' : 'OK';\nreturn [{ json: { ...d, defect_rate: defectRate.toFixed(2), severity, ts: new Date().toISOString() } }];"
},
"name": "Calculate Defect Rate",
"type": "n8n-nodes-base.code",
"position": [450, 300]
},
{
"parameters": {
"conditions": { "string": [{ "value1": "={{ $json.severity }}", "operation": "notEqual", "value2": "OK" }] }
},
"name": "IF Alert Needed",
"type": "n8n-nodes-base.if",
"position": [650, 300]
},
{
"parameters": {
"authentication": "oAuth2",
"channel": "#quality-alerts",
"text": "=:warning: *Quality Alert — {{ $json.severity }}*\nMachine: {{ $json.machine_id }} | Shift: {{ $json.shift }}\nUnits: {{ $json.units_produced }} produced, {{ $json.scrap_count }} scrapped\nDefect Rate: *{{ $json.defect_rate }}%* | Time: {{ $json.ts }}",
"otherOptions": {}
},
"name": "Slack Quality Alert",
"type": "n8n-nodes-base.slack",
"position": [850, 250]
},
{
"parameters": {
"operation": "append",
"sheetId": "YOUR_SHEET_ID",
"range": "QualityLog!A:H",
"options": {},
"dataStartRow": 1
},
"name": "Log to Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [850, 400]
}
],
"connections": {
"Quality Event Webhook": { "main": [[{ "node": "Calculate Defect Rate", "type": "main", "index": 0 }]] },
"Calculate Defect Rate": { "main": [[{ "node": "IF Alert Needed", "type": "main", "index": 0 }]] },
"IF Alert Needed": {
"main": [
[{ "node": "Slack Quality Alert", "type": "main", "index": 0 }, { "node": "Log to Sheets", "type": "main", "index": 0 }],
[{ "node": "Log to Sheets", "type": "main", "index": 0 }]
]
}
}
}
How it works:
- Your MES or SCADA system POSTs to the webhook when a batch completes
- Code node calculates defect rate and assigns severity (>5% = CRITICAL, >2% = WARNING)
- IF node routes alerts only when quality issues exist
- Slack message includes machine, shift, and defect rate for immediate response
- All events (including OK) are logged to Sheets for trend analysis
Setup: Replace YOUR_SHEET_ID with your Google Sheets ID. Configure your MES to POST { machine_id, shift, units_produced, scrap_count } to the webhook URL.
2. Preventive Maintenance Scheduler
Problem: Maintenance runs on paper schedules. Equipment fails between scheduled services because no one checked the calendar.
Solution: Automated daily check that identifies machines due for service and notifies technicians before failures occur.
{
"name": "Preventive Maintenance Scheduler",
"nodes": [
{
"parameters": { "rule": { "interval": [{ "field": "hours", "hoursInterval": 24, "triggerAtHour": 7 }] } },
"name": "Daily 7AM Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [250, 300]
},
{
"parameters": {
"operation": "getAll",
"sheetId": "YOUR_MAINTENANCE_SHEET_ID",
"range": "Schedule!A:F",
"options": { "headerRow": 1 }
},
"name": "Get Maintenance Schedule",
"type": "n8n-nodes-base.googleSheets",
"position": [450, 300]
},
{
"parameters": {
"jsCode": "const today = new Date();\nreturn $input.all().filter(item => {\n const last = new Date(item.json.last_service_date);\n const interval = parseInt(item.json.service_interval_days);\n const dueDate = new Date(last.getTime() + interval * 864e5);\n const daysUntilDue = Math.ceil((dueDate - today) / 864e5);\n item.json.days_until_due = daysUntilDue;\n item.json.due_date = dueDate.toISOString().split('T')[0];\n return daysUntilDue <= 7 && daysUntilDue >= 0;\n});"
},
"name": "Filter Due This Week",
"type": "n8n-nodes-base.code",
"position": [650, 300]
},
{
"parameters": {
"conditions": { "number": [{ "value1": "={{ $input.all().length }}", "operation": "larger", "value2": 0 }] }
},
"name": "IF Any Due",
"type": "n8n-nodes-base.if",
"position": [850, 300]
},
{
"parameters": {
"fromEmail": "maintenance@yourplant.com",
"toEmail": "={{ $json.technician_email }}",
"subject": "=PM Due in {{ $json.days_until_due }} day(s): {{ $json.machine_name }}",
"emailFormat": "text",
"text": "=Preventive maintenance reminder:\n\nMachine: {{ $json.machine_name }} ({{ $json.machine_id }})\nService Type: {{ $json.service_type }}\nDue Date: {{ $json.due_date }} ({{ $json.days_until_due }} days)\nLast Serviced: {{ $json.last_service_date }}\n\nPlease schedule this maintenance before the due date.\n\n— Automated Maintenance System"
},
"name": "Email Technician",
"type": "n8n-nodes-base.gmail",
"position": [1050, 250]
},
{
"parameters": {
"authentication": "oAuth2",
"channel": "#maintenance",
"text": "=:wrench: *Upcoming PM Due: {{ $json.machine_name }}*\nService: {{ $json.service_type }} | Due: {{ $json.due_date }} ({{ $json.days_until_due }}d)\nTechnician: {{ $json.technician_name }}",
"otherOptions": {}
},
"name": "Slack Maintenance Channel",
"type": "n8n-nodes-base.slack",
"position": [1050, 400]
}
],
"connections": {
"Daily 7AM Trigger": { "main": [[{ "node": "Get Maintenance Schedule", "type": "main", "index": 0 }]] },
"Get Maintenance Schedule": { "main": [[{ "node": "Filter Due This Week", "type": "main", "index": 0 }]] },
"Filter Due This Week": { "main": [[{ "node": "IF Any Due", "type": "main", "index": 0 }]] },
"IF Any Due": {
"main": [[{ "node": "Email Technician", "type": "main", "index": 0 }, { "node": "Slack Maintenance Channel", "type": "main", "index": 0 }]]
}
}
}
Google Sheet columns: machine_id | machine_name | service_type | last_service_date | service_interval_days | technician_email | technician_name
Setup: Fill your maintenance schedule in Google Sheets. Run daily — technicians get personalized reminders for their machines.
3. Raw Material Inventory Monitor
Problem: Production stops because a critical material ran out. Procurement only learns about it when the line goes down.
Solution: Hourly inventory check against reorder points — procurement alerted before stockouts happen.
{
"name": "Raw Material Inventory Monitor",
"nodes": [
{
"parameters": { "rule": { "interval": [{ "field": "hours", "hoursInterval": 1 }] } },
"name": "Hourly Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [250, 300]
},
{
"parameters": {
"operation": "getAll",
"sheetId": "YOUR_INVENTORY_SHEET_ID",
"range": "Inventory!A:G",
"options": { "headerRow": 1 }
},
"name": "Get Inventory Levels",
"type": "n8n-nodes-base.googleSheets",
"position": [450, 300]
},
{
"parameters": {
"jsCode": "return $input.all().filter(item => {\n const qty = parseFloat(item.json.current_qty);\n const reorder = parseFloat(item.json.reorder_point);\n item.json.shortage_pct = (((reorder - qty) / reorder) * 100).toFixed(0);\n item.json.urgent = qty <= reorder * 0.5;\n return qty <= reorder;\n});"
},
"name": "Filter Below Reorder Point",
"type": "n8n-nodes-base.code",
"position": [650, 300]
},
{
"parameters": {
"conditions": { "number": [{ "value1": "={{ $input.all().length }}", "operation": "larger", "value2": 0 }] }
},
"name": "IF Stock Low",
"type": "n8n-nodes-base.if",
"position": [850, 300]
},
{
"parameters": {
"authentication": "oAuth2",
"channel": "#procurement",
"text": "=:red_circle: *Low Stock Alert{{ $json.urgent ? ' — URGENT' : '' }}*\n*{{ $json.material_name }}* ({{ $json.material_id }})\nCurrent: {{ $json.current_qty }} {{ $json.unit }} | Reorder Point: {{ $json.reorder_point }} {{ $json.unit }}\nPreferred Supplier: {{ $json.supplier_name }}\n{{ $json.urgent ? ':rotating_light: CRITICAL: Stock at 50% of reorder point — order immediately' : 'Order soon to avoid stockout' }}",
"otherOptions": {}
},
"name": "Slack Procurement Alert",
"type": "n8n-nodes-base.slack",
"position": [1050, 300]
}
],
"connections": {
"Hourly Trigger": { "main": [[{ "node": "Get Inventory Levels", "type": "main", "index": 0 }]] },
"Get Inventory Levels": { "main": [[{ "node": "Filter Below Reorder Point", "type": "main", "index": 0 }]] },
"Filter Below Reorder Point": { "main": [[{ "node": "IF Stock Low", "type": "main", "index": 0 }]] },
"IF Stock Low": { "main": [[{ "node": "Slack Procurement Alert", "type": "main", "index": 0 }]] }
}
}
Sheet columns: material_id | material_name | current_qty | reorder_point | unit | supplier_name | supplier_email
Setup: Update current_qty column manually or via ERP integration. Hourly check alerts #procurement when any material hits its reorder point. Items at 50% of reorder = URGENT flag.
4. Daily Production Report Generator
Problem: Plant managers spend 30-60 minutes each day manually pulling shift data and calculating KPIs for the morning standup.
Solution: Automated 5PM production report — KPIs calculated, formatted HTML email delivered before the manager leaves for the day.
{
"name": "Daily Production Report Generator",
"nodes": [
{
"parameters": { "rule": { "interval": [{ "field": "weekdays", "triggerAtHour": 17, "triggerAtMinute": 0 }] } },
"name": "5PM Weekday Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [250, 300]
},
{
"parameters": {
"operation": "getAll",
"sheetId": "YOUR_PRODUCTION_SHEET_ID",
"range": "ShiftData!A:J",
"options": { "headerRow": 1, "filters": { "conditions": [{ "keyName": "date", "condition": "eq", "keyValue": "={{ $today }}" }] } }
},
"name": "Get Today Shift Data",
"type": "n8n-nodes-base.googleSheets",
"position": [450, 300]
},
{
"parameters": {
"jsCode": "const rows = $input.all();\nconst planned = rows.reduce((s,r) => s + Number(r.json.units_planned), 0);\nconst produced = rows.reduce((s,r) => s + Number(r.json.units_produced), 0);\nconst scrap = rows.reduce((s,r) => s + Number(r.json.scrap_count), 0);\nconst downtime = rows.reduce((s,r) => s + Number(r.json.downtime_minutes), 0);\nconst oee = ((produced / (planned || 1)) * ((produced / (produced + scrap || 1))) * ((480 - downtime) / 480) * 100).toFixed(1);\nconst today = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });\nconst html = `<h2>Daily Production Report — ${today}</h2><table border='1' cellpadding='8' style='border-collapse:collapse'><tr><th>KPI</th><th>Value</th></tr><tr><td>Units Planned</td><td>${planned}</td></tr><tr><td>Units Produced</td><td>${produced}</td></tr><tr><td>Scrap / Rework</td><td>${scrap} (${((scrap/(produced+scrap||1))*100).toFixed(1)}%)</td></tr><tr><td>Downtime</td><td>${downtime} min</td></tr><tr><td>OEE</td><td><b>${oee}%</b></td></tr></table><p><i>Automated report — reply with corrections.</i></p>`;\nreturn [{ json: { html, oee, produced, planned, scrap, downtime, today } }];"
},
"name": "Calculate Production KPIs",
"type": "n8n-nodes-base.code",
"position": [650, 300]
},
{
"parameters": {
"fromEmail": "reports@yourplant.com",
"toEmail": "plant.manager@yourcompany.com",
"subject": "=Daily Production Report — {{ $json.today }} | OEE {{ $json.oee }}%",
"emailFormat": "html",
"html": "={{ $json.html }}"
},
"name": "Email Plant Manager",
"type": "n8n-nodes-base.gmail",
"position": [850, 300]
}
],
"connections": {
"5PM Weekday Trigger": { "main": [[{ "node": "Get Today Shift Data", "type": "main", "index": 0 }]] },
"Get Today Shift Data": { "main": [[{ "node": "Calculate Production KPIs", "type": "main", "index": 0 }]] },
"Calculate Production KPIs": { "main": [[{ "node": "Email Plant Manager", "type": "main", "index": 0 }]] }
}
}
Sheet columns: date | shift | machine_id | units_planned | units_produced | scrap_count | downtime_minutes
What you get: OEE, quality rate, daily totals — delivered at 5PM every weekday without manual calculation.
5. Equipment Downtime Alert & Escalation
Problem: When a machine goes down, it takes 20 minutes for the right people to find out. Every minute of unplanned downtime costs money.
Solution: Webhook-triggered downtime alert with automatic escalation to management if the issue isn't resolved within 30 minutes.
{
"name": "Equipment Downtime Alert & Escalation",
"nodes": [
{
"parameters": { "httpMethod": "POST", "path": "downtime-event", "responseMode": "lastNode", "options": {} },
"name": "Downtime Event Webhook",
"type": "n8n-nodes-base.webhook",
"position": [250, 300]
},
{
"parameters": {
"jsCode": "const d = $json;\nconst severity = d.reason_code === 'BREAKDOWN' ? 'CRITICAL' : d.reason_code === 'CHANGEOVER' ? 'PLANNED' : 'UNPLANNED';\nconst cost_per_min = d.cost_per_minute || 150;\nreturn [{ json: { ...d, severity, cost_per_min, ts: new Date().toISOString(), incident_id: 'DT-' + Date.now() } }];"
},
"name": "Classify Downtime",
"type": "n8n-nodes-base.code",
"position": [450, 300]
},
{
"parameters": {
"authentication": "oAuth2",
"channel": "#maintenance",
"text": "=:rotating_light: *Downtime Alert — {{ $json.severity }}*\nMachine: {{ $json.machine_id }} | Line: {{ $json.production_line }}\nReason: {{ $json.reason_description }} ({{ $json.reason_code }})\nEst. Cost: ${{ $json.cost_per_min }}/min | Started: {{ $json.ts }}\nIncident: {{ $json.incident_id }}",
"otherOptions": {}
},
"name": "Alert Maintenance Team",
"type": "n8n-nodes-base.slack",
"position": [650, 250]
},
{
"parameters": {
"conditions": { "string": [{ "value1": "={{ $json.severity }}", "operation": "notEqual", "value2": "PLANNED" }] }
},
"name": "IF Unplanned",
"type": "n8n-nodes-base.if",
"position": [650, 400]
},
{
"parameters": { "amount": 30, "unit": "minutes" },
"name": "Wait 30 Minutes",
"type": "n8n-nodes-base.wait",
"position": [850, 400]
},
{
"parameters": {
"authentication": "oAuth2",
"channel": "#management",
"text": "=:sos: *30-Min Escalation — {{ $json.machine_id }} Still Down*\nIncident {{ $json.incident_id }} — Machine down for 30+ minutes\nLine: {{ $json.production_line }} | Reason: {{ $json.reason_description }}\nEst. loss so far: ${{ ($json.cost_per_min * 30).toLocaleString() }}\nMaintenance team alerted at start — please follow up.",
"otherOptions": {}
},
"name": "Escalate to Management",
"type": "n8n-nodes-base.slack",
"position": [1050, 400]
},
{
"parameters": {
"operation": "append",
"sheetId": "YOUR_DOWNTIME_SHEET_ID",
"range": "DowntimeLog!A:H",
"options": {}
},
"name": "Log Downtime Event",
"type": "n8n-nodes-base.googleSheets",
"position": [850, 250]
}
],
"connections": {
"Downtime Event Webhook": { "main": [[{ "node": "Classify Downtime", "type": "main", "index": 0 }]] },
"Classify Downtime": {
"main": [[{ "node": "Alert Maintenance Team", "type": "main", "index": 0 }, { "node": "IF Unplanned", "type": "main", "index": 0 }, { "node": "Log Downtime Event", "type": "main", "index": 0 }]]
},
"IF Unplanned": { "main": [[{ "node": "Wait 30 Minutes", "type": "main", "index": 0 }]] },
"Wait 30 Minutes": { "main": [[{ "node": "Escalate to Management", "type": "main", "index": 0 }]] }
}
}
Payload from SCADA/MES: { machine_id, production_line, reason_code, reason_description, cost_per_minute }
How it works:
- SCADA or MES POSTs to the webhook when a machine stops
- Immediate alert to #maintenance with machine, line, reason, and cost per minute
- If downtime is unplanned: waits 30 minutes, then escalates to #management with total loss estimate
- Every event logged to Sheets for downtime analysis and reporting
Why Self-Hosted n8n for Manufacturing?
Traditional cloud automation tools like Zapier and Make.com weren't built for industrial use cases:
| Need | Zapier/Make | n8n (self-hosted) |
|---|---|---|
| OT/SCADA data on internal network | Requires cloud exposure | Runs on-premises, connects directly |
| Custom KPI calculations | Limited | Full JavaScript in Code node |
| Unlimited workflow runs | Per-task billing ($) | Unlimited at flat cost |
| Your data on your servers | Cloud-stored | On your infrastructure |
| Webhook from internal systems | External URL required | Internal URL works fine |
n8n runs on a single VM or Docker container. It connects directly to your internal systems — no need to expose your MES or ERP to the internet.
Ready-to-Use Templates
The 5 workflows above are illustrative — the real templates in the FlowKit store include:
- Full production-ready JSON (import directly into n8n)
- Pre-built setup guides with field mapping instructions
- Multi-shift variants (day/evening/night shift handling)
- ERP connector patterns (SAP, Oracle NetSuite, Epicor-style REST APIs)
Browse the full template library: stripeai.gumroad.com
Templates most relevant to manufacturing:
- Daily Report Generator ($19) — production metrics, OEE, shift summaries
- Appointment Reminder ($15) — adapt for maintenance scheduling and shift notifications
- Webhook to Database ($12) — log quality events, downtime data, sensor readings to any database
- AI Customer Support Bot ($29) — adapt for internal IT/maintenance help desk triage
Getting Started
-
Install n8n — Docker:
docker run -it --rm --name n8n -p 5678:5678 docker.n8n.io/n8nio/n8n - Connect your Google Sheets — Credentials → Add New → Google Sheets OAuth2
- Connect Slack — Credentials → Add New → Slack OAuth2 API
- Import any workflow — Copy JSON → n8n → Import from Clipboard
- Customize thresholds — Edit the Code node to match your production targets
Questions about adapting these for your specific MES or ERP? Drop a comment below — I'm happy to help.
FlowKit publishes practical n8n automation templates for teams who want to move fast without building from scratch. Browse all 15 templates at stripeai.gumroad.com.
Top comments (0)