AutomationFlowsSlack & Telegram › Report Daily Workflow Sla Metrics with the N8n Api, Data Tables and Telegram

Report Daily Workflow Sla Metrics with the N8n Api, Data Tables and Telegram

BySerhii Bondarenko @serhiilabs on n8n.io

This workflow runs daily, queries the n8n API for workflows and recent executions, calculates per-workflow success rate, error count and p95 runtime over a rolling window, detects stalled schedules, stores a snapshot in an n8n Data Table, and sends a change-aware fleet health…

Cron / scheduled trigger★★★★☆ complexity25 nodesn8nData TableTelegram
Slack & Telegram Trigger: Cron / scheduled Nodes: 25 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #16483 — 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
{
  "name": "Report daily workflow SLA compliance to Telegram via the n8n API and Data Tables",
  "tags": [],
  "nodes": [
    {
      "id": "s-main",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        112
      ],
      "parameters": {
        "width": 480,
        "height": 1042,
        "content": "## Daily workflow SLA report\n\nSends one daily Telegram message that checks every workflow on your n8n instance against a success-rate and a p95 runtime target, then reports which workflows are breaching SLA and which have gone silent. A [Data Table](https://docs.n8n.io/data/data-tables/) keeps history, so each run also reports new breaches and recoveries since the last run.\n\n**Who it's for:** teams and freelancers running more than a handful of automations on self-hosted n8n who want a daily SLA digest instead of hearing about a silent failure from a client first.\n\n### How it works\n* A daily schedule starts the run.\n* The n8n node fetches every workflow and the recent executions.\n* A Code node computes per-workflow success rate, error count and p95 duration over a rolling window.\n* Each workflow is checked against the success and p95 targets; a stalled schedule counts as an availability breach.\n* The previous snapshot gives the SLA transitions: new breaches and recoveries.\n* The snapshot is saved back and the report goes to Telegram.\n\n### How to set up\n1. Set the hour in the Schedule Trigger.\n2. Set the SLA targets and the rolling window in the Config node.\n3. Work through the three grey setup notes below the flow: the n8n API credential, the Data Table, the Telegram chat.\n4. Edit the report layout in the Compose message node.\n5. Run once manually to seed the history and check the message, then activate.\n\n### Requirements\n* n8n **v1.113+** with Data Tables enabled (self-hosted: set `N8N_ENABLED_MODULES=data-table`)\n* [n8n API key](https://docs.n8n.io/api/authentication/) (Settings > n8n API), scopes `workflow:list` and `execution:list`\n* [Telegram bot credential](https://docs.n8n.io/integrations/builtin/credentials/telegram/)\n\n### Need help?\nAsk in the [n8n Forum](https://community.n8n.io/)!"
      },
      "typeVersion": 1
    },
    {
      "id": "s-sec1",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        528,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 600,
        "height": 542,
        "content": "## 1. Schedule and thresholds\n[Read more about the Schedule Trigger](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.scheduletrigger/)\n\nRuns daily, then the Config node holds the rolling window, the SLA targets and the alert thresholds in one place."
      },
      "typeVersion": 1
    },
    {
      "id": "s-sec2",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1168,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 600,
        "height": 542,
        "content": "## 2. Collect from the n8n API\n[Read more about the n8n node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.n8n/)\n\nFetches every workflow and the recent executions in two parallel calls, then merges both branches so the next node runs once."
      },
      "typeVersion": 1
    },
    {
      "id": "s-sec3",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1808,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 880,
        "height": 542,
        "content": "## 3. Compute and evaluate SLA\n[Read more about the Code node](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/)\n\nComputes per-workflow success rate, errors and p95, then checks each workflow against the SLA targets and flags stalled schedules."
      },
      "typeVersion": 1
    },
    {
      "id": "s-sec4",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2720,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 880,
        "height": 542,
        "content": "## 4. Compare and store history\n[Read more about Data Tables](https://docs.n8n.io/data/data-tables/)\n\nDiffs the SLA state against the previous snapshot to find new breaches and recoveries, then saves the current snapshot back to the Data Table."
      },
      "typeVersion": 1
    },
    {
      "id": "s-sec5",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3648,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 880,
        "height": 542,
        "content": "## 5. Build and send the report\n[Read more about the Telegram node](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.telegram/)\n\nPrepares the figures, composes the message in a Set node you can edit, and sends it to Telegram."
      },
      "typeVersion": 1
    },
    {
      "id": "s-cfg",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        528,
        704
      ],
      "parameters": {
        "color": 7,
        "width": 600,
        "height": 440,
        "content": "**Config fields - what each one tunes**\nThe Config node centralises the SLA targets, the rolling window and the alert thresholds. Defaults in brackets:\n\n- `windowHours` (24) - rolling lookback in hours. Success rate, error count and p95 are computed over this window.\n- `targetSuccessRate` (95) - SLA target, in %. A workflow with runs below this rate over the window is breaching SLA.\n- `targetP95Seconds` (30) - SLA target, in seconds. A workflow whose p95 runtime exceeds this is breaching SLA.\n- `degradationPct` (50) - slowdown trend alert, in %. Flags a workflow when its p95 grows by more than this vs the previous snapshot.\n- `successDropPp` (10) - success-rate trend alert, in percentage points. Flags a workflow when its success rate falls by more than this vs the last run.\n- `scheduleBuffer` (1.5) - stalled-schedule grace factor. A schedule is overdue once it has not run for its interval x this factor (daily x 1.5 = 36h); a stall counts as an availability breach.\n- `executionLimit` (250) - max executions fetched from the API per run. If hit, the window is truncated and the report warns you to raise it."
      },
      "typeVersion": 1
    },
    {
      "id": "s-api",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1168,
        704
      ],
      "parameters": {
        "color": 7,
        "width": 600,
        "height": 240,
        "content": "**Setup - n8n API credential**\n1. Settings > n8n API > create an [API key](https://docs.n8n.io/api/authentication/) with scopes `workflow:list` and `execution:list`.\n2. Save it as an n8n API credential and select it in both \"Get ...\" nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "s-dt",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2720,
        704
      ],
      "parameters": {
        "color": 7,
        "width": 880,
        "height": 240,
        "content": "**Setup - Data Table**\nCreate a [Data Table](https://docs.n8n.io/data/data-tables/) named `sla_history` with columns:\n- `runAt` - string\n- `metrics` - string\n\nSelect it in both Data Table nodes."
      },
      "typeVersion": 1
    },
    {
      "id": "s-tg",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3648,
        704
      ],
      "parameters": {
        "color": 7,
        "width": 880,
        "height": 240,
        "content": "**Setup - Telegram**\nSet your [Telegram credential](https://docs.n8n.io/integrations/builtin/credentials/telegram/) and chat ID. The report text is in `{{ $json.text }}` and the subject in `{{ $json.subject }}`, so this node swaps for Slack, email or a webhook."
      },
      "typeVersion": 1
    },
    {
      "id": "s-out",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4560,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 500,
        "height": 652,
        "content": "## Final output\n\nExample of the daily report this workflow sends to Telegram:\n\nn8n workflow SLA report - 2026-06-13\nWindow: last 24h, 9 workflows\nTargets: success >= 95%, p95 <= 30s\n\nSLA: 6/9 workflows meeting SLA\nFleet: 214 runs, 96% success, 9 errors (-2pp vs last)\n\nBreaching SLA:\n  - Stripe webhook to NetSuite: success 84% < 95%\n  - Nightly warehouse sync: p95 41.2s > 30s\n  - Daily backup to S3: stalled\n\nTop offenders:\n  - Stripe webhook to NetSuite: 38 runs, 84% ok, 6 errors\n  - Lead enrichment: 52 runs, 94% ok, 3 errors\n\nSlowest (p95):\n  - Nightly warehouse sync: 41.2s p95 over 4 runs\n  - Stripe webhook to NetSuite: 3.8s p95 over 38 runs\n\nChanges vs last run:\n  - new breach: Stripe webhook to NetSuite (success)\n  - new breach: Daily backup to S3 (availability)"
      },
      "typeVersion": 1
    },
    {
      "id": "629ea88b-bb91-4963-9ffd-809550a28255",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        640,
        352
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "33764479-cd97-4441-8a81-9715608a4294",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "position": [
        912,
        352
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "c0c83f05-c71e-452d-b55c-225f16a6d7e5",
              "name": "windowHours",
              "type": "number",
              "value": 24
            },
            {
              "id": "a1111111-1111-4111-8111-111111111111",
              "name": "targetSuccessRate",
              "type": "number",
              "value": 95
            },
            {
              "id": "a2222222-2222-4222-8222-222222222222",
              "name": "targetP95Seconds",
              "type": "number",
              "value": 30
            },
            {
              "id": "0887b625-a4c9-4648-95af-8f491efcf05d",
              "name": "degradationPct",
              "type": "number",
              "value": 50
            },
            {
              "id": "67a08980-ba38-43f5-95be-02853b496327",
              "name": "successDropPp",
              "type": "number",
              "value": 10
            },
            {
              "id": "06e0334b-f1be-47d2-bbe9-dcb403229673",
              "name": "scheduleBuffer",
              "type": "number",
              "value": 1.5
            },
            {
              "id": "539c3b22-1e3c-46fe-80e7-9548b8b00e93",
              "name": "executionLimit",
              "type": "number",
              "value": 250
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "0b7fbc2e-e7e5-4c30-a8ef-e7c812e1bd0c",
      "name": "Get all workflows",
      "type": "n8n-nodes-base.n8n",
      "position": [
        1296,
        272
      ],
      "parameters": {
        "filters": {},
        "requestOptions": {}
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "1328e661-9c24-4bc5-b9fc-6ef8be03532d",
      "name": "Get recent executions",
      "type": "n8n-nodes-base.n8n",
      "position": [
        1296,
        464
      ],
      "parameters": {
        "limit": "={{ $('Config').first().json.executionLimit }}",
        "filters": {},
        "options": {},
        "resource": "execution",
        "requestOptions": {}
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "3dc832c7-d513-4d0f-aa99-768dfbff3090",
      "name": "Combine workflows and executions",
      "type": "n8n-nodes-base.merge",
      "position": [
        1552,
        352
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "c2bf353e-bae7-44e9-9c1e-71d368dfc8f9",
      "name": "Aggregate metrics",
      "type": "n8n-nodes-base.code",
      "position": [
        1920,
        352
      ],
      "parameters": {
        "jsCode": "const cfg = $('Config').first().json;\nconst windowHours = cfg.windowHours;\nconst cutoff = Date.now() - windowHours * 3600000;\n\nconst workflows = $('Get all workflows').all().map((i) => i.json);\nconst executions = $('Get recent executions').all().map((i) => i.json);\n\nconst toMs = (value) => {\n  const t = value ? new Date(value).getTime() : NaN;\n  return Number.isFinite(t) ? t : null;\n};\nconst p95 = (durations) => {\n  if (!durations.length) return null;\n  const sorted = [...durations].sort((a, b) => a - b);\n  return sorted[Math.ceil(0.95 * sorted.length) - 1];\n};\n\nconst stats = new Map();\nfor (const wf of workflows) {\n  stats.set(wf.id, { id: wf.id, name: wf.name, runs: 0, success: 0, errors: 0, durations: [], lastRunMs: null });\n}\n\nfor (const exec of executions) {\n  const wf = stats.get(exec.workflowId);\n  if (!wf) continue;\n\n  const startedAt = toMs(exec.startedAt);\n  if (startedAt !== null && (wf.lastRunMs === null || startedAt > wf.lastRunMs)) {\n    wf.lastRunMs = startedAt;\n  }\n  if (startedAt === null || startedAt < cutoff) continue;\n\n  wf.runs++;\n  if (exec.status === 'success') wf.success++;\n  else if (exec.status === 'error') wf.errors++;\n\n  const stoppedAt = toMs(exec.stoppedAt);\n  if (stoppedAt !== null && stoppedAt >= startedAt) wf.durations.push(stoppedAt - startedAt);\n}\n\nconst buckets = [...stats.values()];\nconst perWorkflow = buckets.map((wf) => ({\n  id: wf.id,\n  name: wf.name,\n  runs: wf.runs,\n  errors: wf.errors,\n  successRate: wf.runs ? Math.round((wf.success / wf.runs) * 100) : null,\n  p95Ms: p95(wf.durations),\n  lastRunAt: wf.lastRunMs ? new Date(wf.lastRunMs).toISOString() : null,\n}));\n\nconst runs = buckets.reduce((sum, wf) => sum + wf.runs, 0);\nconst success = buckets.reduce((sum, wf) => sum + wf.success, 0);\nconst errors = buckets.reduce((sum, wf) => sum + wf.errors, 0);\n\nreturn [{ json: {\n  generatedAt: new Date().toISOString(),\n  windowHours,\n  workflowsTotal: workflows.length,\n  windowTruncated: executions.length >= cfg.executionLimit,\n  executionLimit: cfg.executionLimit,\n  fleet: { runs, errors, successRate: runs ? Math.round((success / runs) * 100) : null },\n  perWorkflow,\n} }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "1f2fe4b6-7ba2-4423-9565-9f7cfa2ea498",
      "name": "Detect stalled schedules",
      "type": "n8n-nodes-base.code",
      "position": [
        2192,
        352
      ],
      "parameters": {
        "jsCode": "const buffer = $('Config').first().json.scheduleBuffer;\nconst now = Date.now();\n\nconst lastRunById = new Map(\n  $('Aggregate metrics').first().json.perWorkflow.map((wf) => [wf.id, wf.lastRunAt]),\n);\n\nconst UNIT_MS = {\n  seconds: 1000, minutes: 60000, hours: 3600000,\n  days: 86400000, weeks: 604800000, months: 2592000000,\n};\n\nconst periodMs = (interval) => {\n  const field = interval.field ?? 'days';\n  const unit = UNIT_MS[field];\n  if (!unit) return null;\n  return Number(interval[`${field}Interval`] ?? 1) * unit;\n};\n\nconst findings = [];\nfor (const item of $('Get all workflows').all()) {\n  const wf = item.json;\n  if (!wf.active) continue;\n\n  const periods = (wf.nodes ?? [])\n    .filter((node) => node.type === 'n8n-nodes-base.scheduleTrigger')\n    .flatMap((node) => node.parameters?.rule?.interval ?? [])\n    .map(periodMs)\n    .filter((period) => period > 0);\n  if (!periods.length) continue;\n\n  const maxGapMs = Math.round(Math.min(...periods) * buffer);\n  const lastRunAt = lastRunById.get(wf.id) ?? null;\n  const lastRunMs = lastRunAt ? new Date(lastRunAt).getTime() : null;\n\n  if (lastRunMs === null || now - lastRunMs > maxGapMs) {\n    findings.push({\n      id: wf.id,\n      name: wf.name,\n      reason: lastRunMs === null ? 'no execution on record' : 'overdue since last run',\n      lastRunAt,\n    });\n  }\n}\n\nreturn [{ json: { findings } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "b3333333-3333-4333-8333-333333333333",
      "name": "Evaluate SLA",
      "type": "n8n-nodes-base.code",
      "position": [
        2480,
        352
      ],
      "parameters": {
        "jsCode": "const cfg = $('Config').first().json;\nconst agg = $('Aggregate metrics').first().json;\nconst stalledIds = new Set($('Detect stalled schedules').first().json.findings.map((wf) => wf.id));\n\nconst targetP95Ms = cfg.targetP95Seconds * 1000;\n\nconst perWorkflow = agg.perWorkflow.map((wf) => {\n  const breachReasons = [];\n  const hasRuns = wf.runs > 0;\n\n  if (hasRuns && wf.successRate !== null && wf.successRate < cfg.targetSuccessRate) {\n    breachReasons.push('success');\n  }\n  if (wf.p95Ms !== null && wf.p95Ms > targetP95Ms) {\n    breachReasons.push('latency');\n  }\n  if (stalledIds.has(wf.id)) {\n    breachReasons.push('availability');\n  }\n\n  const evaluated = hasRuns || stalledIds.has(wf.id);\n\n  return {\n    id: wf.id,\n    name: wf.name,\n    runs: wf.runs,\n    errors: wf.errors,\n    successRate: wf.successRate,\n    p95Ms: wf.p95Ms,\n    evaluated,\n    slaStatus: evaluated ? (breachReasons.length ? 'breached' : 'met') : 'skipped',\n    breachReasons,\n  };\n});\n\nconst evaluatedList = perWorkflow.filter((wf) => wf.evaluated);\nconst metCount = evaluatedList.filter((wf) => wf.slaStatus === 'met').length;\nconst breachCount = evaluatedList.filter((wf) => wf.slaStatus === 'breached').length;\n\nreturn [{ json: {\n  generatedAt: agg.generatedAt,\n  windowHours: agg.windowHours,\n  targets: { successRate: cfg.targetSuccessRate, p95Seconds: cfg.targetP95Seconds },\n  fleet: agg.fleet,\n  workflowsTotal: agg.workflowsTotal,\n  windowTruncated: agg.windowTruncated,\n  executionLimit: agg.executionLimit,\n  evaluatedCount: evaluatedList.length,\n  metCount,\n  breachCount,\n  fleetSlaMet: evaluatedList.length > 0 && metCount === evaluatedList.length,\n  perWorkflow,\n} }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "fc043bca-2088-4b3c-9fdd-fe6746067984",
      "name": "Get previous snapshot",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        2832,
        352
      ],
      "parameters": {
        "limit": 1,
        "orderBy": true,
        "operation": "get",
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "sla_history"
        },
        "orderByColumn": "runAt",
        "orderByDirection": "DESC"
      },
      "typeVersion": 1.1,
      "alwaysOutputData": true
    },
    {
      "id": "e6716ae7-f0a7-43b8-8778-bf3ff07d038c",
      "name": "Compare to previous run",
      "type": "n8n-nodes-base.code",
      "position": [
        3120,
        352
      ],
      "parameters": {
        "jsCode": "const cfg = $('Config').first().json;\nconst sla = $('Evaluate SLA').first().json;\n\nconst current = {\n  generatedAt: sla.generatedAt,\n  targets: sla.targets,\n  fleet: sla.fleet,\n  evaluatedCount: sla.evaluatedCount,\n  metCount: sla.metCount,\n  breachCount: sla.breachCount,\n  fleetSlaMet: sla.fleetSlaMet,\n  perWorkflow: sla.perWorkflow.map((wf) => ({\n    id: wf.id,\n    name: wf.name,\n    successRate: wf.successRate,\n    p95Ms: wf.p95Ms,\n    slaStatus: wf.slaStatus,\n    breachReasons: wf.breachReasons,\n  })),\n};\n\nconst prevRow = $('Get previous snapshot').first().json;\nlet previous = null;\nif (prevRow && prevRow.metrics) {\n  try { previous = JSON.parse(prevRow.metrics); } catch { previous = null; }\n}\n\nconst prevById = new Map((previous?.perWorkflow ?? []).map((wf) => [wf.id, wf]));\n\nconst slowdowns = [];\nconst successDrops = [];\nconst newlyBreaching = [];\nconst recovered = [];\n\nfor (const wf of current.perWorkflow) {\n  const before = prevById.get(wf.id);\n\n  if (before && wf.p95Ms !== null && before.p95Ms) {\n    const growthPct = Math.round(((wf.p95Ms - before.p95Ms) / before.p95Ms) * 100);\n    if (growthPct > cfg.degradationPct) slowdowns.push({ name: wf.name, growthPct });\n  }\n  if (before && wf.successRate !== null && before.successRate !== null && before.successRate !== undefined) {\n    const dropPp = before.successRate - wf.successRate;\n    if (dropPp > cfg.successDropPp) successDrops.push({ name: wf.name, dropPp });\n  }\n\n  const wasBreached = before && before.slaStatus === 'breached';\n  if (wf.slaStatus === 'breached' && !wasBreached) {\n    newlyBreaching.push({ name: wf.name, reasons: wf.breachReasons });\n  }\n  if (wf.slaStatus === 'met' && wasBreached) {\n    recovered.push({ name: wf.name });\n  }\n}\n\nconst fleetSuccessDelta =\n  previous?.fleet?.successRate != null && current.fleet.successRate != null\n    ? current.fleet.successRate - previous.fleet.successRate\n    : null;\n\nreturn [{ json: {\n  runAt: current.generatedAt,\n  current,\n  hasPrevious: previous !== null,\n  fleetSuccessDelta,\n  slowdowns,\n  successDrops,\n  newlyBreaching,\n  recovered,\n} }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "8c413bf1-6126-48c4-bd34-37145be2a992",
      "name": "Save snapshot",
      "type": "n8n-nodes-base.dataTable",
      "position": [
        3392,
        352
      ],
      "parameters": {
        "columns": {
          "value": {
            "runAt": "={{ $json.runAt }}",
            "metrics": "={{ JSON.stringify($json.current) }}"
          },
          "schema": [
            {
              "id": "runAt",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "runAt",
              "defaultMatch": false
            },
            {
              "id": "metrics",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "metrics",
              "defaultMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "dataTableId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "sla_history"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "846563de-29f8-43b4-955d-27f8f8075edc",
      "name": "Prepare report fields",
      "type": "n8n-nodes-base.code",
      "position": [
        3760,
        352
      ],
      "parameters": {
        "jsCode": "const cfg = $('Config').first().json;\nconst agg = $('Aggregate metrics').first().json;\nconst sla = $('Evaluate SLA').first().json;\nconst cmp = $('Compare to previous run').first().json;\nconst fleet = agg.fleet;\n\nconst plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;\nconst duration = (ms) => (ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`);\n\nconst reasonText = {\n  success: (wf) => `success ${wf.successRate}% < ${sla.targets.successRate}%`,\n  latency: (wf) => `p95 ${duration(wf.p95Ms)} > ${sla.targets.p95Seconds}s`,\n  availability: () => 'stalled',\n};\n\nconst breaching = sla.perWorkflow\n  .filter((wf) => wf.slaStatus === 'breached')\n  .map((wf) => `  - ${wf.name}: ${wf.breachReasons.map((reason) => reasonText[reason](wf)).join(', ')}`);\n\nconst offenders = agg.perWorkflow\n  .filter((wf) => wf.runs > 0 && wf.errors > 0)\n  .sort((a, b) => b.errors - a.errors)\n  .slice(0, 5)\n  .map((wf) => `  - ${wf.name}: ${plural(wf.runs, 'run')}, ${wf.successRate}% ok, ${plural(wf.errors, 'error')}`);\n\nconst slowest = agg.perWorkflow\n  .filter((wf) => wf.p95Ms !== null)\n  .sort((a, b) => b.p95Ms - a.p95Ms)\n  .slice(0, 3)\n  .map((wf) => `  - ${wf.name}: ${duration(wf.p95Ms)} p95 over ${plural(wf.runs, 'run')}`);\n\nconst changes = [\n  ...cmp.newlyBreaching.map((wf) => `  - new breach: ${wf.name} (${wf.reasons.join(', ')})`),\n  ...cmp.recovered.map((wf) => `  - recovered: ${wf.name}`),\n  ...cmp.slowdowns.map((wf) => `  - slower: ${wf.name} p95 +${wf.growthPct}%`),\n  ...cmp.successDrops.map((wf) => `  - success drop: ${wf.name} -${wf.dropPp}pp`),\n];\n\nconst delta = cmp.fleetSuccessDelta;\nconst deltaSuffix = delta != null ? ` (${delta >= 0 ? '+' : ''}${delta}pp vs last)` : '';\n\nreturn [{ json: {\n  date: cmp.runAt.slice(0, 10),\n  windowHours: agg.windowHours,\n  workflowsLine: plural(agg.workflowsTotal, 'workflow'),\n  truncationLine: agg.windowTruncated\n    ? `Note: hit the ${agg.executionLimit}-execution fetch limit, older runs may be cut off. Raise executionLimit in Config.`\n    : '',\n  targetsLine: `Targets: success >= ${sla.targets.successRate}%, p95 <= ${sla.targets.p95Seconds}s`,\n  slaLine: `SLA: ${sla.metCount}/${sla.evaluatedCount} workflows meeting SLA`,\n  fleetLine: `Fleet: ${plural(fleet.runs, 'run')}, ${fleet.successRate ?? 0}% success, ${plural(fleet.errors, 'error')}${deltaSuffix}`,\n  breachingList: breaching.join('\\n') || '  - none, all workflows within SLA',\n  offendersList: offenders.join('\\n') || '  - none, all green',\n  slowestList: slowest.join('\\n') || '  - no timing data',\n  changesList: cmp.hasPrevious ? (changes.join('\\n') || '  - no significant changes') : '  - first run, no comparison yet',\n  breachCount: sla.breachCount,\n  metCount: sla.metCount,\n  evaluatedCount: sla.evaluatedCount,\n} }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "20333fb8-f397-4a2b-bdab-2ae2cc9fba11",
      "name": "Compose message",
      "type": "n8n-nodes-base.set",
      "position": [
        4032,
        352
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "dd080de4-3593-42c3-a431-9a64a706df88",
              "name": "subject",
              "type": "string",
              "value": "=n8n SLA: {{ $json.metCount }}/{{ $json.evaluatedCount }} met, {{ $json.breachCount }} breaching"
            },
            {
              "id": "3a135f2f-6b6e-47c8-aa9f-01810781eb91",
              "name": "text",
              "type": "string",
              "value": "=n8n workflow SLA report - {{ $json.date }}\nWindow: last {{ $json.windowHours }}h, {{ $json.workflowsLine }}\n{{ $json.truncationLine ? $json.truncationLine + '\\n' : '' }}{{ $json.targetsLine }}\n\n{{ $json.slaLine }}\n{{ $json.fleetLine }}\n\nBreaching SLA:\n{{ $json.breachingList }}\n\nTop offenders:\n{{ $json.offendersList }}\n\nSlowest (p95):\n{{ $json.slowestList }}\n\nChanges vs last run:\n{{ $json.changesList }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "7ec4b5e2-8ca4-42c1-a16a-8c86c1ef8ed0",
      "name": "Send report to Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        4320,
        352
      ],
      "parameters": {
        "text": "={{ $json.text }}",
        "chatId": "YOUR_CHAT_ID",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "connections": {
    "Config": {
      "main": [
        [
          {
            "node": "Get all workflows",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get recent executions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Evaluate SLA": {
      "main": [
        [
          {
            "node": "Get previous snapshot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save snapshot": {
      "main": [
        [
          {
            "node": "Prepare report fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compose message": {
      "main": [
        [
          {
            "node": "Send report to Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate metrics": {
      "main": [
        [
          {
            "node": "Detect stalled schedules",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get all workflows": {
      "main": [
        [
          {
            "node": "Combine workflows and executions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get previous snapshot": {
      "main": [
        [
          {
            "node": "Compare to previous run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get recent executions": {
      "main": [
        [
          {
            "node": "Combine workflows and executions",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Prepare report fields": {
      "main": [
        [
          {
            "node": "Compose message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compare to previous run": {
      "main": [
        [
          {
            "node": "Save snapshot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect stalled schedules": {
      "main": [
        [
          {
            "node": "Evaluate SLA",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine workflows and executions": {
      "main": [
        [
          {
            "node": "Aggregate metrics",
            "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

This workflow runs daily, queries the n8n API for workflows and recent executions, calculates per-workflow success rate, error count and p95 runtime over a rolling window, detects stalled schedules, stores a snapshot in an n8n Data Table, and sends a change-aware fleet health…

Source: https://n8n.io/workflows/16483/ — original creator credit. Request a take-down →

More Slack & Telegram workflows → · Browse all categories →

Related workflows

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

Slack & Telegram

The native n8n security audit misses tokens pasted into node parameters, active workflows without an error handler, plain http:// calls and leftover pinned data. This workflow runs the native audit pl

n8n, Data Table, Telegram
Slack & Telegram

A robust workflow to back up and synchronize your n8n workflows to a GitHub repository, with intelligent change detection and support for file renames.

GitHub, n8n, Telegram +1
Slack & Telegram

TelegramQuery. Uses httpRequest, dataTable, postgres, telegram. Scheduled trigger; 26 nodes.

HTTP Request, Data Table, Postgres +1
Slack & Telegram

Scans every workflow on your n8n instance for credentials, tokens, and personal data pasted straight into node parameters. Findings are reported to Telegram by severity, and the snapshot committed to

n8n, GitHub, Telegram
Slack & Telegram

This workflow is for system administrators or self-hosted n8n users who want to automatically check and update their n8n instance to the latest version — with Telegram notifications for every step. Th

HTTP Request, Telegram, n8n +2