AutomationFlowsSlack & Telegram › Monitor Website Uptime and Content Changes with Http Checks and Slack

Monitor Website Uptime and Content Changes with Http Checks and Slack

ByJulian Abt @automationsmanufaktur on n8n.io

This workflow runs every 30 minutes to check a list of website URLs, detect downtime or page content changes using HTTP requests and stored workflow state, and post alerts to a Slack channel only when something goes down, changes, or recovers. Runs every 30 minutes on a…

Cron / scheduled trigger★★★★☆ complexity11 nodesHTTP RequestSlack
Slack & Telegram Trigger: Cron / scheduled Nodes: 11 Complexity: ★★★★☆ Added:

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

This workflow follows the HTTP Request → Slack recipe pattern — see all workflows that pair these two integrations.

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": "Website Change & Uptime Monitor (no AI)",
  "nodes": [
    {
      "id": "sticky-03-overview",
      "name": "Overview & Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -600,
        -40
      ],
      "parameters": {
        "color": 3,
        "width": 540,
        "height": 560,
        "content": "## \ud83d\udef0\ufe0f Website Change & Uptime Monitor\n\nWatches a list of URLs and posts a Slack alert only when something breaks or changes \u2014 no database, no AI.\n\n### \ud83d\udc64 Who's it for\nAnyone who wants lightweight uptime + content-change monitoring without a paid service or extra infrastructure.\n\n### \u2699\ufe0f How it works\n1. **Schedule** runs every 30 min.\n2. **Fetch Page** requests each URL (never errors on 4xx/5xx).\n3. A Code node diffs status + body against the last signature in **workflow static data**.\n4. **Slack** is messaged only for *down*, *changed* or *recovered* \u2014 quiet runs stay silent.\n\n### \ud83d\udd27 Set up (~2 min)\n- Edit the URLs in **Define URLs to Monitor** (each needs `label` + `url`).\n- Add a **Slack** credential, pick a channel, then activate.\n- First run records a baseline and sends nothing; alerts start from run two."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-03-s1",
      "name": "Section 1 \u00b7 Targets",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -10,
        -40
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 360,
        "content": "### 1. Targets\nEdit the URLs to watch in **Define URLs to Monitor** \u2014 one item per page, each with a `label` and a `url`."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-03-s2",
      "name": "Section 2 \u00b7 Fetch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        650,
        -40
      ],
      "parameters": {
        "color": 7,
        "width": 200,
        "height": 360,
        "content": "### 2. Fetch\n**Fetch Page** runs once per URL with *Never Error* + *Full Response*, so a down site returns its status code instead of aborting."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-03-s3",
      "name": "Section 3 \u00b7 Diff & State",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        990,
        -40
      ],
      "parameters": {
        "color": 7,
        "width": 540,
        "height": 360,
        "content": "### 3. Diff & State\nCompares each page to the last signature in **workflow static data**, then stores the new one. Emits items only for down / changed / recovered."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-03-s4",
      "name": "Section 4 \u00b7 Alert",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1650,
        -40
      ],
      "parameters": {
        "color": 7,
        "width": 200,
        "height": 360,
        "content": "### 4. Alert\nOne Slack message per problem. Quiet runs send nothing because the Code node returns zero items."
      },
      "typeVersion": 1
    },
    {
      "id": "schedule-trigger",
      "name": "Every 30 Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        40,
        80
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 30
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "define-urls",
      "name": "Define URLs to Monitor",
      "type": "n8n-nodes-base.code",
      "position": [
        360,
        80
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Define the targets you want to watch.\n// Return ONE item per URL so the HTTP Request node fetches each one separately.\n// Edit this list: add/remove objects, change the \"label\" (shown in alerts) and \"url\".\nconst targets = [\n  { label: 'Example Homepage', url: 'https://example.com' },\n  { label: 'Example Pricing',  url: 'https://example.com/pricing' },\n  { label: 'Example Status',   url: 'https://example.com/status' },\n];\n\nreturn targets.map((t) => ({ json: { label: t.label, url: t.url } }));\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "fetch-page",
      "name": "Fetch Page",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        700,
        80
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "method": "GET",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": true,
              "responseFormat": "text"
            }
          }
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "detect-change",
      "name": "Detect Status & Content Change",
      "type": "n8n-nodes-base.code",
      "position": [
        1040,
        80
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Compares each freshly fetched page against the last version we saw and\n// emits an alert item ONLY when a site is down or its content changed.\n//\n// State is persisted in workflow static data ('global'), keyed by URL, so it\n// survives between executions without any database. On the very first run a\n// URL has no stored baseline yet -> we record it and stay silent (no alert).\n\nconst staticData = $getWorkflowStaticData('global');\n// Namespaced bucket so we never collide with other workflow state.\nif (!staticData.siteMonitor) staticData.siteMonitor = {};\nconst store = staticData.siteMonitor;\n\n// Cheap, dependency-free signature of a page body. We combine length with a\n// rolling 32-bit hash so unrelated edits of equal length still differ.\nfunction signature(text) {\n  const s = String(text == null ? '' : text);\n  let hash = 0;\n  for (let i = 0; i < s.length; i++) {\n    hash = (hash * 31 + s.charCodeAt(i)) | 0; // | 0 keeps it a 32-bit int\n  }\n  return s.length + ':' + (hash >>> 0).toString(16);\n}\n\nconst alerts = [];\n\nfor (const item of $input.all()) {\n  const data = item.json || {};\n  // 'url' / 'label' were carried through from the target item; the HTTP node\n  // (fullResponse) adds statusCode + body. A network failure leaves them blank.\n  const url = data.url || data.requestUrl || '';\n  const label = data.label || url || 'Unknown target';\n  const statusCode = typeof data.statusCode === 'number' ? data.statusCode : null;\n  const body = data.body !== undefined ? data.body : data.data;\n\n  // Treat a missing/error status or any 4xx/5xx as \"down\".\n  const isDown = statusCode === null || statusCode >= 400;\n\n  const prev = store[url];\n  const newSig = isDown ? null : signature(body);\n\n  let changeType = 'ok';\n  let message = '';\n\n  if (isDown) {\n    changeType = 'down';\n    const codeText = statusCode === null ? 'no response' : 'HTTP ' + statusCode;\n    message = ':rotating_light: ' + label + ' is DOWN (' + codeText + ') \u2014 ' + url;\n  } else if (!prev || prev.signature == null) {\n    // First time we successfully see this URL: record baseline, stay silent.\n    changeType = 'baseline';\n  } else if (prev.signature !== newSig) {\n    changeType = 'changed';\n    message = ':eyes: Content changed on ' + label + ' (' + url + ')';\n  } else if (prev.lastStatus === 'down') {\n    // Recovery: it was down before and is healthy now \u2014 worth a heads-up.\n    changeType = 'recovered';\n    message = ':white_check_mark: ' + label + ' is back UP (HTTP ' + statusCode + ') \u2014 ' + url;\n  }\n\n  // Always update state so the next run compares against the latest version.\n  store[url] = {\n    signature: newSig,\n    lastStatus: isDown ? 'down' : 'ok',\n    statusCode: statusCode,\n    lastCheckedAt: new Date().toISOString(),\n  };\n\n  if (changeType === 'down' || changeType === 'changed' || changeType === 'recovered') {\n    alerts.push({\n      json: { label, url, statusCode, changeType, message },\n    });\n  }\n}\n\n// Returning an empty array means the workflow simply stops here on a quiet run,\n// so no Slack message is sent unless something actually happened.\nreturn alerts;\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "anything-to-report",
      "name": "Anything To Report?",
      "type": "n8n-nodes-base.if",
      "position": [
        1380,
        80
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-has-message",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.message }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "post-alert",
      "name": "Post Alert",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "maxTries": 3,
      "position": [
        1700,
        80
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "select": "channel",
        "resource": "message",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#alerts",
          "cachedResultName": "#alerts"
        },
        "operation": "post",
        "messageType": "text",
        "otherOptions": {}
      },
      "retryOnFail": true,
      "typeVersion": 2.4,
      "waitBetweenTries": 5000
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Fetch Page": {
      "main": [
        [
          {
            "node": "Detect Status & Content Change",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every 30 Minutes": {
      "main": [
        [
          {
            "node": "Define URLs to Monitor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Anything To Report?": {
      "main": [
        [
          {
            "node": "Post Alert",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Define URLs to Monitor": {
      "main": [
        [
          {
            "node": "Fetch Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect Status & Content Change": {
      "main": [
        [
          {
            "node": "Anything To Report?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

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

About this workflow

This workflow runs every 30 minutes to check a list of website URLs, detect downtime or page content changes using HTTP requests and stored workflow state, and post alerts to a Slack channel only when something goes down, changes, or recovers. Runs every 30 minutes on a…

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

debug. Uses httpRequest, slack, redis, mailgun. Scheduled trigger; 60 nodes.

HTTP Request, Slack, Redis +2
Slack & Telegram

This workflow is an automated employee time tracking and reporting system that monitors weekly work hours via TMetric, then delivers personalized summaries directly to each team member on Slack. It co

HTTP Request, Item Lists, Data Table +1
Slack & Telegram

Import Productboard Notes Companies And Features Into Snowflake. Uses stickyNote, httpRequest, splitOut, snowflake. Scheduled trigger; 35 nodes.

HTTP Request, Snowflake, Slack
Slack & Telegram

Import Productboard Notes, Companies and Features into Snowflake. Uses stickyNote, httpRequest, splitOut, snowflake. Scheduled trigger; 35 nodes.

HTTP Request, Snowflake, Slack
Slack & Telegram

This workflow imports Productboard data into Snowflake, automating data extraction, mapping, and updates for features, companies, and notes. It supports scheduled weekly updates, data cleansing, and S

HTTP Request, Snowflake, Slack