AutomationFlowsAI & RAG › Ai-powered Vendor Policy & RSS Feed Analysis with Integrated Risk Scoring

Ai-powered Vendor Policy & RSS Feed Analysis with Integrated Risk Scoring

ByKamalraj @kamalraj on n8n.io

A dual-engine, AI-driven n8n workflow that automates the monitoring of both vendor policy webpages and compliance-related RSS feeds. It intelligently detects recent updates, evaluates their potential risk, and delivers a structured HTML digest categorized by severity — right to…

Cron / scheduled trigger★★★★☆ complexityAI-powered29 nodesHTTP RequestAgentRSS Feed ReadGoogle Gemini ChatGmail
AI & RAG Trigger: Cron / scheduled Nodes: 29 Complexity: ★★★★☆ AI nodes: yes Added:
Ai-powered Vendor Policy & RSS Feed Analysis with Integrated Risk Scoring — n8n workflow card showing HTTP Request, Agent, RSS Feed Read integration

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

This workflow follows the Agent → Gmail 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
{
  "id": "DHLbKyEoYyzkhEBA",
  "name": "AI-Powered Vendor Policy & Feed Analysis with Integrated Risk Scoring",
  "tags": [],
  "nodes": [
    {
      "id": "477ac26f-b2f9-48a9-ba04-8acff89225ff",
      "name": "HTTP Request",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "\ud83c\udf10 Requests vendor policy pages for content inspection.",
      "position": [
        -700,
        260
      ],
      "parameters": {
        "url": "={{ $json.feedUrl }}",
        "options": {
          "response": {
            "response": {
              "fullResponse": true
            }
          }
        }
      },
      "notesInFlow": true,
      "typeVersion": 4.2,
      "alwaysOutputData": false
    },
    {
      "id": "209ac43c-361b-4cdf-b8f1-5cfadf3bace0",
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "notes": "\ud83e\udd16 Analyzes grouped RSS feeds and categorizes risks using structured JSON.",
      "position": [
        200,
        -420
      ],
      "parameters": {
        "text": "={{ $json.entries }}",
        "options": {
          "systemMessage": "=Vendor Risk & Compliance Digest Prompt\n\nAI Agent Role: You are a Senior Compliance & Risk Intelligence Analyst embedded in an automation workflow. Your job is to analyze compliance-related content from vendor feeds and produce structured, actionable summaries for use by compliance, security, and executive teams. Dont Halucinate, Dont add any new news, only act on the provided data\n\n\ud83e\uddfe Input Format\n\nYou will receive a single JSON object containing an array of items. Each item is structured as:\n\n{\n  \"Title\": \"...\",\n  \"Vendor Name\": \"...\",\n  \"link\": \"https://\u2026\",\n  \"content\": \"Snippet or article content here\"\n}\n\n\ud83e\udde0 Instructions\n\nDeduplicate entries by the link field.\n\nFor each unique item:\n\nAssign a Risk Rating:\n\nHigh\n\nMedium\n\nLow\n\nInformational\n\nWrite a 2-sentence summary from a compliance, risk, or security standpoint.\n\nGroup the output by the risk level in a single JSON object.\n\nOmit any empty groups (i.e., risk levels with no entries).\n\nDo not include extra prose, HTML, Markdown, or escape characters.\n\n\u2705 Output Format\n\nRespond with only this structure:\n\n{\n  \"High\": [\n    { \"summary\": \"...\", \"link\": \"https://...\", \"Title\": \"...\",\"Vendor Name\": \"...\" }\n  ],\n  \"Medium\": [ ... ],\n  \"Low\": [ ... ],\n  \"Informational\": [ ... ]\n}\n\nThis output will be used by downstream code to generate an HTML email digest.",
          "passthroughBinaryImages": false
        },
        "promptType": "define"
      },
      "notesInFlow": true,
      "typeVersion": 2,
      "alwaysOutputData": false
    },
    {
      "id": "371b32e3-46de-41a7-9358-34761fb1a796",
      "name": "RSS Feed List",
      "type": "n8n-nodes-base.code",
      "notes": "\ud83d\udcf0 Provide a list of RSS feeds (Security, Privacy, Compliance). Customize URLs as needed.",
      "position": [
        -1120,
        -320
      ],
      "parameters": {
        "jsCode": "const feeds = [\n  { name: 'TechCrunch',    feedUrl: 'https://techcrunch.com/feed/' },\n  { name: 'The Verge',     feedUrl: 'https://www.theverge.com/rss/index.xml' },\n  { name: 'SmashingMag',    feedUrl: 'https://www.smashingmagazine.com/feed/' },\n];\n\n// 2) Transform into n8n-compatible output\nreturn feeds.map(feed => ({\n  json: {\n    name:    feed.name,\n    feedUrl: feed.feedUrl,\n  }\n}));"
      },
      "executeOnce": true,
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "2f441c7f-6acf-49a8-bfdc-7daca7d444d1",
      "name": "Splitting the Feeds",
      "type": "n8n-nodes-base.splitOut",
      "notes": "\ud83d\udd00 Split RSS list so each feed can be fetched individually.",
      "position": [
        -900,
        -320
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "feedUrl"
      },
      "notesInFlow": true,
      "typeVersion": 1
    },
    {
      "id": "745a6e0b-418a-4f1f-ace6-2b91ac5e0a8a",
      "name": "Vendor RSS Feed Read",
      "type": "n8n-nodes-base.rssFeedRead",
      "notes": "\ud83d\udce5 Reads RSS feed content per URL.",
      "position": [
        -680,
        -320
      ],
      "parameters": {
        "url": "={{ $json.feedUrl }}",
        "options": {}
      },
      "notesInFlow": true,
      "typeVersion": 1.1
    },
    {
      "id": "ec6e38d4-dcc4-4b16-8894-d4161a244291",
      "name": "Vendor URLs",
      "type": "n8n-nodes-base.code",
      "notes": "\ud83c\udf10 Provide a list of direct vendor policy webpage URLs to monitor updates.",
      "position": [
        -1140,
        260
      ],
      "parameters": {
        "jsCode": "const feeds = [\n    \n  {\n  name: 'test', feedUrl:'https://www.ted.com/about/our-organization/our-policies-terms/privacy-policy'},\n  {name: 'test2', feedUrl: 'https://www.teamviewer.com/en-in/global/support/knowledge-base/teamviewer-remote/devices/policies/'},\n];\n\n// 2) Transform into n8n-compatible output\nreturn feeds.map(feed => ({\n  json: {\n    name:    feed.name,\n    feedUrl: feed.feedUrl,\n  }\n}));"
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "7c705e85-bff5-4d71-9a82-5985b1f230c1",
      "name": "AI Agent1",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "notes": "\ud83e\udde0 Summarizes individual webpage content into short risk summaries.",
      "position": [
        560,
        260
      ],
      "parameters": {
        "text": "={{ $json.content }} & {{ $json.url }}",
        "options": {
          "systemMessage": "You are a Senior Risk and Compliance Analyst.\n\nYour job is to:\n- Read the content and generate a short 2-line summary\n- Categorize the risk as one of: High, Medium, Low, Informational\n\nReturn a clean JSON like:\n{\n  \"summary\": \"...\",\n  \"risk\": \"Medium\",\n  \"title\": \"...\",\n  \"Vendor Name\": \"....\",\n  \"url\": \"...\"\n}\n\nInput:\nTitle: {{$json[\"title\"]}}\nLink: {{$json[\"link\"]}}\nContent: {{$json[\"content\"]}}\n",
          "passthroughBinaryImages": false
        },
        "promptType": "define"
      },
      "notesInFlow": true,
      "typeVersion": 2,
      "alwaysOutputData": false
    },
    {
      "id": "225e4108-f4e1-428c-a62a-870fdf2e0eb4",
      "name": "Google Gemini Chat Model1",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        580,
        480
      ],
      "parameters": {
        "options": {},
        "modelName": "models/gemini-2.0-flash-001"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "92968717-e76c-4fd4-acad-7cae37e6ca07",
      "name": "Gmail",
      "type": "n8n-nodes-base.gmail",
      "notes": "\ud83d\udce8 Sends summary email for webpage policy updates.",
      "position": [
        1160,
        260
      ],
      "parameters": {
        "sendTo": "Email id here",
        "message": "={{ $json.html }}",
        "options": {},
        "subject": "=Vendor Webpage Change  {{$now}}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "typeVersion": 2.1
    },
    {
      "id": "d7e6e8bd-6f10-4107-b31f-c65650b40ce4",
      "name": "Gmail1",
      "type": "n8n-nodes-base.gmail",
      "notes": "\ud83d\udce7 Sends HTML newsletter summary email for RSS feeds.",
      "position": [
        960,
        -320
      ],
      "parameters": {
        "sendTo": "Email id here",
        "message": "={{ $json.html }}",
        "options": {},
        "subject": "=Vendor RSS Feed Monitoring as on {{ $now }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "typeVersion": 2.1
    },
    {
      "id": "dbe4724d-1914-40f3-b79b-48927a7dd62b",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        200,
        -200
      ],
      "parameters": {
        "options": {},
        "modelName": "models/gemini-2.0-flash-001"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "df8b19b8-a079-4776-9165-4272f65d1833",
      "name": "Filtering last 24hrs feed",
      "type": "n8n-nodes-base.filter",
      "notes": "\u23f3 Filters RSS articles published in the last 24 hours.",
      "position": [
        -460,
        -320
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "09fe1774-6a5a-4cf5-97c8-0afb7d44431e",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ new Date($json.isoDate).getTime() }}",
              "rightValue": "={{Date.now() - 24 * 60 * 60 * 1000 }}"
            },
            {
              "id": "93e97b32-40d7-4514-bc70-a7f51729b681",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ new Date($json.pubDate).getTime()}}",
              "rightValue": "={{ Date.now() - 24 * 60 * 60 * 1000 }}"
            }
          ]
        }
      },
      "notesInFlow": true,
      "typeVersion": 2.2
    },
    {
      "id": "2a4925f3-69cd-4162-9e65-dfd69f47e8c8",
      "name": "Formatting the Content for Mail",
      "type": "n8n-nodes-base.code",
      "notes": "\ud83d\udc8c Formats AI response as styled HTML grouped by risk category.",
      "position": [
        660,
        -320
      ],
      "parameters": {
        "jsCode": "const allItems = $input.all();\nlet combined = [];\n\nfor (const item of allItems) {\n  let rawOutput = item.json.output || '';\n\n  let cleaned = rawOutput\n    .replace(/^```json\\s*/, '')\n    .replace(/```$/, '')\n    .trim()\n    .replace(/,\\s*(\\]|\\})/g, '$1');\n\n  try {\n    const parsed = JSON.parse(cleaned);\n    for (const [riskLevel, entries] of Object.entries(parsed)) {\n      if (Array.isArray(entries)) {\n        entries.forEach(entry => {\n          combined.push({\n            risk: riskLevel,\n            summary: entry.summary,\n            link: entry.link,\n            title: entry.title || '',\n            vendor: entry['Vendor Name'] || ''\n          });\n        });\n      }\n    }\n  } catch (err) {\n    throw new Error(\"Invalid JSON in input item: \" + err.message);\n  }\n}\n\nconst riskData = { High: [], Medium: [], Low: [], Informational: [] };\ncombined.forEach(entry => {\n  const risk = entry.risk || 'Informational';\n  riskData[risk]?.push(entry);\n});\n\nconst categoryStyles = {\n  High: { color: '#f28b82', emoji: '\ud83d\udd34' },\n  Medium: { color: '#fbbc04', emoji: '\ud83d\udfe0' },\n  Low: { color: '#81c995', emoji: '\ud83d\udfe2' },\n  Informational: { color: '#a7c7e7', emoji: '\ud83d\udd35' }\n};\n\nlet html = `\n<!DOCTYPE html>\n<html>\n<head>\n  <style>\n    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');\n    body {\n      font-family: 'Inter', sans-serif;\n      background: #fff;\n      padding: 32px;\n      margin: 0;\n      color: #333;\n    }\n    .risk-YOUR_OPENAI_KEY_HERE {\n      font-size: 22px;\n      font-weight: 700;\n      margin: 30px 0 20px;\n      padding: 16px 24px;\n      border-radius: 16px;\n      color: #fff;\n      display: inline-block;\n      box-shadow: 0 2px 8px rgba(0,0,0,0.08);\n    }\n    .card {\n      background: #fff;\n      border-left: 6px solid;\n      border-radius: 12px;\n      padding: 24px;\n      margin: 20px 0;\n      box-shadow: 0 1px 6px rgba(0,0,0,0.06);\n      max-width: 800px;\n    }\n    .vendor {\n      font-size: 18px;\n      font-weight: 700;\n      color: #111;\n      margin-bottom: 6px;\n    }\n    .title {\n      font-size: 15px;\n      font-weight: 600;\n      color: #444;\n      margin-bottom: 14px;\n    }\n    .summary {\n      font-size: 15px;\n      line-height: 1.6;\n      margin-bottom: 12px;\n    }\n    .read-more a {\n      font-size: 14px;\n      color: #1a73e8;\n      text-decoration: none;\n      font-weight: 500;\n    }\n    .read-more a:hover {\n      text-decoration: underline;\n    }\n  </style>\n</head>\n<body>\n`;\n\nfor (const [category, entries] of Object.entries(riskData)) {\n  if (entries.length === 0) continue;\n  const { color, emoji } = categoryStyles[category];\n  html += `<div class=\"risk-YOUR_OPENAI_KEY_HERE\" style=\"background-color: ${color};\">${emoji} ${category} Risk</div>`;\n  entries.forEach(item => {\n    html += `\n      <div class=\"card\" style=\"border-left-color: ${color};\">\n        <div class=\"vendor\">${item.vendor || '\u2014'}</div>\n        <div class=\"title\">${item.title || '\u2014'}</div>\n        <div class=\"summary\">${item.summary}</div>\n        <div class=\"read-more\"><a href=\"${item.link}\" target=\"_blank\" rel=\"noopener noreferrer\">Read more</a></div>\n      </div>\n    `;\n  });\n}\n\nhtml += `</body></html>`;\n\nreturn [{ json: { html } }];\n"
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "1e2a44d6-bc8b-42e8-abe9-c08e9dfde4a5",
      "name": "Sort the feeds",
      "type": "n8n-nodes-base.sort",
      "notes": "\ud83d\udd03 Sort the RSS entries by pubDate for consistent order.",
      "position": [
        -240,
        -320
      ],
      "parameters": {
        "options": {},
        "sortFieldsUi": {
          "sortField": [
            {
              "fieldName": "pubDate"
            }
          ]
        }
      },
      "notesInFlow": true,
      "typeVersion": 1
    },
    {
      "id": "4a7bb247-bc86-4228-b4a4-9a94e5bfcfb4",
      "name": "Merge the content for AI",
      "type": "n8n-nodes-base.code",
      "notes": "\ud83e\udde9 Merges cleaned RSS entries into one payload for the AI agent.",
      "position": [
        -20,
        -320
      ],
      "parameters": {
        "jsCode": "// Build an array of { link, content } pairs\nconst entries = items.map(item => ({\n  link:    item.json.link,\n  content: item.json.contentSnippet || item.json.content || \"\"\n}));\n\n// Return one item with a single field \u201centries\u201d\nreturn [\n  {\n    json: {\n      entries\n    }\n  }\n];\n"
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "1a4ca2a0-1379-4140-b9a0-f0a7efd5e774",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "notes": "\ud83d\udd00 Merge both page meta and body-based content analysis.",
      "position": [
        -40,
        260
      ],
      "parameters": {},
      "notesInFlow": true,
      "typeVersion": 3.2
    },
    {
      "id": "d8a76c14-7aad-47f8-9b62-8254bb1e2d01",
      "name": "Format the Summary for Email",
      "type": "n8n-nodes-base.code",
      "notes": "\ud83e\uddfe Formats webpage summary output into email-ready HTML.",
      "position": [
        940,
        260
      ],
      "parameters": {
        "jsCode": "const allItems = $input.all();\nlet combined = [];\n\nfor (const item of allItems) {\n  let rawOutput = item.json.output || '';\n\n  let cleaned = rawOutput\n    .replace(/^```json\\s*/, '')\n    .replace(/```$/, '')\n    .trim()\n    .replace(/,\\s*(\\]|\\})/g, '$1');\n\n  try {\n    const parsed = JSON.parse(cleaned);\n    if (Array.isArray(parsed)) {\n      combined.push(...parsed);\n    } else if (parsed && parsed.risk && parsed.summary) {\n      combined.push(parsed);\n    }\n  } catch (err) {\n    throw new Error(\"Invalid JSON in input item: \" + err.message);\n  }\n}\n\n// Group by risk\nlet riskData = {\n  High: [],\n  Medium: [],\n  Low: [],\n  Informational: []\n};\n\ncombined.forEach(entry => {\n  const risk = entry.risk || 'Informational';\n  riskData[risk]?.push(entry);\n});\n\nconst categoryStyles = {\n  High: { color: '#f28b82', emoji: '\ud83d\udd34' },\n  Medium: { color: '#fbbc04', emoji: '\ud83d\udfe0' },\n  Low: { color: '#81c995', emoji: '\ud83d\udfe2' },\n  Informational: { color: '#a7c7e7', emoji: '\ud83d\udd35' },\n};\n\nlet html = `\n<!DOCTYPE html>\n<html>\n<head>\n  <style>\n    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');\n    body {\n      font-family: 'Inter', sans-serif;\n      background: #fff;\n      padding: 32px;\n      margin: 0;\n      color: #333;\n    }\n    .risk-YOUR_OPENAI_KEY_HERE {\n      font-size: 22px;\n      font-weight: 700;\n      margin: 30px 0 20px;\n      padding: 16px 24px;\n      border-radius: 16px;\n      color: #fff;\n      display: inline-block;\n      box-shadow: 0 2px 8px rgba(0,0,0,0.08);\n    }\n    .card {\n      background: #fff;\n      border-left: 6px solid;\n      border-radius: 12px;\n      padding: 24px;\n      margin: 20px 0;\n      box-shadow: 0 1px 6px rgba(0,0,0,0.06);\n      max-width: 800px;\n    }\n    .vendor {\n      font-size: 18px;\n      font-weight: 700;\n      color: #111;\n      margin-bottom: 6px;\n    }\n    .title {\n      font-size: 15px;\n      font-weight: 600;\n      color: #444;\n      margin-bottom: 14px;\n    }\n    .summary {\n      font-size: 15px;\n      line-height: 1.6;\n      margin-bottom: 12px;\n    }\n    .read-more a {\n      font-size: 14px;\n      color: #1a73e8;\n      text-decoration: none;\n      font-weight: 500;\n    }\n    .read-more a:hover {\n      text-decoration: underline;\n    }\n  </style>\n</head>\n<body>\n`;\n\nfor (const [category, entries] of Object.entries(riskData)) {\n  if (entries.length === 0) continue;\n  const { color, emoji } = categoryStyles[category];\n  html += `<div class=\"risk-YOUR_OPENAI_KEY_HERE\" style=\"background-color: ${color};\">${emoji} ${category} Risk</div>`;\n  entries.forEach(item => {\n    html += `\n      <div class=\"card\" style=\"border-left-color: ${color};\">\n        <div class=\"vendor\">${item['Vendor Name'] || 'Vendor Unknown'}</div>\n        <div class=\"title\">${item.title || 'Untitled Update'}</div>\n        <div class=\"summary\">${item.summary}</div>\n        <div class=\"read-more\"><a href=\"${item.url || item.link}\" target=\"_blank\" rel=\"noopener noreferrer\">Read more</a></div>\n      </div>\n    `;\n  });\n}\n\nhtml += `</body></html>`;\n\nreturn [{ json: { html } }];\n"
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "eb66887a-7de8-4151-8368-9e394084bdce",
      "name": "Scrapping inside the Webpage content",
      "type": "n8n-nodes-base.code",
      "notes": "\ud83d\udd0d Inspects webpage HTML body to find 'update/modified' dates.",
      "position": [
        -240,
        380
      ],
      "parameters": {
        "jsCode": "const now = new Date();\nconst results = [];\n\nconst datePatterns = [\n  /\\b(\\d{4}-\\d{2}-\\d{2}(?:[ T]\\d{2}:\\d{2}:\\d{2}(?:Z|[+\\-]\\d{2}:\\d{2})?)?)\\b/g,  // ISO + timezone\n  /\\b(\\d{4}\\/\\d{2}\\/\\d{2}(?:[ T]\\d{2}:\\d{2}:\\d{2})?)\\b/g,\n  /\\b(\\d{2}\\/\\d{2}\\/\\d{4}(?: \\d{2}:\\d{2}:\\d{2})?)\\b/g,\n  /\\b(\\d{2}\\.\\d{2}\\.\\d{4})\\b/g,\n  /\\b([A-Z][a-z]{2,8} \\d{1,2},? \\d{4})\\b/g,\n  /\\b(\\d{1,2} [A-Z][a-z]{2,8} \\d{4})\\b/g,\n  /\\b([A-Z][a-z]{2},? \\d{1,2} [A-Z][a-z]{2} \\d{4})\\b/g  // e.g., Fri, 20 Jun 2025\n];\n\nconst keywordRegex = /(updated|modified|published)/i;\nconst relativeRegex = /(updated|modified|published)[^\\d]{0,30}(today|yesterday)/i;\nconst keywordDateRegex = new RegExp(\n  `(updated|modified|published)[^\\\\dA-Z]{0,30}[:\\\\-\\\\s]*?(\\\\d{4}-\\\\d{2}-\\\\d{2}(?:[ T]\\\\d{2}:\\\\d{2}:\\\\d{2})?|\\\\d{2}\\\\/\\\\d{2}\\\\/\\\\d{4}|\\\\d{2}\\\\.\\\\d{2}\\\\.\\\\d{4}|[A-Z][a-z]{2,8} \\\\d{1,2},? \\\\d{4})`,\n  'i'\n);\n\nfor (const item of items) {\n  const content = item.json.data || \"\";\n  let found = false;\n  let matchedDate = null;\n  let matchedType = null;\n  let matchedKeyword = null;\n  let note = \"\";\n  let published = null;\n\n  // --- 1. Direct keyword-date detection (e.g. Updated: 2025-06-20)\n  const directMatch = content.match(keywordDateRegex);\n  if (directMatch) {\n    matchedKeyword = directMatch[1];\n    const dateStr = directMatch[2];\n    const parsedDate = new Date(dateStr);\n\n    if (!isNaN(parsedDate)) {\n      const isRecent = (() => {\n        const diffMs = now - parsedDate;\n        if (/\\d{2}:\\d{2}:\\d{2}/.test(dateStr)) {\n          return diffMs >= 0 && diffMs <= 24 * 60 * 60 * 1000;\n        } else {\n          const test = new Date(parsedDate); test.setHours(0,0,0,0);\n          const nowTrimmed = new Date(now); nowTrimmed.setHours(0,0,0,0);\n          const diffDays = (nowTrimmed - test) / (24 * 60 * 60 * 1000);\n          return diffDays === 0 || diffDays === 1;\n        }\n      })();\n\n      if (isRecent) {\n        found = true;\n        matchedDate = dateStr;\n        matchedType = /\\d{2}:\\d{2}:\\d{2}/.test(dateStr) ? \"datetime\" : \"date-only\";\n        note = `Found '${matchedKeyword}' with ${matchedType} \"${dateStr}\" (recent).`;\n      }\n    }\n  }\n\n  // --- 2. Relative phrases like \"Updated yesterday\"\n  if (!found) {\n    const relMatch = content.match(relativeRegex);\n    if (relMatch) {\n      matchedKeyword = relMatch[1];\n      const rel = relMatch[2].toLowerCase();\n      if (rel === \"today\" || rel === \"yesterday\") {\n        const refDate = new Date(now);\n        if (rel === \"yesterday\") refDate.setDate(refDate.getDate() - 1);\n        const isoDate = refDate.toISOString().split(\"T\")[0];\n        found = true;\n        matchedDate = isoDate;\n        matchedType = \"relative\";\n        note = `Found '${matchedKeyword}' with relative date \"${rel}\".`;\n      }\n    }\n  }\n\n  // --- 3. Fallback: Any keyword in proximity of any valid date\n  if (!found && keywordRegex.test(content)) {\n    for (const pat of datePatterns) {\n      pat.lastIndex = 0;\n      let dateMatch;\n      while ((dateMatch = pat.exec(content)) !== null) {\n        const dateStr = dateMatch[1];\n        const parsedDate = new Date(dateStr);\n        if (!isNaN(parsedDate)) {\n          const isTime = /\\d{2}:\\d{2}:\\d{2}/.test(dateStr);\n          const diff = isTime ? now - parsedDate : (() => {\n            const pd = new Date(parsedDate); pd.setHours(0,0,0,0);\n            const nd = new Date(now); nd.setHours(0,0,0,0);\n            return (nd - pd);\n          })();\n          const withinLimit = isTime ? diff >= 0 && diff <= 86400000 : (diff === 0 || diff === 86400000);\n          if (withinLimit) {\n            found = true;\n            matchedKeyword = \"generic-fallback\";\n            matchedDate = dateStr;\n            matchedType = isTime ? \"datetime\" : \"date-only\";\n            note = `Fallback: keyword present and found nearby date \"${dateStr}\" (recent).`;\n            break;\n          }\n        }\n      }\n      if (found) break;\n    }\n  }\n\n  // --- Extract published (non-filtering fallback)\n  if (!published) {\n    for (const pat of datePatterns) {\n      const match = pat.exec(content);\n      if (match && Date.parse(match[1])) {\n        published = match[1];\n        break;\n      }\n    }\n  }\n\n  // --- Return only matched results\n  if (!found) continue;\n\n  results.push({\n    json: {\n      keywordDetected: true,\n      matchedKeyword,\n      matchedDate,\n      matchedType,\n      published: published || \"Not available\",\n      note,\n      data: content\n    }\n  });\n}\n\nreturn results;\n"
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "dd0073e0-dbe8-4a46-953f-cc3759160817",
      "name": "Cleaning up the code",
      "type": "n8n-nodes-base.code",
      "notes": "\ud83e\uddf9 Cleans and strips raw HTML into readable content for AI processing.",
      "position": [
        260,
        260
      ],
      "parameters": {
        "jsCode": "const items = [];\n\nfor (const item of $input.all()) {\n  const rawHtml = item.json.body || item.json.data || \"\";\n  let urlFromRequest = item.json.url || item.json.requestUrl || item.json.headers?.['x-final-url'] || '';\n\n  let html = rawHtml.toString();\n\n  // Try to extract canonical URL or og:url from HTML head\n  let extractedUrl = '';\n\n  const canonicalMatch = html.match(/<link[^>]+rel=[\"']canonical[\"'][^>]*href=[\"']([^\"']+)[\"']/i);\n  if (canonicalMatch) {\n    extractedUrl = canonicalMatch[1];\n  } else {\n    const ogUrlMatch = html.match(/<meta[^>]+property=[\"']og:url[\"'][^>]*content=[\"']([^\"']+)[\"']/i);\n    if (ogUrlMatch) {\n      extractedUrl = ogUrlMatch[1];\n    }\n  }\n\n  const finalUrl = extractedUrl || urlFromRequest;\n\n  // Strip scripts/styles/noscript/iframe/svg/etc.\n  html = html.replace(/<script[\\s\\S]*?>[\\s\\S]*?<\\/script>/gi, '');\n  html = html.replace(/<style[\\s\\S]*?>[\\s\\S]*?<\\/style>/gi, '');\n  html = html.replace(/<noscript[\\s\\S]*?>[\\s\\S]*?<\\/noscript>/gi, '');\n  html = html.replace(/<iframe[\\s\\S]*?>[\\s\\S]*?<\\/iframe>/gi, '');\n  html = html.replace(/<svg[\\s\\S]*?>[\\s\\S]*?<\\/svg>/gi, '');\n  html = html.replace(/<!--[\\s\\S]*?-->/g, '');\n  html = html.replace(/<\\/?[^>]+(>|$)/g, '');\n\n  // Decode HTML entities\n  html = html\n    .replace(/&nbsp;/g, ' ')\n    .replace(/&amp;/g, '&')\n    .replace(/&quot;/g, '\"')\n    .replace(/&lt;/g, '<')\n    .replace(/&gt;/g, '>');\n\n  // Clean whitespace\n  let content = html.replace(/\\s{2,}/g, ' ').trim();\n\n  // Limit size (safe for AI)\n  const maxLength = 30000;\n  if (content.length > maxLength) content = content.slice(0, maxLength) + '... [truncated]';\n\n  items.push({\n    json: {\n      url: finalUrl,\n      content\n    }\n  });\n}\n\nreturn items;\n"
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "c37f2b54-bdd5-4177-b70d-b48226692b7b",
      "name": "Daily Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "notes": "\u23f0 Triggers the workflow every day at 3 AM to initiate all newsletter jobs.",
      "position": [
        -1740,
        20
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 3
            }
          ]
        }
      },
      "notesInFlow": true,
      "typeVersion": 1.2
    },
    {
      "id": "7062eea3-a57a-4eb3-89f1-60209f9ed518",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1200,
        -460
      ],
      "parameters": {
        "color": 6,
        "width": 2540,
        "height": 460,
        "content": "## \u2733\ufe0f  RSS Feed-Based Monitoring\n"
      },
      "typeVersion": 1
    },
    {
      "id": "03543938-705f-4ac9-9324-3316e1fa75f1",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1200,
        160
      ],
      "parameters": {
        "color": 4,
        "width": 2560,
        "height": 460,
        "content": "## \ud83d\udd0e Direct Webpage Monitoring"
      },
      "typeVersion": 1
    },
    {
      "id": "13f31425-21fd-4485-b40b-1bccbb18db6b",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1140,
        440
      ],
      "parameters": {
        "width": 170,
        "height": 140,
        "content": "## \u2b06\ufe0f  Provide your vendor URLs here"
      },
      "typeVersion": 1
    },
    {
      "id": "1f0d93fa-8ba9-4389-abca-5ea4f75ecbec",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1140,
        -160
      ],
      "parameters": {
        "width": 170,
        "height": 140,
        "content": "## \u2b06\ufe0f  Provide your vendor URLs here"
      },
      "typeVersion": 1
    },
    {
      "id": "e210deb2-b084-4bd9-8355-6c1b1bb85fd9",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        980,
        -160
      ],
      "parameters": {
        "width": 150,
        "height": 80,
        "content": "## \ud83d\udce7 Add your email"
      },
      "typeVersion": 1
    },
    {
      "id": "3ce9e429-f52e-49e1-aaa4-d9c4e59a4469",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1160,
        440
      ],
      "parameters": {
        "width": 150,
        "height": 80,
        "content": "## \ud83d\udce7 Add your email"
      },
      "typeVersion": 1
    },
    {
      "id": "e6c24c42-8c67-4d2d-93e2-e7f7a7bfce4f",
      "name": "Splitting the Urls",
      "type": "n8n-nodes-base.splitOut",
      "notes": "\ud83d\udd00 Split array of vendor URLs to process them individually.",
      "position": [
        -920,
        260
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "feedUrl"
      },
      "notesInFlow": true,
      "typeVersion": 1
    },
    {
      "id": "0d873aa3-baa9-4632-b489-bf439ed7d897",
      "name": "Check the headers",
      "type": "n8n-nodes-base.if",
      "notes": "\ud83d\udd75\ufe0f\u200d\u2642\ufe0f Checks vendor page headers for signs of recent updates (last modified).",
      "position": [
        -480,
        260
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "3cc0b4e8-e015-44fc-8118-ec7ec79dc2de",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{    Object.entries($json.headers).some(([key, val]) =>     /update|modified/i.test(key) &&     !isNaN(Date.parse(val)) &&     Date.parse(val) >= Date.now() - 24*60*60*1000   ) }}",
              "rightValue": ""
            }
          ]
        }
      },
      "notesInFlow": true,
      "typeVersion": 2.2
    },
    {
      "id": "059afda6-dc5f-457f-bdec-f6acd7feb161",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2700,
        -880
      ],
      "parameters": {
        "width": 800,
        "height": 1840,
        "content": "## \ud83d\udee1\ufe0f AI-Powered Vendor Policy & RSS Feed Analysis with Integrated Risk Scoring\n\nThis workflow automates monitoring, analyzing, and scoring vendor-related updates using RSS feeds and direct web scraping. It leverages AI agents to produce actionable, risk-YOUR_OPENAI_KEY_HERE summaries delivered via email in a clean, styled HTML format.\n\n### \ud83d\ude80 Features\n- \ud83d\udce5 **Pulls updates** from RSS feeds and vendor policy pages  \n- \ud83e\udd16 **Uses AI agents** to analyze and assign risk ratings (High, Medium, Low, Informational)  \n- \ud83e\uddf9 **Extracts and cleans** web content for LLM processing  \n- \ud83d\udd75\ufe0f **Filters changes** within last 24 hours using headers, body text, and AI-based date detection  \n- \ud83d\udc8c **Sends separate HTML email digests** for RSS and webpage-based insights  \n\n### \ud83e\uddf1 Workflow Modules\n\n#### \ud83d\udce1 RSS Feed Monitoring\n- \ud83d\udd17 Reads vendor feed URLs  \n- \u23f3 Filters posts from the last 24 hours  \n- \ud83e\udde0 Sends entries to AI agent for risk assessment and summarization  \n\n#### \ud83c\udf10 Webpage Monitoring\n- \ud83d\udd0d Scrapes vendor policy URLs  \n- \ud83d\udcc5 Extracts update dates using:\n  - \ud83e\uddfe *Last-Modified headers*\n  - \ud83e\udde0 *HTML body scan* for keywords like \u201cupdated\u201d or \u201cmodified\u201d\n- \u270d\ufe0f Uses AI to extract, summarize, and categorize risk content  \n\n#### \ud83e\uddfe Output\n- \ud83d\uddc2\ufe0f Formats summaries by risk level  \n- \ud83d\udce7 Sends HTML digest emails to designated recipients  \n\n---\n\n### \ud83e\uddea Use Case Scenarios\n\n#### \u2705 Use Case 1: RSS Feed-Based Risk Categorization\n\n**Scenario:**  \nYour organization subscribes to cybersecurity and compliance blogs like TechCrunch or Smashing Magazine. You want to identify which new updates might indicate High or Medium vendor risk.\n\n**Workflow Behavior:**\n- \ud83d\uddde\ufe0f Filters feed articles from the last 24 hours  \n- \ud83e\udde0 AI assigns risk level and summarizes content  \n- \ud83d\udcec Email shows grouped insights (\ud83d\udd34 High, \ud83d\udfe0 Medium, etc.)\n\n**Example Output:**\n\n\ud83d\udd34 **High Risk**  \n**Vendor:** Vendor name  \n**Title:** \u201cZero-Day Vulnerability in XYZ\u201d  \n**Summary:** A critical zero-day was reported affecting vendor systems. Immediate patching is required.  \n\ud83d\udc49 *Read more*\n\n---\n\n#### \u2705 Use Case 2: Vendor Policy Page Monitoring\n\n**Scenario:**  \nYou need to track privacy or terms-of-service pages for subtle but important changes.\n\n**Workflow Behavior:**\n- \ud83c\udf10 Scrapes specified vendor URLs (e.g., Privacy Policy)  \n- \ud83d\udcc5 Extracts \u201cLast Modified\u201d date or infers updates via HTML content  \n- \ud83e\udd16 AI summarizes the change and assesses risk  \n\n**Example Output:**\n\n\ud83d\udfe0 **Medium Risk**  \n**Vendor:** Vendor name  \n**Title:** \u201cPrivacy Policy Updated\u201d  \n**Summary:** The vendor added new clauses around third-party data sharing. Legal review recommended.  \n\ud83d\udc49 *Read more*\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "d815af19-e2bb-4f3a-b954-c10b47d98347",
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "Cleaning up the code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent": {
      "main": [
        [
          {
            "node": "Formatting the Content for Mail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent1": {
      "main": [
        [
          {
            "node": "Format the Summary for Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vendor URLs": {
      "main": [
        [
          {
            "node": "Splitting the Urls",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request": {
      "main": [
        [
          {
            "node": "Check the headers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily Trigger": {
      "main": [
        [
          {
            "node": "Vendor URLs",
            "type": "main",
            "index": 0
          },
          {
            "node": "RSS Feed List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RSS Feed List": {
      "main": [
        [
          {
            "node": "Splitting the Feeds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sort the feeds": {
      "main": [
        [
          {
            "node": "Merge the content for AI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check the headers": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Scrapping inside the Webpage content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Splitting the Urls": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Splitting the Feeds": {
      "main": [
        [
          {
            "node": "Vendor RSS Feed Read",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cleaning up the code": {
      "main": [
        [
          {
            "node": "AI Agent1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vendor RSS Feed Read": {
      "main": [
        [
          {
            "node": "Filtering last 24hrs feed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Merge the content for AI": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filtering last 24hrs feed": {
      "main": [
        [
          {
            "node": "Sort the feeds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent1",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Format the Summary for Email": {
      "main": [
        [
          {
            "node": "Gmail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Formatting the Content for Mail": {
      "main": [
        [
          {
            "node": "Gmail1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrapping inside the Webpage content": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    }
  }
}

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 dual-engine, AI-driven n8n workflow that automates the monitoring of both vendor policy webpages and compliance-related RSS feeds. It intelligently detects recent updates, evaluates their potential risk, and delivers a structured HTML digest categorized by severity — right to…

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

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

This workflow contains community nodes that are only compatible with the self-hosted version of n8n.

RSS Feed Read, @Brightdata/N8N Nodes Brightdata, Agent +4
AI & RAG

AI powered workflow that scans HR news via RSS, checks which of your policies or contract templates might need updates, and sends a weekly internal newsletter as HTML.

RSS Feed Read, HTTP Request, Google Drive +5
AI & RAG

LinkedIn_Job_Hunt_and_Cover_Letter. Uses outputParserStructured, outputParserAutofixing, googleDrive, agent. Scheduled trigger; 85 nodes.

Output Parser Structured, Output Parser Autofixing, Google Drive +6
AI & RAG

Automatically scan major financial newswires for biotech catalyst events, score them with AI sentiment analysis, and surface ranked trade candidates — all without manual monitoring.

RSS Feed Read, Data Table, HTTP Request +4
AI & RAG

The Multi-Model Agency Content Engine is a high-performance editorial system designed for agencies. It solves the "blank page" problem by alternating between real-world social proof and strategic expe

Google Sheets, Gmail, Google Drive +6