AutomationFlowsWeb Scraping › Nola — Weekly Health Digest

Nola — Weekly Health Digest

nola — weekly health digest. Uses httpRequest. Scheduled trigger; 4 nodes.

Cron / scheduled trigger★★★★☆ complexity4 nodesHTTP Request
Web Scraping Trigger: Cron / scheduled Nodes: 4 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 weekly health digest",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "cronExpression": "0 14 * * 1"
        }
      },
      "id": "schedule-trigger",
      "name": "Monday 6AM Pacific",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        220,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Collect health summary from all Netdata hosts\nconst hostString = process.env.NETDATA_HOSTS || '';\nconst hosts = {};\nhostString.split(',').forEach(h => {\n  const colonIdx = h.indexOf(':http');\n  if (colonIdx > 0) {\n    hosts[h.slice(0, colonIdx).trim()] = 'http' + h.slice(colonIdx + 1).trim();\n  }\n});\n\nconst lines = ['\ud83d\udcca **NOLA Weekly Health Digest**', ''];\n\nawait Promise.all(Object.entries(hosts).map(async ([name, baseUrl]) => {\n  try {\n    const resp = await $helpers.httpRequest({ method: 'GET', url: `${baseUrl}/api/v1/allmetrics?format=json`, timeout: 10000 });\n    const d = typeof resp === 'string' ? JSON.parse(resp) : resp;\n\n    const cpuIdle = d?.['system.cpu']?.dimensions?.idle?.value;\n    const cpuUsed = cpuIdle !== undefined ? (100 - cpuIdle).toFixed(1) + '%' : 'N/A';\n\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    let ramStr = 'N/A';\n    if (ramUsed !== undefined && ramFree !== undefined) {\n      const total = ramUsed + ramFree + ramBuff;\n      const pct   = total > 0 ? ((ramUsed / total) * 100).toFixed(1) : '0';\n      ramStr = `${pct}%`;\n    }\n\n    const uptime  = d?.['system.uptime']?.dimensions?.uptime?.value;\n    const uptimeStr = uptime !== undefined\n      ? uptime > 86400 ? `${Math.floor(uptime / 86400)}d ${Math.floor((uptime % 86400) / 3600)}h`\n        : `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m`\n      : 'N/A';\n\n    lines.push(`**${name}**: CPU ${cpuUsed} | RAM ${ramStr} | Uptime ${uptimeStr}`);\n  } catch (err) {\n    lines.push(`**${name}**: \u274c unreachable`);\n  }\n}));\n\n// UPS\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 model    = kv['MODEL']    || 'UPS';\n    const status   = kv['STATUS']   || 'UNKNOWN';\n    const bcharge  = kv['BCHARGE']  || '?';\n    const loadpct  = kv['LOADPCT']  || '?';\n    const timeleft = kv['TIMELEFT'] || '?';\n    lines.push('');\n    lines.push(`\ud83d\udd0b **UPS** (${model}): ${status} | Battery ${bcharge} | Load ${loadpct} | Runtime ${timeleft}`);\n  }\n} catch (err) {\n  lines.push(`\ud83d\udd0b **UPS**: check failed \u2014 ${err.message}`);\n}\n\n// Prometheus scraper health\ntry {\n  const promUrl = process.env.PROMETHEUS_URL;\n  if (promUrl) {\n    const resp = await $helpers.httpRequest({ method: 'GET', url: `${promUrl}/api/v1/query?query=up`, timeout: 8000 });\n    const data  = typeof resp === 'string' ? JSON.parse(resp) : resp;\n    const results = data?.data?.result || [];\n    const up   = results.filter(r => r.value?.[1] === '1').length;\n    const down = results.filter(r => r.value?.[1] === '0').length;\n    lines.push('');\n    lines.push(`\ud83d\udce1 **Prometheus scrapers**: ${up} up, ${down} down`);\n    if (down > 0) {\n      results.filter(r => r.value?.[1] === '0').forEach(r => {\n        lines.push(`   \u274c ${r.metric?.instance || r.metric?.job}`);\n      });\n    }\n  }\n} catch (err) {\n  lines.push(`\ud83d\udce1 **Prometheus**: check failed`);\n}\n\nlines.push('');\nlines.push(`_Generated by NOLA \u2014 ${new Date().toUTCString()}_`);\n\nreturn [{ json: { rawDigest: lines.join('\\n') } }];"
      },
      "id": "build-digest",
      "name": "Build Health Digest",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// Ask Ollama (knox.universe) to write a brief narrative for the weekly digest.\n// Falls back gracefully if Ollama is unavailable.\nconst rawDigest = $input.first().json.rawDigest;\n\nconst ollamaUrl = process.env.OLLAMA_URL_KNOX || 'http://knox.universe:11434';\nconst model = process.env.OLLAMA_MODEL || 'llama3.2:3b';\n\nconst prompt = `You are NOLA, a homelab AI butler. Based on this weekly health report, write a single short paragraph (2-3 sentences) with your overall assessment of the lab's health this week. Be conversational but technical. Don't repeat all the numbers \u2014 give your read on whether things look good, concerning, or need attention.\\n\\nReport:\\n${rawDigest}`;\n\nlet narrative = 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: 45000\n  });\n  const data = typeof resp === 'string' ? JSON.parse(resp) : resp;\n  narrative = data?.response?.trim() || null;\n} catch (err) {\n  // Ollama unavailable \u2014 post digest without narrative\n}\n\nconst message = narrative\n  ? `${rawDigest}\\n\\n\ud83d\udcac **NOLA's Take:** ${narrative}`\n  : rawDigest;\n\nreturn [{ json: { message } }];"
      },
      "id": "ollama-narrate",
      "name": "Narrate with Ollama",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.DISCORD_WEBHOOK_URL }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={ \"content\": \"{{ $json.message }}\" }",
        "options": {}
      },
      "id": "post-discord",
      "name": "Post to Discord",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4,
      "position": [
        880,
        300
      ]
    }
  ],
  "connections": {
    "Monday 6AM Pacific": {
      "main": [
        [
          {
            "node": "Build Health Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Health Digest": {
      "main": [
        [
          {
            "node": "Narrate with Ollama",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Narrate with Ollama": {
      "main": [
        [
          {
            "node": "Post to Discord",
            "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 — weekly health digest. Uses httpRequest. Scheduled trigger; 4 nodes.

Source: https://github.com/iamgadgetman/nola/blob/main/workflows/weekly-digest.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

Birthday Automation - Production (Fixed). Uses stopAndError, httpRequest, emailSend, bannerbear. Scheduled trigger; 86 nodes.

Stop And Error, HTTP Request, Email Send +1
Web Scraping

This template runs two scheduled workflows to govern Microsoft Entra ID (Azure AD) guest accounts by detecting stale users via Microsoft Graph, staging deletions in SharePoint with a 72-hour window, n

Microsoft SharePoint, Microsoft Teams, Microsoft Entra +1
Web Scraping

Jira-Allure-Auto-Qa. Uses httpRequest, jira. Scheduled trigger; 68 nodes.

HTTP Request, Jira
Web Scraping

Spotify-Sync-Surrealdb-V1. Uses httpRequest, n8n-nodes-surrealdb, spotify. Scheduled trigger; 62 nodes.

HTTP Request, N8N Nodes Surrealdb, Spotify
Web Scraping

As n8n instances scale, teams often lose track of sub-workflows—who uses them, where they are referenced, and whether they can be safely updated. This leads to inefficiencies like unnecessary copies o

HTTP Request, n8n, N8N Trigger +1