AutomationFlowsWeb Scraping › Nola — Proactive Monitor

Nola — Proactive Monitor

nola — proactive monitor. Uses httpRequest. Scheduled trigger; 8 nodes.

Cron / scheduled trigger★★★★☆ complexity8 nodesHTTP Request
Web Scraping Trigger: Cron / scheduled Nodes: 8 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "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"
  ]
}
Pro

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 →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

Proactively alert to service endpoint changes and pod/container issues (Pending, Not Ready, Restart spikes) using Prometheus metrics, formatted and sent to Slack.

HTTP Request
Web Scraping

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!

HTTP Request, Execute Workflow Trigger
Web Scraping

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

N8N Nodes Capsolver, HTTP Request
Web Scraping

🔄 Monitor Container Images from Docker Hub or GHCR.

HTTP Request
Web Scraping

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

Email Send, HTTP Request