The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "nola \u2014 proactive monitor",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 5
}
]
}
},
"id": "schedule-trigger",
"name": "Every 5 Minutes",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [
220,
300
]
},
{
"parameters": {
"jsCode": "// Query all configured Netdata hosts in parallel\nconst hostString = process.env.NETDATA_HOSTS || '';\nif (!hostString) return [{ json: { alerts: ['NETDATA_HOSTS not configured'], isCritical: false } }];\n\nconst hosts = {};\nhostString.split(',').forEach(h => {\n const colonIdx = h.indexOf(':http');\n if (colonIdx > 0) {\n const name = h.slice(0, colonIdx).trim();\n const url = 'http' + h.slice(colonIdx + 1).trim();\n hosts[name] = url;\n }\n});\n\nconst THRESHOLDS = {\n cpu: 85, // %\n ram: 90, // %\n disk: 85, // %\n uptime: 900 // seconds \u2014 flag if uptime < 15min (recently rebooted)\n};\n\nconst alerts = [];\n\nawait Promise.all(Object.entries(hosts).map(async ([name, baseUrl]) => {\n try {\n const resp = await $helpers.httpRequest({\n method: 'GET',\n url: `${baseUrl}/api/v1/allmetrics?format=json`,\n timeout: 8000\n });\n const d = typeof resp === 'string' ? JSON.parse(resp) : resp;\n\n // CPU\n const cpuIdle = d?.['system.cpu']?.dimensions?.idle?.value;\n if (cpuIdle !== undefined) {\n const cpuUsed = 100 - cpuIdle;\n if (cpuUsed > THRESHOLDS.cpu) alerts.push(`\u26a0\ufe0f ${name}: CPU ${cpuUsed.toFixed(1)}% (threshold ${THRESHOLDS.cpu}%)`);\n }\n\n // RAM\n const ramUsed = d?.['system.ram']?.dimensions?.used?.value;\n const ramFree = d?.['system.ram']?.dimensions?.free?.value;\n const ramBuff = (d?.['system.ram']?.dimensions?.buffers?.value || 0) + (d?.['system.ram']?.dimensions?.cached?.value || 0);\n if (ramUsed !== undefined && ramFree !== undefined) {\n const total = ramUsed + ramFree + ramBuff;\n const pct = total > 0 ? (ramUsed / total) * 100 : 0;\n if (pct > THRESHOLDS.ram) alerts.push(`\u26a0\ufe0f ${name}: RAM ${pct.toFixed(1)}% used (threshold ${THRESHOLDS.ram}%)`);\n }\n\n // Uptime\n const uptime = d?.['system.uptime']?.dimensions?.uptime?.value;\n if (uptime !== undefined && uptime < THRESHOLDS.uptime) {\n alerts.push(`\ud83d\udd14 ${name}: recently rebooted (uptime ${Math.round(uptime / 60)} min)`);\n }\n\n } catch (err) {\n alerts.push(`\ud83d\udd34 ${name}: unreachable \u2014 ${err.message}`);\n }\n}));\n\n// UPS check\ntry {\n const net = require('net');\n const UPS_HOST = process.env.UPS_HOST;\n const UPS_PORT = parseInt(process.env.UPS_PORT || '3551');\n if (UPS_HOST) {\n const upsRaw = await new Promise((resolve, reject) => {\n const client = net.createConnection(UPS_PORT, UPS_HOST, () => {\n const cmd = Buffer.alloc(8);\n cmd.writeUInt16BE(6, 0);\n cmd.write('status', 2);\n client.write(cmd);\n });\n let data = Buffer.alloc(0);\n client.on('data', c => { data = Buffer.concat([data, c]); });\n client.on('end', () => resolve(data.toString('ascii')));\n client.on('error', reject);\n setTimeout(() => { client.destroy(); reject(new Error('timeout')); }, 5000);\n });\n const kv = {};\n upsRaw.split('\\n').forEach(line => {\n const i = line.indexOf(':');\n if (i > 0) kv[line.slice(0, i).trim()] = line.slice(i + 1).trim();\n });\n const status = kv['STATUS'] || '';\n const bcharge = parseFloat(kv['BCHARGE'] || '100');\n if (status.includes('ONBATT')) {\n alerts.push(`\ud83d\udd34 UPS ON BATTERY \u2014 battery ${bcharge}%`);\n } else if (bcharge < 30) {\n alerts.push(`\ud83d\udd34 UPS battery critically low: ${bcharge}%`);\n }\n }\n} catch (err) {\n // UPS check failure is not itself an alert \u2014 host may not have UPS\n}\n\nconst isCritical = alerts.some(a =>\n a.includes('ON BATTERY') ||\n a.includes('battery critically low') ||\n a.includes('unreachable')\n);\n\nreturn [{ json: { alerts, isCritical, alertCount: alerts.length } }];"
},
"id": "check-hosts",
"name": "Check All Hosts",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
440,
300
]
},
{
"parameters": {
"conditions": {
"number": [
{
"value1": "={{ $json.alertCount }}",
"operation": "larger",
"value2": 0
}
]
}
},
"id": "if-alerts",
"name": "Any Alerts?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
660,
300
]
},
{
"parameters": {
"jsCode": "// Alert deduplication \u2014 suppress repeated identical alerts within the cooldown window.\n// State persists across executions via $workflow.staticData (n8n built-in per-workflow KV).\n//\n// Cooldown window: ALERT_DEDUP_MINUTES env var (default 30 minutes).\n// Critical alerts bypass dedup \u2014 they always fire.\nconst alerts = $input.first().json.alerts;\nconst isCritical = $input.first().json.isCritical;\nconst alertCount = $input.first().json.alertCount;\n\nconst COOLDOWN_MS = parseInt(process.env.ALERT_DEDUP_MINUTES || '30') * 60 * 1000;\nconst fingerprint = [...alerts].sort().join('|');\nconst state = $workflow.staticData;\nconst now = Date.now();\n\nconst sameFingerprint = state.lastFingerprint === fingerprint;\nconst withinCooldown = state.lastSentAt && (now - state.lastSentAt) < COOLDOWN_MS;\n\nif (!isCritical && sameFingerprint && withinCooldown) {\n // Identical non-critical alerts already sent within the cooldown window \u2014 suppress\n return [];\n}\n\n// New, changed, or critical alerts \u2014 update state and pass through\nstate.lastFingerprint = fingerprint;\nstate.lastSentAt = now;\n\nreturn [{ json: { alerts, isCritical, alertCount } }];"
},
"id": "dedup-check",
"name": "Dedup Check",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
880,
200
]
},
{
"parameters": {
"jsCode": "// Summarize alerts with Ollama (containy.galaxy)\n// Falls back gracefully if Ollama is unavailable.\nconst alerts = $input.first().json.alerts;\nconst isCritical = $input.first().json.isCritical;\nconst alertCount = $input.first().json.alertCount;\n\nconst ollamaUrl = process.env.OLLAMA_URL_CONTAINY || 'http://containy.galaxy:11434';\nconst model = process.env.OLLAMA_MODEL || 'llama3.2:3b';\n\nconst prompt = `You are NOLA, a homelab monitoring assistant. Summarize these infrastructure alerts in 1-2 sentences. Be direct and technical \u2014 provide insight or context, do not just repeat the raw data verbatim.\\n\\nAlerts:\\n${alerts.join('\\n')}`;\n\nlet aiSummary = null;\ntry {\n const resp = await $helpers.httpRequest({\n method: 'POST',\n url: `${ollamaUrl}/api/generate`,\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ model, prompt, stream: false }),\n timeout: 15000\n });\n const data = typeof resp === 'string' ? JSON.parse(resp) : resp;\n aiSummary = data?.response?.trim() || null;\n} catch (err) {\n // Ollama unavailable \u2014 continue without AI summary\n}\n\nreturn [{ json: { alerts, isCritical, alertCount, aiSummary } }];"
},
"id": "ollama-summarize",
"name": "Summarize with Ollama",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1100,
200
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.DISCORD_WEBHOOK_URL }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"content\": \"\ud83d\udd14 **NOLA Proactive Monitor**\\n{{ $json.alerts.join('\\n') }}{{ $json.aiSummary ? '\\n\\n\ud83d\udcac ' + $json.aiSummary : '' }}\"\n}",
"options": {}
},
"id": "post-discord",
"name": "Post Alerts to Discord",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
1320,
200
]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $('Summarize with Ollama').first().json.isCritical }}",
"value2": true
}
]
}
},
"id": "if-critical",
"name": "Is Critical?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
1320,
400
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.vapi.ai/call/phone",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $env.VAPI_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={\n \"assistantId\": \"{{ $env.VAPI_ASSISTANT_ID }}\",\n \"phoneNumberId\": \"{{ $env.VAPI_PHONE_NUMBER_ID }}\",\n \"customer\": { \"number\": \"{{ $env.ALERT_PHONE_NUMBER }}\" },\n \"assistantOverrides\": {\n \"firstMessage\": \"This is NOLA with a critical alert. {{ $('Check All Hosts').first().json.alerts.join('. ') }}\"\n }\n}",
"options": {}
},
"id": "vapi-call",
"name": "VAPI Phone Call",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
1540,
400
]
}
],
"connections": {
"Every 5 Minutes": {
"main": [
[
{
"node": "Check All Hosts",
"type": "main",
"index": 0
}
]
]
},
"Check All Hosts": {
"main": [
[
{
"node": "Any Alerts?",
"type": "main",
"index": 0
}
]
]
},
"Any Alerts?": {
"main": [
[
{
"node": "Dedup Check",
"type": "main",
"index": 0
}
],
[]
]
},
"Dedup Check": {
"main": [
[
{
"node": "Summarize with Ollama",
"type": "main",
"index": 0
}
]
]
},
"Summarize with Ollama": {
"main": [
[
{
"node": "Post Alerts to Discord",
"type": "main",
"index": 0
},
{
"node": "Is Critical?",
"type": "main",
"index": 0
}
]
]
},
"Post Alerts to Discord": {
"main": [
[]
]
},
"Is Critical?": {
"main": [
[
{
"node": "VAPI Phone Call",
"type": "main",
"index": 0
}
],
[]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"tags": [
"nola",
"monitor"
]
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
nola — proactive monitor. Uses httpRequest. Scheduled trigger; 8 nodes.
Source: https://github.com/iamgadgetman/nola/blob/main/workflows/proactive-monitor.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Proactively alert to service endpoint changes and pod/container issues (Pending, Not Ready, Restart spikes) using Prometheus metrics, formatted and sent to Slack.
Tired of being let down by the Google Drive Trigger? Rather not exhaust system resources by polling every minute? Then this workflow is for you!
Triggers at a regular interval or via a webhook request. Solves AWS WAF challenge then makes a request to fetch the product page. Extracts product data from the retrieved HTML page. Compares the curre
🔄 Monitor Container Images from Docker Hub or GHCR.
Automatically monitor billable Kimai projects every weekday morning and receive a formatted HTML email when a project deadline is approaching or its hour budget is running low. If nothing requires att