AI 기반 공급업체 정책 및 RSS 피드 분석 및 위험 점수 매기기
고급
이것은AI분야의자동화 워크플로우로, 29개의 노드를 포함합니다.주로 If, Code, Sort, Gmail, Merge 등의 노드를 사용하며인공지능 기술을 결합하여 스마트 자동화를 구현합니다. 리스크 점수 통합 AI 공급업체 정책 및 RSS 피드 분석 시스템
사전 요구사항
- •Google 계정 및 Gmail API 인증 정보
- •대상 API의 인증 정보가 필요할 수 있음
- •Google Gemini API Key
사용된 노드 (29)
카테고리
워크플로우 미리보기
노드 연결 관계를 시각적으로 표시하며, 확대/축소 및 이동을 지원합니다
워크플로우 내보내기
다음 JSON 구성을 복사하여 n8n에 가져오면 이 워크플로우를 사용할 수 있습니다
{
"id": "DHLbKyEoYyzkhEBA",
"meta": {
"instanceId": "818946a823ca34034ea73f38168c80e4b7ae5021e8280b7645e14b95574ba259"
},
"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": "🌐 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 에이전트",
"type": "@n8n/n8n-nodes-langchain.agent",
"notes": "🤖 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🧾 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://…\",\n \"content\": \"Snippet or article content here\"\n}\n\n🧠 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✅ 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 피드 목록",
"type": "n8n-nodes-base.code",
"notes": "📰 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": "피드 분할",
"type": "n8n-nodes-base.splitOut",
"notes": "🔀 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": "공급업체 RSS 피드 읽기",
"type": "n8n-nodes-base.rssFeedRead",
"notes": "📥 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": "공급업체 URL",
"type": "n8n-nodes-base.code",
"notes": "🌐 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 에이전트1",
"type": "@n8n/n8n-nodes-langchain.agent",
"notes": "🧠 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": {
"id": "3Ekb1D8LUyg2zvwJ",
"name": "Google Gemini(PaLM) Api account"
}
},
"typeVersion": 1
},
{
"id": "92968717-e76c-4fd4-acad-7cae37e6ca07",
"name": "Gmail",
"type": "n8n-nodes-base.gmail",
"notes": "📨 Sends summary email for webpage policy updates.",
"position": [
1160,
260
],
"webhookId": "d33f7afd-47c3-4616-9a37-6319d93c0e92",
"parameters": {
"sendTo": "Email id here",
"message": "={{ $json.html }}",
"options": {},
"subject": "=Vendor Webpage Change {{$now}}"
},
"credentials": {
"gmailOAuth2": {
"id": "DCglvd9VagxSYbJx",
"name": "Gmail account"
}
},
"notesInFlow": true,
"typeVersion": 2.1
},
{
"id": "d7e6e8bd-6f10-4107-b31f-c65650b40ce4",
"name": "Gmail1",
"type": "n8n-nodes-base.gmail",
"notes": "📧 Sends HTML newsletter summary email for RSS feeds.",
"position": [
960,
-320
],
"webhookId": "66d44179-c4aa-4acd-b61e-d91a1f888d9d",
"parameters": {
"sendTo": "Email id here",
"message": "={{ $json.html }}",
"options": {},
"subject": "=Vendor RSS Feed Monitoring as on {{ $now }}"
},
"credentials": {
"gmailOAuth2": {
"id": "DCglvd9VagxSYbJx",
"name": "Gmail account"
}
},
"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": {
"id": "3Ekb1D8LUyg2zvwJ",
"name": "Google Gemini(PaLM) Api account"
}
},
"typeVersion": 1
},
{
"id": "df8b19b8-a079-4776-9165-4272f65d1833",
"name": "지난 24시간 피드 필터링",
"type": "n8n-nodes-base.filter",
"notes": "⏳ 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": "메일용 콘텐츠 포맷팅",
"type": "n8n-nodes-base.code",
"notes": "💌 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: '🔴' },\n Medium: { color: '#fbbc04', emoji: '🟠' },\n Low: { color: '#81c995', emoji: '🟢' },\n Informational: { color: '#a7c7e7', emoji: '🔵' }\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-heading {\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-heading\" 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 || '—'}</div>\n <div class=\"title\">${item.title || '—'}</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": "피드 정렬",
"type": "n8n-nodes-base.sort",
"notes": "🔃 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": "AI용 콘텐츠 병합",
"type": "n8n-nodes-base.code",
"notes": "🧩 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 “entries”\nreturn [\n {\n json: {\n entries\n }\n }\n];\n"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "1a4ca2a0-1379-4140-b9a0-f0a7efd5e774",
"name": "병합",
"type": "n8n-nodes-base.merge",
"notes": "🔀 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": "이메일용 요약 포맷팅",
"type": "n8n-nodes-base.code",
"notes": "🧾 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: '🔴' },\n Medium: { color: '#fbbc04', emoji: '🟠' },\n Low: { color: '#81c995', emoji: '🟢' },\n Informational: { color: '#a7c7e7', emoji: '🔵' },\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-heading {\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-heading\" 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": "웹페이지 내부 콘텐츠 스크래핑",
"type": "n8n-nodes-base.code",
"notes": "🔍 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": "코드 정리",
"type": "n8n-nodes-base.code",
"notes": "🧹 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(/ /g, ' ')\n .replace(/&/g, '&')\n .replace(/"/g, '\"')\n .replace(/</g, '<')\n .replace(/>/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": "일일 트리거",
"type": "n8n-nodes-base.scheduleTrigger",
"notes": "⏰ 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": "스티커 노트1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1200,
-460
],
"parameters": {
"color": 6,
"width": 2540,
"height": 460,
"content": "## ✳️ RSS Feed-Based Monitoring\n"
},
"typeVersion": 1
},
{
"id": "03543938-705f-4ac9-9324-3316e1fa75f1",
"name": "스티커 노트",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1200,
160
],
"parameters": {
"color": 4,
"width": 2560,
"height": 460,
"content": "## 🔎 Direct Webpage Monitoring"
},
"typeVersion": 1
},
{
"id": "13f31425-21fd-4485-b40b-1bccbb18db6b",
"name": "스티커 노트2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1140,
440
],
"parameters": {
"width": 170,
"height": 140,
"content": "## ⬆️ Provide your vendor URLs here"
},
"typeVersion": 1
},
{
"id": "1f0d93fa-8ba9-4389-abca-5ea4f75ecbec",
"name": "스티커 노트3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1140,
-160
],
"parameters": {
"width": 170,
"height": 140,
"content": "## ⬆️ Provide your vendor URLs here"
},
"typeVersion": 1
},
{
"id": "e210deb2-b084-4bd9-8355-6c1b1bb85fd9",
"name": "스티커 노트4",
"type": "n8n-nodes-base.stickyNote",
"position": [
980,
-160
],
"parameters": {
"width": 150,
"height": 80,
"content": "## 📧 Add your email"
},
"typeVersion": 1
},
{
"id": "3ce9e429-f52e-49e1-aaa4-d9c4e59a4469",
"name": "스티커 노트5",
"type": "n8n-nodes-base.stickyNote",
"position": [
1160,
440
],
"parameters": {
"width": 150,
"height": 80,
"content": "## 📧 Add your email"
},
"typeVersion": 1
},
{
"id": "e6c24c42-8c67-4d2d-93e2-e7f7a7bfce4f",
"name": "URL 분할",
"type": "n8n-nodes-base.splitOut",
"notes": "🔀 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": "헤더 확인",
"type": "n8n-nodes-base.if",
"notes": "🕵️♂️ 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": "스티커 노트6",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2700,
-880
],
"parameters": {
"width": 800,
"height": 1840,
"content": "## 🛡️ 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-categorized summaries delivered via email in a clean, styled HTML format.\n\n### 🚀 Features\n- 📥 **Pulls updates** from RSS feeds and vendor policy pages \n- 🤖 **Uses AI agents** to analyze and assign risk ratings (High, Medium, Low, Informational) \n- 🧹 **Extracts and cleans** web content for LLM processing \n- 🕵️ **Filters changes** within last 24 hours using headers, body text, and AI-based date detection \n- 💌 **Sends separate HTML email digests** for RSS and webpage-based insights \n\n### 🧱 Workflow Modules\n\n#### 📡 RSS Feed Monitoring\n- 🔗 Reads vendor feed URLs \n- ⏳ Filters posts from the last 24 hours \n- 🧠 Sends entries to AI agent for risk assessment and summarization \n\n#### 🌐 Webpage Monitoring\n- 🔍 Scrapes vendor policy URLs \n- 📅 Extracts update dates using:\n - 🧾 *Last-Modified headers*\n - 🧠 *HTML body scan* for keywords like “updated” or “modified”\n- ✍️ Uses AI to extract, summarize, and categorize risk content \n\n#### 🧾 Output\n- 🗂️ Formats summaries by risk level \n- 📧 Sends HTML digest emails to designated recipients \n\n---\n\n### 🧪 Use Case Scenarios\n\n#### ✅ 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- 🗞️ Filters feed articles from the last 24 hours \n- 🧠 AI assigns risk level and summarizes content \n- 📬 Email shows grouped insights (🔴 High, 🟠 Medium, etc.)\n\n**Example Output:**\n\n🔴 **High Risk** \n**Vendor:** Vendor name \n**Title:** “Zero-Day Vulnerability in XYZ” \n**Summary:** A critical zero-day was reported affecting vendor systems. Immediate patching is required. \n👉 *Read more*\n\n---\n\n#### ✅ 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- 🌐 Scrapes specified vendor URLs (e.g., Privacy Policy) \n- 📅 Extracts “Last Modified” date or infers updates via HTML content \n- 🤖 AI summarizes the change and assesses risk \n\n**Example Output:**\n\n🟠 **Medium Risk** \n**Vendor:** Vendor name \n**Title:** “Privacy Policy Updated” \n**Summary:** The vendor added new clauses around third-party data sharing. Legal review recommended. \n👉 *Read more*\n"
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "d815af19-e2bb-4f3a-b954-c10b47d98347",
"connections": {
"1a4ca2a0-1379-4140-b9a0-f0a7efd5e774": {
"main": [
[
{
"node": "dd0073e0-dbe8-4a46-953f-cc3759160817",
"type": "main",
"index": 0
}
]
]
},
"209ac43c-361b-4cdf-b8f1-5cfadf3bace0": {
"main": [
[
{
"node": "2a4925f3-69cd-4162-9e65-dfd69f47e8c8",
"type": "main",
"index": 0
}
]
]
},
"7c705e85-bff5-4d71-9a82-5985b1f230c1": {
"main": [
[
{
"node": "d8a76c14-7aad-47f8-9b62-8254bb1e2d01",
"type": "main",
"index": 0
}
]
]
},
"ec6e38d4-dcc4-4b16-8894-d4161a244291": {
"main": [
[
{
"node": "e6c24c42-8c67-4d2d-93e2-e7f7a7bfce4f",
"type": "main",
"index": 0
}
]
]
},
"477ac26f-b2f9-48a9-ba04-8acff89225ff": {
"main": [
[
{
"node": "0d873aa3-baa9-4632-b489-bf439ed7d897",
"type": "main",
"index": 0
}
]
]
},
"c37f2b54-bdd5-4177-b70d-b48226692b7b": {
"main": [
[
{
"node": "ec6e38d4-dcc4-4b16-8894-d4161a244291",
"type": "main",
"index": 0
},
{
"node": "371b32e3-46de-41a7-9358-34761fb1a796",
"type": "main",
"index": 0
}
]
]
},
"371b32e3-46de-41a7-9358-34761fb1a796": {
"main": [
[
{
"node": "2f441c7f-6acf-49a8-bfdc-7daca7d444d1",
"type": "main",
"index": 0
}
]
]
},
"1e2a44d6-bc8b-42e8-abe9-c08e9dfde4a5": {
"main": [
[
{
"node": "4a7bb247-bc86-4228-b4a4-9a94e5bfcfb4",
"type": "main",
"index": 0
}
]
]
},
"0d873aa3-baa9-4632-b489-bf439ed7d897": {
"main": [
[
{
"node": "1a4ca2a0-1379-4140-b9a0-f0a7efd5e774",
"type": "main",
"index": 0
}
],
[
{
"node": "eb66887a-7de8-4151-8368-9e394084bdce",
"type": "main",
"index": 0
}
]
]
},
"e6c24c42-8c67-4d2d-93e2-e7f7a7bfce4f": {
"main": [
[
{
"node": "477ac26f-b2f9-48a9-ba04-8acff89225ff",
"type": "main",
"index": 0
}
]
]
},
"2f441c7f-6acf-49a8-bfdc-7daca7d444d1": {
"main": [
[
{
"node": "745a6e0b-418a-4f1f-ace6-2b91ac5e0a8a",
"type": "main",
"index": 0
}
]
]
},
"dd0073e0-dbe8-4a46-953f-cc3759160817": {
"main": [
[
{
"node": "7c705e85-bff5-4d71-9a82-5985b1f230c1",
"type": "main",
"index": 0
}
]
]
},
"745a6e0b-418a-4f1f-ace6-2b91ac5e0a8a": {
"main": [
[
{
"node": "df8b19b8-a079-4776-9165-4272f65d1833",
"type": "main",
"index": 0
}
]
]
},
"dbe4724d-1914-40f3-b79b-48927a7dd62b": {
"ai_languageModel": [
[
{
"node": "209ac43c-361b-4cdf-b8f1-5cfadf3bace0",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"4a7bb247-bc86-4228-b4a4-9a94e5bfcfb4": {
"main": [
[
{
"node": "209ac43c-361b-4cdf-b8f1-5cfadf3bace0",
"type": "main",
"index": 0
}
]
]
},
"df8b19b8-a079-4776-9165-4272f65d1833": {
"main": [
[
{
"node": "1e2a44d6-bc8b-42e8-abe9-c08e9dfde4a5",
"type": "main",
"index": 0
}
]
]
},
"225e4108-f4e1-428c-a62a-870fdf2e0eb4": {
"ai_languageModel": [
[
{
"node": "7c705e85-bff5-4d71-9a82-5985b1f230c1",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"d8a76c14-7aad-47f8-9b62-8254bb1e2d01": {
"main": [
[
{
"node": "92968717-e76c-4fd4-acad-7cae37e6ca07",
"type": "main",
"index": 0
}
]
]
},
"2a4925f3-69cd-4162-9e65-dfd69f47e8c8": {
"main": [
[
{
"node": "d7e6e8bd-6f10-4107-b31f-c65650b40ce4",
"type": "main",
"index": 0
}
]
]
},
"eb66887a-7de8-4151-8368-9e394084bdce": {
"main": [
[
{
"node": "1a4ca2a0-1379-4140-b9a0-f0a7efd5e774",
"type": "main",
"index": 1
}
]
]
}
}
}자주 묻는 질문
이 워크플로우를 어떻게 사용하나요?
위의 JSON 구성 코드를 복사하여 n8n 인스턴스에서 새 워크플로우를 생성하고 "JSON에서 가져오기"를 선택한 후, 구성을 붙여넣고 필요에 따라 인증 설정을 수정하세요.
이 워크플로우는 어떤 시나리오에 적합한가요?
고급 - 인공지능
유료인가요?
이 워크플로우는 완전히 무료이며 직접 가져와 사용할 수 있습니다. 다만, 워크플로우에서 사용하는 타사 서비스(예: OpenAI API)는 사용자 직접 비용을 지불해야 할 수 있습니다.
관련 워크플로우 추천
시각화 참조 라이브러리에서 n8n 노드를 탐색
可视化 참조 라이브러리에서 n8n 노드를 탐색
If
Ftp
Set
+
If
Ftp
Set
113 노드I versus AI
기타
AI를 사용한 LinkedIn RSS 처리
Gemini AI 및 RSS 피드를 사용한 LinkedIn 뉴스 게시 자동화
If
Set
Code
+
If
Set
Code
43 노드Club de Inteligencia Artificial Politécnico CIAP
인공지능
GPT-4.1, Outlook 및 Mem.ai를 사용한 Microsoft Teams 회의 분석 자동화
GPT-4.1, Outlook 및 Mem.ai를 사용한 자동화된 Microsoft Teams 회의 분석
If
Set
Code
+
If
Set
Code
61 노드Wayne Simpson
인사
안전 개인 정보 준수 구독 소스 지능 요약
안전, 개인 정보 보호 및 준수를 위한 지능형 AI 요약
Set
Code
Sort
+
Set
Code
Sort
43 노드Niranjan G
인공지능
MailChimp자동화
基于AI의餐厅通讯생성기,통합Mailchimp및Telegram审批
If
Set
Code
+
If
Set
Code
43 노드Femi Ad
인공지능
GPT-4o, WordPress, LinkedIn을 사용한 RSS 콘텐츠 자동 블로그 게시
GPT-4o, WordPress 및 LinkedIn을 사용한 RSS 콘텐츠 블로그 게시물 자동 발행
If
Set
Code
+
If
Set
Code
40 노드Immanuel
인공지능