AutomationFlowsEmail & Gmail › Monitor Web Pages for Relevant Listings with Claude Haiku and Gmail

Monitor Web Pages for Relevant Listings with Claude Haiku and Gmail

ByWLTI @wlti on n8n.io

This workflow manually fetches a webpage, uses Anthropic Claude to extract listing-style items from the raw HTML and score them against your criteria, then emails an HTML summary of high-scoring matches via Gmail. Starts when you run the workflow manually. Requests the target…

Event trigger★★★★☆ complexity17 nodesHTTP RequestGmail
Email & Gmail Trigger: Event Nodes: 17 Complexity: ★★★★☆ Added:

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

This workflow follows the Gmail → HTTP Request 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": "AI Site Monitor - Free Starter",
  "nodes": [
    {
      "id": "sticky-upsell",
      "name": "Get the Full Version",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        -460
      ],
      "parameters": {
        "color": 4,
        "width": 460,
        "height": 420,
        "content": "### This is the FREE Starter version\n\nShows the core trick: an AI reads any webpage and decides what's relevant - no CSS scraping, works on any site/language.\n\nIn this free version you edit the URL and criteria directly inside the nodes below (no config screen), and you run it manually (no schedule).\n\n**Want the full version?** It adds:\n- Daily automatic schedule\n- Duplicate detection (never get alerted on the same item twice)\n- One simple CONFIG screen (edit URL/criteria without touching code)\n- Full setup README + support\n\nGet it here: https://wlti.gumroad.com/l/ai-site-monitor ($39)"
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-fetch",
      "name": "How it works: Fetch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        250
      ],
      "parameters": {
        "color": 6,
        "width": 420,
        "height": 160,
        "content": "### 1. Fetch\nGrabs the raw HTML of the page you want to monitor."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-extract",
      "name": "How it works: Extract",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        440,
        250
      ],
      "parameters": {
        "color": 6,
        "width": 640,
        "height": 160,
        "content": "### 2. Extract\nSends the page content to Claude, which reads it and pulls out a structured list of items (title, description, link, date) - no custom scraper per site needed."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-score",
      "name": "How it works: Score & Filter",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1100,
        250
      ],
      "parameters": {
        "color": 6,
        "width": 860,
        "height": 160,
        "content": "### 3. Score & Filter\nEach item goes back to Claude to be judged against your criteria, written in plain English. Only items that score high enough move forward."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-notify",
      "name": "How it works: Notify",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1980,
        280
      ],
      "parameters": {
        "color": 6,
        "width": 640,
        "height": 160,
        "content": "### 4. Notify\nBuilds an HTML summary of the approved items and emails it to you."
      },
      "typeVersion": 1
    },
    {
      "id": "trigger-manual",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        0,
        0
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "http-fonte",
      "name": "HTTP Source (edit the URL above)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        220,
        0
      ],
      "parameters": {
        "url": "https://REPLACE-WITH-THE-URL-OF-THE-SITE-YOU-WANT-TO-CHECK.com",
        "method": "GET",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (compatible; AIMonitorBot/1.0)"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "code-montar-extracao",
      "name": "Build Extraction Request",
      "type": "n8n-nodes-base.code",
      "position": [
        440,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// EDIT THIS if your site's items don't fit the default description below\nconst EXTRACTION_CRITERIA = \"Extract each relevant listing item found on the page (e.g. each ad, product, job posting, auction lot, property - depending on the monitored site).\";\n\nconst raw = $input.first().json.data || '';\nlet html = String(raw);\nhtml = html\n  .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n  .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n  .replace(/<!--[\\s\\S]*?-->/g, ' ');\n\n// character limit to control token cost - increase if the page is large and items are getting missed\nconst MAX_CHARS = 100000;\nif (html.length > MAX_CHARS) html = html.slice(0, MAX_CHARS);\n\nconst systemText = \"You are an assistant that extracts structured listings from web pages based on raw HTML. \" + EXTRACTION_CRITERIA + \" For each item, extract: title, description (short summary), link (full URL - use the href found in the HTML; if relative, keep it as-is), date (if available, otherwise leave empty). Return ONLY a JSON array, no extra text, no markdown, no code blocks, using exactly these field names: title, description, link, date. If you find no items, return an empty array [].\";\n\nconst userText = \"PAGE HTML:\\n\" + html;\n\nconst requestBody = {\n  model: 'claude-haiku-4-5',\n  max_tokens: 8000,\n  system: [ { type: 'text', text: systemText } ],\n  messages: [ { role: 'user', content: userText } ]\n};\n\nreturn [{ json: { requestBody } }];\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "http-anthropic-extracao",
      "name": "Call Anthropic API (Extraction)",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        660,
        0
      ],
      "parameters": {
        "url": "https://api.anthropic.com/v1/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json.requestBody) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "code-extrair-itens",
      "name": "Extract Items",
      "type": "n8n-nodes-base.code",
      "position": [
        880,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "let items = [];\ntry {\n  const resp = $input.first().json;\n  const blocks = resp.content || [];\n  const textBlock = blocks.find(b => b && b.type === 'text');\n  let text = textBlock ? textBlock.text : '';\n  text = text.trim().replace(/^```(?:json)?\\s*/i, '').replace(/```\\s*$/i, '').trim();\n  const parsed = JSON.parse(text);\n  items = Array.isArray(parsed) ? parsed : [];\n} catch (e) {\n  items = [];\n}\nreturn items.map(v => ({ json: v }));\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "code-montar-score",
      "name": "Build AI Request (Score)",
      "type": "n8n-nodes-base.code",
      "position": [
        1100,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// EDIT THIS: describe in plain English what you consider relevant\nconst CRITERIA_TEXT = \"Describe here what you consider relevant. Example: 'I'm looking for a 2 or 3 bedroom apartment in the Porto area, appraisal value under 150,000 euros, preferably unoccupied and with no condo debts mentioned in the auction notice.'\";\n\nconst SYSTEM_TEXT = \"You are an assistant that evaluates whether an item found on a web page is relevant according to the user's criteria.\\n\\nUSER CRITERIA:\\n\" + CRITERIA_TEXT;\n\nreturn $input.all().map(item => {\n  const v = item.json;\n\n  const userText = \"ITEM:\\n\" +\n    \"Title: \" + (v.title || '') + \"\\n\" +\n    \"Description: \" + (v.description || '') + \"\\n\" +\n    \"Link: \" + (v.link || '') + \"\\n\\n\" +\n    \"Evaluate this item's compatibility with the user's criteria. Return ONLY a JSON in the following format, no extra text, no markdown, no code blocks:\\n\" +\n    \"{\\n\" +\n    \"  \\\"score\\\": (rating from 0 to 10, number),\\n\" +\n    \"  \\\"short_justification\\\": \\\"1-2 sentences explaining the score\\\"\\n\" +\n    \"}\";\n\n  const requestBody = {\n    model: 'claude-haiku-4-5',\n    max_tokens: 300,\n    system: [ { type: 'text', text: SYSTEM_TEXT, cache_control: { type: 'ephemeral' } } ],\n    messages: [ { role: 'user', content: userText } ]\n  };\n\n  return { json: { item: v, requestBody } };\n});\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "http-anthropic-score",
      "name": "Call Anthropic API (Score)",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        1320,
        0
      ],
      "parameters": {
        "url": "https://api.anthropic.com/v1/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json.requestBody) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "code-extrair-score",
      "name": "Extract Score",
      "type": "n8n-nodes-base.code",
      "position": [
        1540,
        0
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const originals = $('Build AI Request (Score)').all();\nconst responses = $input.all();\nconst result = [];\n\nfor (let i = 0; i < responses.length; i++) {\n  const original = (originals[i] && originals[i].json && originals[i].json.item) || {};\n  const resp = responses[i].json;\n\n  let scoreData = { score: null, short_justification: 'Error processing AI response.' };\n\n  try {\n    const blocks = resp.content || [];\n    const textBlock = blocks.find(b => b && b.type === 'text');\n    let text = textBlock ? textBlock.text : '';\n    text = text.trim().replace(/^```(?:json)?\\s*/i, '').replace(/```\\s*$/i, '').trim();\n    const parsed = JSON.parse(text);\n    scoreData = {\n      score: typeof parsed.score === 'number' ? parsed.score : null,\n      short_justification: parsed.short_justification || ''\n    };\n  } catch (e) {}\n\n  result.push({ json: Object.assign({}, original, scoreData) });\n}\n\nreturn result;\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "if-filtrar-aprovados",
      "name": "Filter Approved (score >= 7)",
      "type": "n8n-nodes-base.if",
      "position": [
        1760,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-score-cutoff",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $json.score }}",
              "rightValue": 7
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "code-montar-resumo",
      "name": "Build Summary",
      "type": "n8n-nodes-base.code",
      "position": [
        1980,
        120
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "function esc(s) {\n  return String(s === undefined || s === null ? '' : s)\n    .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n}\n\nconst approved = $input.all().map(i => i.json);\n\nconst blocks = approved.map(v => {\n  return (\n    '<div style=\"border:1px solid #ddd;border-radius:8px;padding:16px;margin-bottom:20px;\">' +\n    '<h3 style=\"margin:0 0 4px;\">' + esc(v.title) + '</h3>' +\n    '<p style=\"margin:0 0 8px;color:#555;\">Score: <b>' + esc(v.score) + '</b>/10</p>' +\n    '<p><b>Why it is relevant:</b> ' + esc(v.short_justification) + '</p>' +\n    '<p>' + esc(v.description) + '</p>' +\n    '<p><a href=\"' + esc(v.link) + '\">View original item</a></p>' +\n    '</div>'\n  );\n}).join('');\n\nconst html = '<h2>AI Site Monitor - Results (' + approved.length + (approved.length === 1 ? ' item' : ' items') + ')</h2>' + blocks;\n\nreturn [{ json: { total_approved: approved.length, summary_html: html } }];\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "if-tem-itens",
      "name": "Has Results",
      "type": "n8n-nodes-base.if",
      "position": [
        2200,
        120
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-tem-itens",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.total_approved }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "gmail-enviar-resumo",
      "name": "Send Results Email (edit the address above)",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2420,
        60
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "={{ $json.summary_html }}",
        "options": {},
        "subject": "=AI Site Monitor - Results ({{ $json.total_approved }} items)",
        "resource": "message",
        "emailType": "html",
        "operation": "send"
      },
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Has Results": {
      "main": [
        [
          {
            "node": "Send Results Email (edit the address above)",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Build Summary": {
      "main": [
        [
          {
            "node": "Has Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Items": {
      "main": [
        [
          {
            "node": "Build AI Request (Score)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Score": {
      "main": [
        [
          {
            "node": "Filter Approved (score >= 7)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "HTTP Source (edit the URL above)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build AI Request (Score)": {
      "main": [
        [
          {
            "node": "Call Anthropic API (Score)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Extraction Request": {
      "main": [
        [
          {
            "node": "Call Anthropic API (Extraction)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Anthropic API (Score)": {
      "main": [
        [
          {
            "node": "Extract Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Approved (score >= 7)": {
      "main": [
        [
          {
            "node": "Build Summary",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Call Anthropic API (Extraction)": {
      "main": [
        [
          {
            "node": "Extract Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Source (edit the URL above)": {
      "main": [
        [
          {
            "node": "Build Extraction Request",
            "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 manually fetches a webpage, uses Anthropic Claude to extract listing-style items from the raw HTML and score them against your criteria, then emails an HTML summary of high-scoring matches via Gmail. Starts when you run the workflow manually. Requests the target…

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

More Email & Gmail workflows → · Browse all categories →

Related workflows

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

Email & Gmail

Gmail Workflow. Uses gmail, googleCalendar, executeWorkflowTrigger, whatsApp. Event-driven trigger; 61 nodes.

Gmail, Google Calendar, Execute Workflow Trigger +2
Email & Gmail

This workflow ingests proof-of-delivery and completion documents from Gmail or a webhook, extracts key fields with an OpenRouter vision model, reconciles them against Google Sheets dispatch data, arch

Gmail Trigger, HTTP Request, Google Sheets +2
Email & Gmail

Splitout Code. Uses manualTrigger, httpRequest, stickyNote, splitOut. Event-driven trigger; 46 nodes.

HTTP Request, Execute Workflow Trigger, Gmail +1
Email & Gmail

Automate CSV imports into HubSpot without the mess. Powered by n8n. Supercharged by Pollup AI.

HTTP Request, Execute Workflow Trigger, Gmail +1
Email & Gmail

Echo Brand Voice Analysis (Processor) - TASK-074 Dec 10 Fix. Uses formTrigger, httpRequest, executeWorkflowTrigger, moveBinaryData. Event-driven trigger; 40 nodes.

Form Trigger, HTTP Request, Execute Workflow Trigger +2