AutomationFlowsWeb Scraping › Monitor [prod] Workflows in Real Time with the N8n Public API Dashboard

Monitor [prod] Workflows in Real Time with the N8n Public API Dashboard

ByLucas Hideki @lucashideki on n8n.io

A real-time monitoring dashboard for your n8n production workflows, accessible directly from the browser via webhook.

Webhook trigger★★★★☆ complexity15 nodesHTTP Request
Web Scraping Trigger: Webhook Nodes: 15 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #13665 — we link there as the canonical source.

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
{
  "nodes": [
    {
      "id": "b049d0bd-c963-4af8-93b4-b264249d06ff",
      "name": "Sticky Note - Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -304,
        -48
      ],
      "parameters": {
        "color": 4,
        "width": 500,
        "height": 560,
        "content": "## \ud83d\udcca n8n Resiliency Dashboard\n\nThis workflow generates a **real-time HTML dashboard** that monitors all your `[PROD]`-tagged workflows.\n\n### What it does\n- Lists all workflows tagged `[PROD]`\n- Fetches the last 50 success and 50 error executions per workflow\n- Calculates success rate, error count and last error timestamp\n- Renders an auto-refreshing HTML dashboard (every 30 seconds)\n- Serves it via Webhook \u2014 just open the URL in your browser\n\n### Setup\n1. Enable the n8n API: **Settings \u2192 n8n API**\n2. Create an **n8n API credential** and connect it to the two HTTP Request nodes\n3. Set your instance URL in the **Config** node\n4. Tag the workflows you want to monitor with `[PROD]`\n5. Activate this workflow and open the Webhook URL\n\n### Webhook URL\n`https://YOUR-N8N-URL/webhook/resiliency-dashboard`"
      },
      "typeVersion": 1
    },
    {
      "id": "4e5b8ef5-396b-464f-85ae-67f1461bfa82",
      "name": "Sticky Note - Config",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        288,
        -80
      ],
      "parameters": {
        "color": 3,
        "width": 260,
        "height": 196,
        "content": "### \u2699\ufe0f Step 1 \u2014 Config\nSet your n8n base URL here.\n\n**Example:**\n`https://your-instance.n8n.cloud`\n\nThis URL is reused by all HTTP Request nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "acfacd6c-7846-48cc-a9a3-1e7201e0f7ef",
      "name": "Sticky Note - Fetch & Filter",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        -80
      ],
      "parameters": {
        "color": 3,
        "width": 370,
        "height": 196,
        "content": "### \ud83d\udd0d Step 2 \u2014 Fetch & Filter\nFetches all workflows via the n8n API, then filters only those tagged `[PROD]`.\n\nTo monitor a workflow, add the tag `[PROD]` to it in n8n Settings."
      },
      "typeVersion": 1
    },
    {
      "id": "eaf0fc85-3cc8-4fbb-b5e5-766bac347fca",
      "name": "Sticky Note - Executions",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        912,
        -80
      ],
      "parameters": {
        "color": 3,
        "width": 316,
        "height": 196,
        "content": "### \ud83d\udce5 Step 3 \u2014 Fetch Executions\nFor each `[PROD]` workflow, fetches the last **50 successes** and **50 errors** in parallel.\n\nBoth run simultaneously via branched connections."
      },
      "typeVersion": 1
    },
    {
      "id": "bda8db2d-3b5a-4a03-9eb6-347d78d69118",
      "name": "Sticky Note - Compare & Render",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1232,
        -80
      ],
      "parameters": {
        "color": 3,
        "width": 628,
        "height": 196,
        "content": "### \ud83d\udcca Step 4 \u2014 Compare & Render\nMerges success + error executions, computes per-workflow stats (success rate, error count, last error timestamp), builds a 7-day error chart and renders the full HTML dashboard, served via the Webhook response node."
      },
      "typeVersion": 1
    },
    {
      "id": "dd3be40c-a892-4397-9fcd-ec9f1e42cb40",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        1376,
        320
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "674276f1-5a50-44ca-8806-038ef9ffefc6",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2048,
        320
      ],
      "parameters": {
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "text/html; charset=utf-8"
              }
            ]
          }
        },
        "respondWith": "text",
        "responseBody": "={{ $json.html }}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "ee08f9c3-ce24-4f4c-a4ce-d50a616674a8",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        256,
        320
      ],
      "parameters": {
        "path": "resiliency-dashboard",
        "options": {},
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "10ac5efd-4434-4f81-8448-9393ac45a36e",
      "name": "GET success",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1152,
        224
      ],
      "parameters": {
        "url": "={{ $('Config').item.json.base_url }}/api/v1/executions?status=success&limit=50&workflowId={{ $json.id }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "n8nApi"
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "4e6d2c87-39d5-4105-aa92-7b5d57529b65",
      "name": "GET error",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1152,
        416
      ],
      "parameters": {
        "url": "={{ $('Config').item.json.base_url }}/api/v1/executions?status=error&limit=50&workflowId={{ $json.id }}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "n8nApi"
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "5bb16dd7-04ba-41df-bfc2-32073319d000",
      "name": "Compare",
      "type": "n8n-nodes-base.code",
      "disabled": true,
      "position": [
        1600,
        320
      ],
      "parameters": {
        "jsCode": "const mergeItems = $input.all();\n\nconst erros = [];\nconst sucessos = [];\n\nmergeItems.forEach(item => {\n  const data = item.json.data;\n  if (!data) return;\n  data.forEach(ex => {\n    if (ex.status === 'error') erros.push(ex);\n    else if (ex.status === 'success') sucessos.push(ex);\n  });\n});\n\nconst workflows = $('Filter: [PROD] Tag').all().map(i => i.json);\n\nconst workflowStats = workflows.map(wf => {\n  const wfErros = erros.filter(e => e.workflowId === wf.id);\n  const wfSucessos = sucessos.filter(e => e.workflowId === wf.id);\n  const total = wfErros.length + wfSucessos.length;\n  const taxaSucesso = total > 0 ? Math.round((wfSucessos.length / total) * 100) : 100;\n  const ultimoErro = wfErros.sort((a, b) => \n    new Date(b.startedAt) - new Date(a.startedAt)\n  )[0];\n\n  return {\n    id: wf.id,\n    name: wf.name,\n    totalErros: wfErros.length,\n    totalSucessos: wfSucessos.length,\n    taxaSucesso,\n    saudavel: wfErros.length === 0,\n    ultimoErro: ultimoErro ? ultimoErro.startedAt : null,\n  };\n});\n\nconst totalMonitorados = workflows.length;\nconst totalSaudaveis = workflowStats.filter(w => w.saudavel).length;\nconst totalComErro = workflowStats.filter(w => !w.saudavel).length;\n\nconst hoje = new Date();\nconst diasLabels = [];\nconst diasErros = [];\n\nfor (let i = 6; i >= 0; i--) {\n  const dia = new Date(hoje);\n  dia.setDate(dia.getDate() - i);\n  const label = dia.toLocaleDateString('en-US', { month: 'short', day: '2-digit' });\n  const count = erros.filter(e => {\n    const d = new Date(e.startedAt);\n    return d.toDateString() === dia.toDateString();\n  }).length;\n  diasLabels.push(label);\n  diasErros.push(count);\n}\n\nreturn [{\n  json: {\n    workflowStats,\n    totalMonitorados,\n    totalSaudaveis,\n    totalComErro,\n    diasLabels,\n    diasErros\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "68365082-a8dd-40b4-8f18-1af48964ac11",
      "name": "HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        1824,
        320
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\n\nconst {\n  workflowStats,\n  totalMonitorados,\n  totalSaudaveis,\n  totalComErro,\n  diasLabels,\n  diasErros\n} = data;\n\nconst generatedAt = new Date().toLocaleString('en-US', { timeZone: 'UTC' });\n\nconst workflowRows = workflowStats.map(wf => {\n  const statusPill = wf.saudavel\n    ? '<span class=\"pill success\">Healthy</span>'\n    : '<span class=\"pill error\">Error</span>';\n  const lastError = wf.ultimoErro\n    ? new Date(wf.ultimoErro).toLocaleString('en-US', { timeZone: 'UTC' })\n    : '\u2014';\n\n  return `\n    <tr>\n      <td>${wf.name}</td>\n      <td>${statusPill}</td>\n      <td class=\"mono\">${wf.taxaSucesso}%</td>\n      <td class=\"mono\">${wf.totalErros}</td>\n      <td class=\"mono\">${wf.totalSucessos}</td>\n      <td class=\"mono\">${lastError}</td>\n    </tr>`;\n}).join('');\n\nconst html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta http-equiv=\"refresh\" content=\"30\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Resiliency Dashboard</title>\n<link href=\"https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@700;800&family=DM+Sans:wght@300;400;500&display=swap\" rel=\"stylesheet\">\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js\"></script>\n<style>\n  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n  :root {\n    --bg: #0a0d14; --surface: #111520; --surface-2: #181d2e;\n    --border: #1e2640; --accent: #4fffb0;\n    --red: #ff6b6b; --amber: #ffb347; --blue: #6b8cff;\n    --text: #e8eaf0; --text-muted: #5a6380; --text-dim: #8b94b2;\n    --font-display: 'Syne', sans-serif; --font-body: 'DM Sans', sans-serif;\n    --font-mono: 'DM Mono', monospace; --radius: 12px; --radius-sm: 7px;\n  }\n  body { background: var(--bg); color: var(--text); font-family: var(--font-body); min-height: 100vh; }\n  body::before {\n    content: ''; position: fixed; inset: 0;\n    background-image: linear-gradient(rgba(79,255,176,0.025) 1px, transparent 1px), linear-gradient(90deg, rgba(79,255,176,0.025) 1px, transparent 1px);\n    background-size: 40px 40px; pointer-events: none; z-index: 0;\n  }\n  .shell { position: relative; z-index: 1; max-width: 1280px; margin: 0 auto; padding: 40px 24px 80px; }\n  .header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 48px; padding-bottom: 24px; border-bottom: 1px solid var(--border); }\n  .badge { display: inline-flex; align-items: center; gap: 6px; font-family: var(--font-mono); font-size: 0.72rem; color: var(--accent); letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 4px; }\n  .badge::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 10px var(--accent); animation: pulse 2s ease-in-out infinite; }\n  @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }\n  h1 { font-family: var(--font-display); font-size: clamp(1.6rem, 3vw, 2.4rem); font-weight: 800; letter-spacing: -0.03em; line-height: 1.1; }\n  .header-right { font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-muted); text-align: right; line-height: 1.9; }\n  .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 32px; }\n  .stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 22px; }\n  .stat-label { font-family: var(--font-mono); font-size: 0.65rem; color: var(--text-muted); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 10px; }\n  .stat-value { font-family: var(--font-display); font-size: 2.2rem; font-weight: 800; line-height: 1; letter-spacing: -0.04em; }\n  .stat-value.green { color: #4fffb0; }\n  .stat-value.red { color: #ff6b6b; }\n  .stat-value.blue { color: #6b8cff; }\n  .stat-sub { font-size: 0.72rem; color: var(--text-muted); margin-top: 6px; font-weight: 300; }\n  .section { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 28px; margin-bottom: 32px; }\n  .section-title { font-family: var(--font-display); font-size: 0.9rem; font-weight: 700; letter-spacing: 0.04em; color: var(--text-dim); text-transform: uppercase; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; }\n  .section-title::before { content: ''; display: block; width: 18px; height: 2px; background: var(--accent); border-radius: 2px; }\n  .chart-wrap { height: 220px; position: relative; }\n  .logs-scroll { overflow-x: auto; }\n  table { width: 100%; border-collapse: collapse; font-size: 0.78rem; }\n  thead th { font-family: var(--font-mono); font-size: 0.62rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-muted); text-align: left; padding: 10px 16px; border-bottom: 1px solid var(--border); white-space: nowrap; background: var(--surface-2); }\n  tbody tr { border-bottom: 1px solid rgba(30,38,64,0.6); transition: background 0.12s; }\n  tbody tr:hover { background: rgba(79,255,176,0.03); }\n  td { padding: 11px 16px; vertical-align: middle; }\n  td.mono { font-family: var(--font-mono); color: var(--text-dim); }\n  .pill { display: inline-flex; align-items: center; gap: 5px; padding: 3px 9px; border-radius: 99px; font-family: var(--font-mono); font-size: 0.65rem; font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; }\n  .pill.success { background: rgba(79,255,176,0.12); color: #4fffb0; }\n  .pill.error { background: rgba(255,107,107,0.12); color: #ff6b6b; }\n  .pill::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }\n  .refresh-btn { font-family: var(--font-mono); font-size: 0.7rem; background: transparent; border: 1px solid var(--border); color: var(--text-muted); padding: 7px 14px; border-radius: var(--radius-sm); cursor: pointer; transition: all 0.15s; }\n  .refresh-btn:hover { border-color: var(--accent); color: var(--accent); }\n</style>\n</head>\n<body>\n<div class=\"shell\">\n  <header class=\"header\">\n    <div>\n      <span class=\"badge\">Live</span>\n      <h1>Resiliency Dashboard</h1>\n      <p style=\"color:var(--text-muted);font-size:0.8rem;font-weight:300;margin-top:6px\">Real-time monitoring of [PROD] workflows</p>\n    </div>\n    <div class=\"header-right\">\n      <div>Generated at</div>\n      <div style=\"color:var(--text-dim)\">${generatedAt}</div>\n      <div style=\"margin-top:8px\"><button class=\"refresh-btn\" onclick=\"location.reload()\">\u21bb Refresh</button></div>\n    </div>\n  </header>\n\n  <div class=\"stats-grid\">\n    <div class=\"stat-card\">\n      <div class=\"stat-label\">Monitored</div>\n      <div class=\"stat-value blue\">${totalMonitorados}</div>\n      <div class=\"stat-sub\">[PROD] Workflows</div>\n    </div>\n    <div class=\"stat-card\">\n      <div class=\"stat-label\">Healthy</div>\n      <div class=\"stat-value green\">${totalSaudaveis}</div>\n      <div class=\"stat-sub\">No recent errors</div>\n    </div>\n    <div class=\"stat-card\">\n      <div class=\"stat-label\">With errors</div>\n      <div class=\"stat-value red\">${totalComErro}</div>\n      <div class=\"stat-sub\">Need attention</div>\n    </div>\n  </div>\n\n  <div class=\"section\">\n    <div class=\"section-title\">Errors per day \u2014 last 7 days</div>\n    <div class=\"chart-wrap\"><canvas id=\"chart\"></canvas></div>\n  </div>\n\n  <div class=\"section\">\n    <div class=\"section-title\">Workflow status</div>\n    <div class=\"logs-scroll\">\n      <table>\n        <thead>\n          <tr>\n            <th>Workflow</th>\n            <th>Status</th>\n            <th>Success rate</th>\n            <th>Errors</th>\n            <th>Successes</th>\n            <th>Last error</th>\n          </tr>\n        </thead>\n        <tbody>${workflowRows}</tbody>\n      </table>\n    </div>\n  </div>\n</div>\n\n<script>\nnew Chart(document.getElementById('chart').getContext('2d'), {\n  type: 'bar',\n  data: {\n    labels: ${JSON.stringify(diasLabels)},\n    datasets: [{\n      label: 'Errors',\n      data: ${JSON.stringify(diasErros)},\n      backgroundColor: 'rgba(255,107,107,0.25)',\n      borderColor: 'rgba(255,107,107,0.8)',\n      borderWidth: 1,\n      borderRadius: 4\n    }]\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    plugins: { legend: { labels: { color: '#5a6380', font: { family: 'DM Mono', size: 11 } } } },\n    scales: {\n      x: { grid: { color: 'rgba(30,38,64,0.6)' }, ticks: { color: '#5a6380', font: { family: 'DM Mono', size: 10 } } },\n      y: { grid: { color: 'rgba(30,38,64,0.6)' }, ticks: { color: '#5a6380', font: { family: 'DM Mono', size: 10 } } }\n    }\n  }\n});\n</script>\n</body>\n</html>`;\n\nreturn [{ json: { html } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "84da998d-421b-4f28-842d-1a93eb6767c3",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "position": [
        480,
        320
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "f505883e-8474-4d45-b9d0-474cb4f8ceac",
              "name": "base_url",
              "type": "string",
              "value": "https://YOUR-N8N-URL-HERE"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "d60eac82-4de8-4621-8af9-ae8c665a184b",
      "name": "API: List Workflows",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        704,
        320
      ],
      "parameters": {
        "url": "={{ $('Config').item.json.base_url }}/api/v1/workflows ",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "n8nApi"
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "0f66b66b-a520-4378-b75d-5737632f35a8",
      "name": "Filter: [PROD] Tag",
      "type": "n8n-nodes-base.code",
      "position": [
        928,
        320
      ],
      "parameters": {
        "jsCode": "const response = $input.first().json;\nconst workflows = response.data;\n\nconst prodWorkflows = workflows.filter(wf => \n  wf.tags && wf.tags.some(tag => tag.name === \"[PROD]\")\n);\n\nreturn prodWorkflows.map(wf => ({\n  json: {\n    id: wf.id,\n    name: wf.name,\n    updatedAt: wf.updatedAt,\n    versionId: wf.versionId,\n    workflow: wf\n  }\n}));"
      },
      "typeVersion": 2
    }
  ],
  "connections": {
    "HTML": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Compare",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "API: List Workflows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compare": {
      "main": [
        [
          {
            "node": "HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GET error": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "GET success": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter: [PROD] Tag": {
      "main": [
        [
          {
            "node": "GET error",
            "type": "main",
            "index": 0
          },
          {
            "node": "GET success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API: List Workflows": {
      "main": [
        [
          {
            "node": "Filter: [PROD] Tag",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

A real-time monitoring dashboard for your n8n production workflows, accessible directly from the browser via webhook.

Source: https://n8n.io/workflows/13665/ — 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

*Tags: AI Agent, MCP Server, n8n API, Monitoring, Debugging, Workflow Analytics, Automation*

HTTP Request
Web Scraping

This n8n workflow automates the process of monitoring inventory levels for Shopify products, ensuring timely updates and efficient stock management. It is designed to alert users when inventory levels

HTTP Request, GraphQL
Web Scraping

Performs HTTP health checks on websites and APIs with automatic health status validation Checks HTTP status codes and analyzes JSON responses for common health indicators Returns detailed status infor

HTTP Request
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