This workflow corresponds to n8n.io template #17445 — we link there as the canonical source.
This workflow follows the Chainllm → Google Sheets 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 →
{
"id": "cdTsbS89f6ZoBmfj",
"name": "Filter, Deduplicate and Approve News Articles with Embeddings and a GPT Judge",
"tags": [
{
"id": "WiPfwUZYcO7Er2qC",
"name": "Template",
"createdAt": "2026-02-10T16:54:20.430Z",
"updatedAt": "2026-02-10T16:54:20.430Z"
}
],
"nodes": [
{
"id": "sticky_config",
"name": "Sticky config",
"type": "n8n-nodes-base.stickyNote",
"position": [
-976,
-400
],
"parameters": {
"color": 4,
"width": 480,
"height": 764,
"content": "## Edit knobs here, nothing else\nAll tuning lives in Configure context:\n- **sheetDocumentId / sourceTab / articlesTab / approvedTab** - the spreadsheet, the incoming-feed tab, the seen-articles ledger tab, and the output tab\n- **rulesKeywords** - term-to-weight map for Step 1; a headline hit counts double a body hit (word-boundary matched)\n- **sourceWeights** - domain-to-weight map (live articles carry their feed's domain as source); unknown domains score 0\n- **rulesRejectThreshold** - score below this = rejected immediately (default 2)\n- **rulesHardWinThreshold** - score at or above this skips the LLM judge (default 12)\n- **clusterSimilarityThreshold** - cosine similarity at which two articles are the same story (default 0.80; raise it and clusters fragment, lower it and distinct stories merge)\n- **embeddingProvider** - huggingface or openai; both branches emit the same shape\n- **hfEmbeddingModel / openaiEmbeddingModel** - embedding model per provider\n- **judgeModel** - OpenAI chat model used by the Step 3 judge\n- **clearBeforeRun** - true wipes the ledger and output tabs (headers always survive) first (clean demo take); false keeps history, and articles already in the ledger are dropped before any money is spent\n- **rssFeedUrls** - JSON array of RSS feed URLs, all fetched every run; add as many as you like\n- **useLiveFetch** - true (default) pulls every feed in rssFeedUrls into the run; false reads only the Source Articles tab\n- **nicheContext** - the judge's definition of relevant; swap it to point the whole pipeline at another niche"
},
"typeVersion": 1
},
{
"id": "d38ca2d6-5964-49d5-9523-b9423e859d2c",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-464,
-160
],
"parameters": {
"color": 7,
"width": 416,
"height": 528,
"content": "## Triggers and context\n\nStarts the pipeline manually or on a daily schedule, then sets the spreadsheet, tab, feed, and run configuration used by downstream nodes."
},
"typeVersion": 1
},
{
"id": "d2faf635-1be5-4ea9-b28e-c6ee7ab86fc7",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-16,
-288
],
"parameters": {
"color": 7,
"width": 864,
"height": 480,
"content": "## Fresh run cleanup\n\nChecks whether this run should start fresh, clears prior approved packages and the seen ledger when requested, then rejoins the normal flow."
},
"typeVersion": 1
},
{
"id": "07a4b82e-48d7-4ca7-b447-e01a034fea1a",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
208,
208
],
"parameters": {
"color": 7,
"width": 864,
"height": 304,
"content": "## Live RSS intake\n\nOptionally fetches live articles by splitting configured feed URLs, reading each RSS feed, and mapping results into the same article shape as the sheet data."
},
"typeVersion": 1
},
{
"id": "881a1639-ffd4-4da9-9556-72c9a9b01490",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
880,
-352
],
"parameters": {
"color": 7,
"width": 416,
"height": 544,
"content": "## Read source records\n\nReads source articles from Google Sheets, appends any mapped live articles, and reads the existing seen ledger for deduplication."
},
"typeVersion": 1
},
{
"id": "e1df064e-6455-497f-add1-c66a85de908a",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
1328,
-80
],
"parameters": {
"color": 7,
"width": 640,
"height": 320,
"content": "## Deduplicate new URLs\n\nDrops articles whose URLs are already in the ledger, stamps the remaining items with pending defaults, and records them as seen before scoring."
},
"typeVersion": 1
},
{
"id": "83f04724-d163-4124-ae37-dd71fa0f4547",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
2000,
-80
],
"parameters": {
"color": 7,
"width": 640,
"height": 320,
"content": "## Rule score candidates\n\nApplies deterministic rule-based scoring, removes rejected noise, and collects the surviving items into a batch for embedding."
},
"typeVersion": 1
},
{
"id": "9afc69f8-6401-4ee2-a55e-a46100b50b4c",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
2672,
-144
],
"parameters": {
"color": 7,
"width": 640,
"height": 528,
"content": "## Generate article embeddings\n\nSelects an embedding provider, sends the batch to Hugging Face or OpenAI, and normalizes either provider\u2019s vector response into a common vectors field."
},
"typeVersion": 1
},
{
"id": "19e029ba-3d6a-4b93-9c5e-73fcb88d8f28",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
3344,
-80
],
"parameters": {
"color": 7,
"width": 416,
"height": 320,
"content": "## Cluster and route\n\nClusters embedded articles to group near-duplicate coverage of the same story, then decides which clusters require LLM judgment."
},
"typeVersion": 1
},
{
"id": "fb8639bf-a6e1-4188-bb2f-072d0d0c834f",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
3792,
-144
],
"parameters": {
"color": 7,
"width": 400,
"height": 528,
"content": "## GPT relevance judge\n\nRuns the OpenAI-powered LLM judge for ambiguous items and parses its decision into a structured verdict."
},
"typeVersion": 1
},
{
"id": "53cb95eb-a1de-4f35-b2a0-8d9e7f10bf48",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
4224,
-80
],
"parameters": {
"color": 7,
"width": 416,
"height": 320,
"content": "## Combine approved paths\n\nMerges items that bypassed the judge with judged results, then assembles final approved story packages from the clustered and scored data."
},
"typeVersion": 1
},
{
"id": "64599911-5ea6-4268-b520-06bbc9fe0984",
"name": "Sticky Note11",
"type": "n8n-nodes-base.stickyNote",
"position": [
4672,
-208
],
"parameters": {
"color": 7,
"height": 384,
"content": "## Write approved packages\n\nWrites the assembled approved story packages to the configured approved output sheet."
},
"typeVersion": 1
},
{
"id": "58d2365d-d1dc-40e0-8b95-49b2546d9ecb",
"name": "Sticky Note12",
"type": "n8n-nodes-base.stickyNote",
"position": [
4672,
192
],
"parameters": {
"color": 7,
"width": 416,
"height": 352,
"content": "## Update outcome ledger\n\nCompiles every article\u2019s final fate and updates the seen ledger so future runs can skip or report on previously processed URLs."
},
"typeVersion": 1
},
{
"id": "293a50f6-37ee-471c-9eb4-0a78d598c687",
"name": "Manual Pipeline Trigger",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-416,
0
],
"parameters": {},
"typeVersion": 1
},
{
"id": "db541963-7378-4615-b2e9-ebcbc7ae31c1",
"name": "Set Workflow Context",
"type": "n8n-nodes-base.set",
"position": [
-192,
80
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "c01",
"name": "sheetDocumentId",
"type": "string",
"value": "11UAshmcWzrzNZRBqeDSL78r7b6GWxou-UJdxhCXRlUY"
},
{
"id": "c16",
"name": "sourceTab",
"type": "string",
"value": "Source Articles"
},
{
"id": "c02",
"name": "articlesTab",
"type": "string",
"value": "Articles"
},
{
"id": "c03",
"name": "approvedTab",
"type": "string",
"value": "Approved Package"
},
{
"id": "c04",
"name": "rulesKeywords",
"type": "object",
"value": "{\"ai\": 1, \"artificial intelligence\": 2, \"model\": 2, \"llm\": 2, \"agent\": 2, \"openai\": 2, \"anthropic\": 2, \"chip\": 1, \"compute\": 2, \"inference\": 2, \"open source\": 1, \"lawsuit\": 1, \"regulation\": 1, \"data center\": 1, \"funding\": 1, \"gpu\": 1, \"voice\": 1, \"copyright\": 1}"
},
{
"id": "c05",
"name": "sourceWeights",
"type": "object",
"value": "{\"techcrunch.com\": 2, \"theverge.com\": 2, \"arstechnica.com\": 2, \"venturebeat.com\": 1, \"wired.com\": 1, \"reuters.com\": 2}"
},
{
"id": "c06",
"name": "rulesRejectThreshold",
"type": "number",
"value": 2
},
{
"id": "c07",
"name": "rulesHardWinThreshold",
"type": "number",
"value": 12
},
{
"id": "c08",
"name": "clusterSimilarityThreshold",
"type": "number",
"value": 0.8
},
{
"id": "c15",
"name": "clearBeforeRun",
"type": "boolean",
"value": false
},
{
"id": "c09",
"name": "embeddingProvider",
"type": "string",
"value": "huggingface"
},
{
"id": "c10",
"name": "hfEmbeddingModel",
"type": "string",
"value": "sentence-transformers/all-MiniLM-L6-v2"
},
{
"id": "c11",
"name": "openaiEmbeddingModel",
"type": "string",
"value": "text-embedding-3-small"
},
{
"id": "c12",
"name": "judgeModel",
"type": "string",
"value": "gpt-5-mini"
},
{
"id": "c17",
"name": "rssFeedUrls",
"type": "array",
"value": "[\"https://techcrunch.com/category/artificial-intelligence/feed/\"]"
},
{
"id": "c14",
"name": "useLiveFetch",
"type": "boolean",
"value": true
},
{
"id": "c13",
"name": "nicheContext",
"type": "string",
"value": "News for people who build AI products and automation: model releases, AI infrastructure and compute, AI business deals, AI regulation, and AI engineering practice."
}
]
}
},
"typeVersion": 3.4
},
{
"id": "dca7b91a-c863-444e-a444-bdd69a97d085",
"name": "Clear Package Data in Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
256,
-112
],
"parameters": {
"operation": "clear",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $('Set Workflow Context').first().json.approvedTab }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Set Workflow Context').first().json.sheetDocumentId }}"
},
"keepFirstRow": true
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "7e2b0a7f-6133-4090-94ab-3efbf4cc0eca",
"name": "Read Articles from Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
928,
-192
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $('Set Workflow Context').first().json.sourceTab }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Set Workflow Context').first().json.sheetDocumentId }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "4c4ae13e-726f-4d8d-bf4c-64f87efd6a96",
"name": "Apply Scoring Rules",
"type": "n8n-nodes-base.code",
"position": [
2048,
80
],
"parameters": {
"jsCode": "// Step 1 of 3: deterministic scoring. Free, instant, and it never changes its mind.\n// Code instead of a native IF-chain: the keyword and source weight maps live in\n// 'Configure context', and native nodes cannot iterate a configurable map.\nconst config = $('Set Workflow Context').first().json;\nconst keywordWeights = config.rulesKeywords;\nconst sourceWeights = config.sourceWeights;\n\nconst scoredItems = [];\nfor (const item of $input.all()) {\n const article = item.json;\n const titleText = String(article.title || '').toLowerCase();\n const bodyText = (String(article.summary || '') + ' ' + String(article.content_excerpt || '')).toLowerCase();\n\n // A keyword in the headline is worth double a keyword buried in the body.\n // Word-boundary matching: 'ai' must not match inside 'bargain'.\n let keywordScore = 0;\n for (const [term, weight] of Object.entries(keywordWeights)) {\n const escapedTerm = term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const termPattern = new RegExp(`\\\\b${escapedTerm}\\\\b`, 'i');\n if (termPattern.test(titleText)) keywordScore += weight * 2;\n else if (termPattern.test(bodyText)) keywordScore += weight;\n }\n const sourceScore = sourceWeights[article.source] || 0;\n const rulesScore = keywordScore + sourceScore;\n\n let rulesBand = 'pass';\n if (rulesScore < config.rulesRejectThreshold) rulesBand = 'reject';\n else if (rulesScore >= config.rulesHardWinThreshold) rulesBand = 'hardWin';\n\n scoredItems.push({ json: { ...article, rulesScore, rulesBand, embedText: `${article.title}. ${article.summary}` } });\n}\nreturn scoredItems;"
},
"typeVersion": 2
},
{
"id": "eb988b03-aaef-4bda-be7c-a90eb4548130",
"name": "Filter Rejected Articles",
"type": "n8n-nodes-base.filter",
"position": [
2272,
80
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f1",
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.rulesBand }}",
"rightValue": "reject"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "d9d9093d-4dee-4ef8-974c-404ae1cc4c58",
"name": "Aggregate for Embedding",
"type": "n8n-nodes-base.aggregate",
"position": [
2496,
80
],
"parameters": {
"options": {},
"aggregate": "aggregateAllItemData"
},
"typeVersion": 1
},
{
"id": "959210e7-d8cb-47cf-8961-a476de01289f",
"name": "Select Embedding Provider",
"type": "n8n-nodes-base.switch",
"position": [
2720,
80
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "Hugging Face",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "s1",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $('Set Workflow Context').first().json.embeddingProvider }}",
"rightValue": "huggingface"
}
]
},
"renameOutput": true
},
{
"outputKey": "OpenAI",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "s2",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $('Set Workflow Context').first().json.embeddingProvider }}",
"rightValue": "openai"
}
]
},
"renameOutput": true
}
]
},
"options": {}
},
"typeVersion": 3.2
},
{
"id": "bcd2c569-151c-40f0-ae4b-0e3b874234c6",
"name": "Post to Hugging Face API",
"type": "n8n-nodes-base.httpRequest",
"position": [
2944,
16
],
"parameters": {
"url": "={{ 'https://router.huggingface.co/hf-inference/models/' + $('Set Workflow Context').first().json.hfEmbeddingModel + '/pipeline/feature-extraction' }}",
"method": "POST",
"options": {
"response": {
"response": {
"fullResponse": true
}
}
},
"jsonBody": "={{ JSON.stringify({ inputs: $json.data.map(article => article.embedText) }) }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "huggingFaceApi"
},
"credentials": {
"huggingFaceApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "2626a24a-f3a2-48b6-baf4-f1052f2106f9",
"name": "Post to OpenAI Embeddings",
"type": "n8n-nodes-base.httpRequest",
"position": [
2944,
208
],
"parameters": {
"url": "https://api.openai.com/v1/embeddings",
"method": "POST",
"options": {
"response": {
"response": {
"fullResponse": true
}
}
},
"jsonBody": "={{ JSON.stringify({ model: $('Set Workflow Context').first().json.openaiEmbeddingModel, input: $json.data.map(article => article.embedText) }) }}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "openAiApi"
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "d2c9c1d1-ef13-44c9-b8ca-50203ac1e7fe",
"name": "Set Hugging Face Vectors",
"type": "n8n-nodes-base.set",
"position": [
3168,
16
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "n1",
"name": "vectors",
"type": "array",
"value": "={{ $json.body }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "be511255-f154-481f-8e44-0bb25b3b47c9",
"name": "Set OpenAI Vectors",
"type": "n8n-nodes-base.set",
"position": [
3168,
208
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "n2",
"name": "vectors",
"type": "array",
"value": "={{ $json.body.data.map(entry => entry.embedding) }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "2c097be8-2772-42f9-9030-697206cf2447",
"name": "Cluster Similar Stories",
"type": "n8n-nodes-base.code",
"position": [
3392,
80
],
"parameters": {
"jsCode": "// Step 2 of 3: group near-duplicate coverage of the same story.\n// Code because similarity clustering needs a pairwise cosine matrix,\n// which no native node expresses. Greedy single-link over 50 articles is instant.\n// One item per STORY leaves this node, so the canvas count itself shows the dedup.\nconst config = $('Set Workflow Context').first().json;\nconst threshold = Number(config.clusterSimilarityThreshold);\nconst articles = $('Filter Rejected Articles').all().map(item => item.json);\nconst vectors = $input.first().json.vectors;\nif (!Array.isArray(vectors) || vectors.length !== articles.length) {\n throw new Error(`Vector count (${vectors && vectors.length}) does not match article count (${articles.length})`);\n}\n\nfunction cosineSimilarity(vectorA, vectorB) {\n let dotProduct = 0;\n let magnitudeA = 0;\n let magnitudeB = 0;\n for (let dim = 0; dim < vectorA.length; dim++) {\n dotProduct += vectorA[dim] * vectorB[dim];\n magnitudeA += vectorA[dim] * vectorA[dim];\n magnitudeB += vectorB[dim] * vectorB[dim];\n }\n return dotProduct / (Math.sqrt(magnitudeA) * Math.sqrt(magnitudeB));\n}\n\nconst clusters = [];\nfor (let articleIndex = 0; articleIndex < articles.length; articleIndex++) {\n let joinedExisting = false;\n for (const cluster of clusters) {\n if (cluster.some(memberIndex => cosineSimilarity(vectors[articleIndex], vectors[memberIndex]) >= threshold)) {\n cluster.push(articleIndex);\n joinedExisting = true;\n break;\n }\n }\n if (!joinedExisting) clusters.push([articleIndex]);\n}\n\n// The primary is picked deterministically: highest rules score, longer excerpt on ties.\n// Every other member rides along on the primary as corroborating coverage.\nconst storyItems = [];\nclusters.forEach((memberIndexes, clusterNumber) => {\n const primaryIndex = memberIndexes.reduce((bestIndex, candidateIndex) => {\n const bestArticle = articles[bestIndex];\n const candidateArticle = articles[candidateIndex];\n if (candidateArticle.rulesScore !== bestArticle.rulesScore) {\n return candidateArticle.rulesScore > bestArticle.rulesScore ? candidateIndex : bestIndex;\n }\n return String(candidateArticle.content_excerpt || '').length > String(bestArticle.content_excerpt || '').length\n ? candidateIndex : bestIndex;\n });\n const corroborating = memberIndexes\n .filter(memberIndex => memberIndex !== primaryIndex)\n .map(memberIndex => ({\n title: articles[memberIndex].title,\n source: articles[memberIndex].source,\n url: articles[memberIndex].url,\n }));\n storyItems.push({ json: {\n ...articles[primaryIndex],\n clusterId: clusterNumber + 1,\n clusterSize: memberIndexes.length,\n isPrimary: true,\n corroborating,\n }});\n});\nreturn storyItems;"
},
"typeVersion": 2
},
{
"id": "f3d87989-002a-4226-a122-14fb28c57a12",
"name": "Check for LLM Judgment",
"type": "n8n-nodes-base.if",
"position": [
3616,
80
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "i1",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.isPrimary }}",
"rightValue": true
},
{
"id": "i2",
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.rulesBand }}",
"rightValue": "hardWin"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "e85bc260-3647-4351-a51e-1ff687232cea",
"name": "Relevance Judgment",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"position": [
3888,
16
],
"parameters": {
"text": "=You are the relevance judge in a content pipeline.\nNiche: {{ $('Set Workflow Context').first().json.nicheContext }}\n\nArticle:\nTitle: {{ $json.title }}\nSource: {{ $json.source }}\nSummary: {{ $json.summary }}\nExcerpt: {{ $json.content_excerpt }}\n\nDecide whether this article is relevant enough to become client-facing content in this niche. Reject encyclopedia-style evergreen pages, thin social posts with no substance, and anything off-niche.",
"batching": {
"batchSize": 10
},
"messages": {
"messageValues": []
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 1.9
},
{
"id": "0820d66e-867b-4549-8295-178ff874a6e5",
"name": "Set OpenAI Judge Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
3888,
240
],
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "={{ $('Set Workflow Context').first().json.judgeModel }}"
},
"options": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "8ff317d3-d417-4083-8873-02344192c034",
"name": "Parse Judgment Verdict",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
4096,
240
],
"parameters": {
"schemaType": "manual",
"inputSchema": "{\"type\": \"object\", \"properties\": {\"verdict\": {\"type\": \"string\", \"enum\": [\"approved\", \"rejected\"]}, \"reason\": {\"type\": \"string\", \"description\": \"One sentence explaining the verdict\"}}, \"required\": [\"verdict\", \"reason\"]}"
},
"typeVersion": 1.2
},
{
"id": "15b20e32-de78-4f2b-90a2-1e49946f9108",
"name": "Assemble Approved Articles",
"type": "n8n-nodes-base.code",
"position": [
4496,
80
],
"parameters": {
"jsCode": "// The payoff: one row per approved story. Code because this aggregates three\n// sources (story primaries, judge verdicts, run counts) into a single package shape.\nconst storyPrimaries = $('Cluster Similar Stories').all().map(item => item.json);\nconst judgedPrimaries = storyPrimaries.filter(story => story.rulesBand !== 'hardWin');\n\nlet judgeOutputs = [];\ntry {\n judgeOutputs = $('Relevance Judgment').all().map(item => item.json);\n} catch (noJudgeRun) {\n judgeOutputs = []; // every story was a hard win; the LLM was never called\n}\n\nconst verdictByCluster = {};\nfor (const story of storyPrimaries) {\n if (story.rulesBand === 'hardWin') {\n verdictByCluster[story.clusterId] = { verdict: 'approved', reason: 'Rules hard win - skipped the LLM entirely' };\n }\n}\njudgedPrimaries.forEach((story, judgeIndex) => {\n const judgeResult = judgeOutputs[judgeIndex] ? (judgeOutputs[judgeIndex].output || judgeOutputs[judgeIndex]) : null;\n verdictByCluster[story.clusterId] = {\n verdict: judgeResult && judgeResult.verdict ? judgeResult.verdict : 'rejected',\n reason: judgeResult && judgeResult.reason ? judgeResult.reason : 'No judge output received',\n };\n});\n\nconst articlesIn = $('Read Articles from Sheets').all().length;\nconst survivedRules = $('Filter Rejected Articles').all().length;\n\nconst runId = $execution.id || 'manual';\nconst runAt = new Date().toISOString();\nconst runSignature = `${articlesIn}-${survivedRules}-${storyPrimaries.length}-${judgedPrimaries.length}`;\n\nconst packageRows = [];\nfor (const story of storyPrimaries) {\n const clusterVerdict = verdictByCluster[story.clusterId] || { verdict: 'rejected', reason: 'Missing verdict' };\n if (clusterVerdict.verdict !== 'approved') continue;\n const corroborating = (story.corroborating || [])\n .map(member => `${member.title} - ${member.source} (${member.url})`)\n .join(' | ');\n packageRows.push({ json: {\n run_id: runId,\n run_at: runAt,\n run_signature: runSignature,\n cluster_id: story.clusterId,\n best_title: story.title,\n best_source: story.source,\n best_url: story.url,\n rules_score: story.rulesScore,\n verdict_reason: clusterVerdict.reason,\n corroborating_sources: corroborating || 'none - single-source story',\n articles_in_cluster: story.clusterSize,\n run_articles_in: articlesIn,\n run_survived_rules: survivedRules,\n run_unique_stories: storyPrimaries.length,\n run_llm_calls: judgedPrimaries.length,\n }});\n}\nreturn packageRows;"
},
"typeVersion": 2
},
{
"id": "aabb209a-6e59-4017-a42b-8cb996e50580",
"name": "Append Approved to Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
4720,
16
],
"parameters": {
"columns": {
"value": {
"run_at": "={{ $json.run_at }}",
"run_id": "={{ $json.run_id }}",
"best_url": "={{ $json.best_url }}",
"best_title": "={{ $json.best_title }}",
"cluster_id": "={{ $json.cluster_id }}",
"best_source": "={{ $json.best_source }}",
"rules_score": "={{ $json.rules_score }}",
"run_llm_calls": "={{ $json.run_llm_calls }}",
"run_signature": "={{ $json.run_signature }}",
"verdict_reason": "={{ $json.verdict_reason }}",
"run_articles_in": "={{ $json.run_articles_in }}",
"run_survived_rules": "={{ $json.run_survived_rules }}",
"run_unique_stories": "={{ $json.run_unique_stories }}",
"articles_in_cluster": "={{ $json.articles_in_cluster }}",
"corroborating_sources": "={{ $json.corroborating_sources }}"
},
"schema": [
{
"id": "run_id",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "run_id",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "run_at",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "run_at",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "run_signature",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "run_signature",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "cluster_id",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "cluster_id",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "best_title",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "best_title",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "best_source",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "best_source",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "best_url",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "best_url",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "rules_score",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "rules_score",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "verdict_reason",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "verdict_reason",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "corroborating_sources",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "corroborating_sources",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "articles_in_cluster",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "articles_in_cluster",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "run_articles_in",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "run_articles_in",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "run_survived_rules",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "run_survived_rules",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "run_unique_stories",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "run_unique_stories",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "run_llm_calls",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "run_llm_calls",
"defaultMatch": false,
"canBeUsedToMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": []
},
"options": {
"useAppend": true
},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $('Set Workflow Context').first().json.approvedTab }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Set Workflow Context').first().json.sheetDocumentId }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "41eb7e42-b8b5-4c3e-bf41-4b482d3cc429",
"name": "Check for Live Articles",
"type": "n8n-nodes-base.if",
"position": [
256,
352
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "lg1",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $('Set Workflow Context').first().json.useLiveFetch }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "86da3f1f-77e8-4e52-95e5-e1784f2a0219",
"name": "Fetch RSS Feeds",
"type": "n8n-nodes-base.rssFeedRead",
"position": [
704,
352
],
"parameters": {
"url": "={{ $json.rssFeedUrls }}",
"options": {}
},
"typeVersion": 1.1
},
{
"id": "51385fe5-bd16-4d92-8dcd-074c9197636f",
"name": "Combine Sheet and Live Articles",
"type": "n8n-nodes-base.merge",
"position": [
1152,
-160
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "22b0269c-1dfc-4ff7-b023-ae072e225a19",
"name": "Set Live Article Fields",
"type": "n8n-nodes-base.set",
"position": [
928,
352
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "r1",
"name": "id",
"type": "string",
"value": "={{ 'live-' + ($itemIndex + 1) }}"
},
{
"id": "r2",
"name": "title",
"type": "string",
"value": "={{ $json.title }}"
},
{
"id": "r3",
"name": "summary",
"type": "string",
"value": "={{ $json.contentSnippet }}"
},
{
"id": "r4",
"name": "source",
"type": "string",
"value": "={{ ($json.link || '').replace(/^https?:\\/\\/(www\\.)?/, '').split('/')[0] }}"
},
{
"id": "r5",
"name": "url",
"type": "string",
"value": "={{ $json.link }}"
},
{
"id": "r6",
"name": "published_at",
"type": "string",
"value": "={{ $json.isoDate }}"
},
{
"id": "r7",
"name": "content_excerpt",
"type": "string",
"value": "={{ $json.contentSnippet }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "51393caa-04df-4be7-9aa5-adbabfdd33a3",
"name": "Merge LLM Judgments",
"type": "n8n-nodes-base.merge",
"position": [
4272,
80
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "6469bd65-bfcd-4102-8fc7-038e2736749e",
"name": "Check for Fresh Start",
"type": "n8n-nodes-base.if",
"position": [
32,
16
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cg1",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $('Set Workflow Context').first().json.clearBeforeRun }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "87ed75f1-16cc-42a3-b852-01d2305c4725",
"name": "Resume After Clear",
"type": "n8n-nodes-base.merge",
"position": [
704,
16
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "e8fbc2b7-759c-45b1-bf31-6cec2d5b7c4f",
"name": "Clear Seen Ledger in Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
480,
-112
],
"parameters": {
"operation": "clear",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $('Set Workflow Context').first().json.articlesTab }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Set Workflow Context').first().json.sheetDocumentId }}"
},
"keepFirstRow": true
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "5dfd931e-e3a3-47b9-b766-ef98c70f2bde",
"name": "Read Seen Ledger from Sheets",
"type": "n8n-nodes-base.googleSheets",
"position": [
1152,
32
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $('Set Workflow Context').first().json.articlesTab }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Set Workflow Context').first().json.sheetDocumentId }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7,
"alwaysOutputData": true
},
{
"id": "45a5a0dd-fc07-428a-926f-0c71dc8006c6",
"name": "Remove Duplicate URLs",
"type": "n8n-nodes-base.merge",
"position": [
1376,
80
],
"parameters": {
"mode": "combine",
"options": {},
"joinMode": "keepNonMatches",
"outputDataFrom": "input1",
"fieldsToMatchString": "url"
},
"typeVersion": 3.2
},
{
"id": "db28763a-a420-44a7-ba36-8943772936f8",
"name": "Log to Seen Ledger",
"type": "n8n-nodes-base.googleSheets",
"position": [
1824,
80
],
"parameters": {
"columns": {
"value": {
"id": "={{ $json.id }}",
"url": "={{ $json.url }}",
"title": "={{ $json.title }}",
"reason": "={{ $json.reason }}",
"run_id": "={{ $json.run_id }}",
"source": "={{ $json.source }}",
"status": "={{ $json.status }}",
"summary": "={{ $json.summary }}",
"cluster_id": "={{ $json.cluster_id }}",
"rules_score": "={{ $json.rules_score }}",
"published_at": "={{ $json.published_at }}",
"content_excerpt": "={{ $json.content_excerpt }}"
},
"schema": [
{
"id": "id",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "id",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "title",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "title",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "summary",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "summary",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "source",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "source",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "url",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "url",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "published_at",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "published_at",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "content_excerpt",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "content_excerpt",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "status",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "status",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "rules_score",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "rules_score",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "reason",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "reason",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "cluster_id",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "cluster_id",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "run_id",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "run_id",
"defaultMatch": false,
"canBeUsedToMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": []
},
"options": {
"useAppend": true
},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $('Set Workflow Context').first().json.articlesTab }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Set Workflow Context').first().json.sheetDocumentId }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "4ed612a6-c350-4134-aa26-df58f572b322",
"name": "Set Pending Article Status",
"type": "n8n-nodes-base.set",
"position": [
1600,
80
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "p1",
"name": "status",
"type": "string",
"value": "pending"
},
{
"id": "p2",
"name": "rules_score",
"type": "string",
"value": ""
},
{
"id": "p3",
"name": "reason",
"type": "string",
"value": ""
},
{
"id": "p4",
"name": "cluster_id",
"type": "string",
"value": ""
},
{
"id": "p5",
"name": "run_id",
"type": "string",
"value": "={{ $execution.id }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "c6d5d892-e635-4118-a916-b7ad38cbfa17",
"name": "Compile Article Outcomes",
"type": "n8n-nodes-base.code",
"position": [
4720,
368
],
"parameters": {
"jsCode": "// Writes every article's fate back to the seen ledger, matched by URL.\n// Recomputes the same verdict mapping the assembler uses - both nodes read\n// identical sources, so they cannot drift.\nconst scoredArticles = $('Apply Scoring Rules').all().map(item => item.json);\nconst storyPrimaries = $('Cluster Similar Stories').all().map(item => item.json);\nconst judgedPrimaries = storyPrimaries.filter(story => story.rulesBand !== 'hardWin');\nlet judgeOutputs = [];\ntry {\n judgeOutputs = $('Relevance Judgment').all().map(item => item.json);\n} catch (noJudgeRun) {\n judgeOutputs = [];\n}\n\nconst config = $('Set Workflow Context').first().json;\nconst runId = String($execution.id || 'manual');\n\nconst verdictByCluster = {};\nfor (const story of storyPrimaries) {\n if (story.rulesBand === 'hardWin') {\n verdictByCluster[story.clusterId] = { verdict: 'approved', reason: 'Rules hard win - skipped the LLM entirely' };\n }\n}\njudgedPrimaries.forEach((story, judgeIndex) => {\n const judgeResult = judgeOutputs[judgeIndex] ? (judgeOutputs[judgeIndex].output || judgeOutputs[judgeIndex]) : null;\n verdictByCluster[story.clusterId] = {\n verdict: judgeResult && judgeResult.verdict ? judgeResult.verdict : 'rejected',\n reason: judgeResult && judgeResult.reason ? judgeResult.reason : 'No judge output received',\n };\n});\n\nconst outcomeByUrl = {};\nfor (const article of scoredArticles) {\n if (article.rulesBand === 'reject') {\n outcomeByUrl[article.url] = {\n status: 'rejected_rules',\n rules_score: article.rulesScore,\n reason: `Rules score ${article.rulesScore} below reject threshold ${config.rulesRejectThreshold}`,\n cluster_id: '',\n };\n }\n}\nfor (const story of storyPrimaries) {\n const clusterVerdict = verdictByCluster[story.clusterId] || { verdict: 'rejected', reason: 'Missing verdict' };\n outcomeByUrl[story.url] = {\n status: clusterVerdict.verdict === 'approved' ? 'approved' : 'rejected_judge',\n rules_score: story.rulesScore,\n reason: clusterVerdict.reason,\n cluster_id: story.clusterId,\n };\n for (const member of story.corroborating || []) {\n const memberScored = scoredArticles.find(candidate => candidate.url === member.url);\n outcomeByUrl[member.url] = {\n status: 'corroborating',\n rules_score: memberScored ? memberScored.rulesScore : '',\n reason: `Duplicate coverage of: ${story.title}`,\n cluster_id: story.clusterId,\n };\n }\n}\n\nreturn Object.entries(outcomeByUrl).map(([url, outcome]) => ({ json: { url, ...outcome, run_id: runId } }));"
},
"typeVersion": 2
},
{
"id": "c3ceb932-f4cf-4461-a20a-5a4c548c8bc4",
"name": "Update Ledger Outcomes",
"type": "n8n-nodes-base.googleSheets",
"position": [
4944,
368
],
"parameters": {
"columns": {
"value": {
"url": "={{ $json.url }}",
"reason": "={{ $json.reason }}",
"run_id": "={{ $json.run_id }}",
"status": "={{ $json.status }}",
"cluster_id": "={{ $json.cluster_id }}",
"rules_score": "={{ $json.rules_score }}"
},
"schema": [
{
"id": "url",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "url",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "status",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "status",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "rules_score",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "rules_score",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "reason",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "reason",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "cluster_id",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "cluster_id",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "run_id",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "run_id",
"defaultMatch": false,
"canBeUsedToMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"url"
]
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $('Set Workflow Context').first().json.articlesTab }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Set Workflow Context').first().json.sheetDocumentId }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "70e487fc-3392-4532-a3b5-e65c3c18d6b7",
"name": "Split RSS Feed URLs",
"type": "n8n-nodes-base.splitOut",
"position": [
480,
352
],
"parameters": {
"options": {},
"fieldToSplitOut": "rssFeedUrls"
},
"typeVersion": 1
},
{
"id": "69828c23-2118-48f6-ab6a-6116d465e729",
"name": "Scheduled Midnight Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-416,
208
],
"parameters": {
"rule": {
"interval": [
{}
]
}
},
"typeVersion": 1.3
},
{
"id": "dfd0152a-2eb0-42a3-9c2a-970f011fcb30",
"name": "Sticky howitworks",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1632,
-784
],
"parameters": {
"width": 620,
"height": 1152,
"content": "# How It Works \ud83e\udde0\nTurns a noisy news feed into a deduplicated, approved content package - spending real money only on stories that earn it.\n\nThree escalating-cost steps:\n1. **Rules tier (free)** - keyword + source scoring kills the obvious noise; articles above the hard-win threshold skip the LLM entirely\n2. **Semantic dedup (pennies)** - embeddings + cosine similarity cluster outlets covering the same story; one primary per cluster proceeds\n3. **LLM judge (paid, and only here)** - the model rules only on unique, ambiguous stories; every clustered duplicate inherits its primary's verdict for free. **The LLM never sees the same story twice.**\n\nOne row per approved story lands in the Approved Package tab: best article, every other outlet's angle as corroborating sources, and the run counts. Every article's fate is written back to the Articles ledger with the reason - and already-seen URLs are dropped before a penny is spent, so repeat runs are free.\n\n## Setup (~5 minutes)\n1. **Credentials** - create Google Sheets (OAuth), Hugging Face (free token) and OpenAI credentials, then attach them to the Sheets, embedding and judge nodes\n2. **Sheet** - copy the starter sheet linked in the template description (File \u2192 Make a copy), or run the starter seed workflow from the GitHub repo\n3. **Configure context** - paste your spreadsheet ID into `sheetDocumentId` and put your feeds in `rssFeedUrls`\n4. **Run** - live headlines flow through the three steps into the Approved Package tab\n\n**Adapt it to your niche** by editing three config values: `nicheContext` (what the judge considers relevant), `rulesKeywords`, and `sourceWeights`.\n\n## Starter Google Sheet: https://docs.google.com/spreadsheets/d/11UAshmcWzrzNZRBqeDSL78r7b6GWxou-UJdxhCXRlUY/edit?usp=sharing\n\n## Data sources\nTwo ways to feed the pipeline:\n- **Live RSS (default)** - every URL in `rssFeedUrls` is fetched each run; an article's `source` is its domain, so key `sourceWeights` by domain (e.g. `techcrunch.com`)\n- **Source Articles tab** - populate it from another workflow (or any tool that writes rows in its seven-column shape: id, title, summary, source, url, published_at, content_excerpt) and the pipeline categorises whatever lands there\n\nThe Articles ledger drops anything already seen, so repeat runs cost nothing."
},
"typeVersion": 1
}
],
"active": true,
"settings": {
"binaryMode": "separate",
"callerPolicy": "workflowsFromSameOwner",
"timeSavedMode": "fixed",
"availableInMCP": true,
"executionOrder": "v1"
},
"versionId": "bbbc4b56-c411-4d05-a4fc-ae925a52e1cc",
"nodeGroups": [],
"connections": {
"Fetch RSS Feeds": {
"main": [
[
{
"node": "Set Live Article Fields",
"type": "main",
"index": 0
}
]
]
},
"Log to Seen Ledger": {
"main": [
[
{
"node": "Apply Scoring Rules",
"type": "main",
"index": 0
}
]
]
},
"Relevance Judgment": {
"main": [
[
{
"node": "Merge LLM Judgments",
"type": "main",
"index": 0
}
]
]
},
"Resume After Clear": {
"main": [
[
{
"node": "Read Articles fro
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.
googleSheetsOAuth2ApihuggingFaceApiopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Reads your RSS feeds daily, kills obvious noise with free keyword rules, then groups outlets covering the same story using cheap embeddings, so the AI judge never rules on a story twice. Approved story packages, plus a kept-or-rejected reason for every article, land in Google…
Source: https://n8n.io/workflows/17445/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Who is this for? This workflow is ideal for HR teams, startups, and enterprises that want to handle employee interactions through WhatsApp and automate responses using LLM (OpenAI) and intelligent rou
AI News Data Ingestion. Uses rssFeedReadTrigger, chainLlm, s3, httpRequest. Event-driven trigger; 80 nodes.
This advanced n8n workflow automates the full lead enrichment, qualification, and personalized outreach process tailored specifically for the B2B real estate sector. Integrating top platforms like Api
This n8n template automatically classifies incoming emails (Sales, Support, Internal, Finance, Promotions) and routes them to a dedicated OpenAI LLM Agent for processing. Depending on the category, th
Generate Exam Questions. Uses manualTrigger, vectorStoreQdrant, httpRequest, embeddingsOpenAi. Event-driven trigger; 37 nodes.