Alex KanePlant managers and operations directors deal with a brutal reality: equipment failures cost thousands...
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.
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:
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.
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.
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.
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.
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:
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.
The 5 workflows above are illustrative — the real templates in the FlowKit store include:
Browse the full template library: stripeai.gumroad.com
Templates most relevant to manufacturing:
docker run -it --rm --name n8n -p 5678:5678 docker.n8n.io/n8nio/n8n
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.