Taming Messy RSS Feeds with n8n and a Bit of Vibecoding
Every morning I would wake up and swipe left on my phone - Google Discover. Then open browser and there is another news feed right there before I have even had tea. On top of that I had other News apps doing the same thing.
The content was interesting. That was the problem. I would start reading something about a CVE or a breach and ten minutes later I am down a completely different rabbit hole. Pure dopamine and because the feed reloads every time you close the browser I had this habit of tapping links and leaving them open as tabs just so I would not lose the article. At peak it was somewhere around fifteen open tabs everyday.
I was starting every morning overloaded before I had done anything useful. I wanted out of that loop.
What I Built
I set up an n8n workflow that polls a bunch of RSS sources I actually trust, filters out anything older than 15 minutes and pushes fresh articles to a Telegram bot (I created bot using @BotFather). Runs every 15 minutes on its own. I don't touch anything.
The Telegram part is what makes this work for me. Messages don't disappear. Every article that comes in sits there until I choose to read it. No algorithm deciding what to surface and when. It acts like an automatic bookmark. I don't need to keep browser tabs open as reminders anymore.
Mornings are noticeably calmer now.
How It Works
The cron triggers every 15 minutes. Each RSS source is processed one at a time through a batch loop. Before anything gets sent the workflow checks two things ā is this article within the last 15 minutes and has it already been sent before. If it passes both it gets formatted and pushed to Telegram Bot. After a successful send the article is marked as seen so it never shows up again (still WIP..).
All the settings sit in a single config node at the very start of the workflow. Lookback window, Telegram chat ID and max stored article IDs. One place to change anything, nothing scattered around.

Each message in Telegram looks like this:
š° Article Title
š https://link-to-article
š
Publication date
āļø Author / Source
Short preview of the content...

The Workflow File
I am attaching the n8n JSON export below. Here is exactly what you need to do before and after importing it.
Before importing ā edit the JSON file:
- Replace
YOUR_TELEGRAM_CHAT_IDwith your actual chat ID (Check this bot on Telegram: @userinfobot)
After importing into n8n:
- Open the Telegram node called
Personal RSS TG1and connect your Telegram bot credential there - Open the
RSS Sources1node and swap out the feed URLs for sources you actually want to follow. The ones in the file are what I use ā mostly cybersecurity and cloud
Want a full step by step guide on setting this up from scratch including creating the Telegram bot and configuring n8n? Let me know here. If there is enough interest I will do a proper follow up post.
One Limitation
The whole thing runs locally in a Docker container on my laptop right now. The laptop needs to stay on for the cron to fire. Not ideal long term. I touched on this in my birthday post as well. Will sort out a proper hosting setup eventually.
n8n Workflow file (JSON)
{
"nodes": [
{
"parameters": {},
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-112,
2760
],
"typeVersion": 1,
"id": "2f2ce6d6-c3fa-4ad2-b865-c6d5ae0e1129"
},
{
"parameters": {
"jsCode": "// āļø CHANGE SETTINGS HERE ONLY\nreturn [{\n json: {\n LOOKBACK_MINUTES: 15,\n MAX_STORED_IDS: 500,\n CHAT_ID: 'YOUR_TELEGRAM_CHAT_ID'\n }\n}];"
},
"name": "āļø Config",
"type": "n8n-nodes-base.code",
"position": [
112,
2688
],
"typeVersion": 2,
"id": "de1931f4-8fc8-4b09-854f-3b9f937b2077"
},
{
"parameters": {
"jsCode": "// Add or remove RSS feed URLs here\nreturn [\n { json: { url: 'https://www.bleepingcomputer.com/feed/' } },\n { json: { url: 'https://www.wiz.io/feed/rss.xml' } },\n { json: { url: 'https://thehackernews.com/feeds/posts/default' } },\n { json: { url: 'https://krebsonsecurity.com/feed/' } },\n { json: { url: 'https://aws.amazon.com/blogs/security/feed/' } },\n { json: { url: 'https://cloudsecurityalliance.org/blog/feed/' } },\n { json: { url: 'https://techcrunch.com/feed/' } },\n { json: { url: 'https://www.techrepublic.com/rssfeeds/topic/cloud/' } },\n { json: { url: 'https://www.techrepublic.com/rssfeeds/topic/cloud-security/' } },\n { json: { url: 'https://www.techrepublic.com/rssfeeds/topic/cybersecurity/' } },\n { json: { url: 'https://www.techrepublic.com/rssfeeds/topic/devops/' } },\n { json: { url: 'https://www.techrepublic.com/rssfeeds/topic/security/' } },\n { json: { url: 'https://www.techrepublic.com/rssfeeds/topic/ai/' } },\n { json: { url: 'https://www.techrepublic.com/rssfeeds/topic/ai-governance-policy/' } },\n { json: { url: 'https://feeds.feedburner.com/pudkrebsonsec' } },\n { json: { url: 'https://www.nytimes.com/svc/collections/v1/publish/https://www.nytimes.com/spotlight/cybersecurity/rss.xml' } }\n];"
},
"name": "RSS Sources1",
"type": "n8n-nodes-base.code",
"position": [
336,
2688
],
"typeVersion": 2,
"id": "0ccd94e1-c35c-46a4-b3c4-b8412e0021e0"
},
{
"parameters": {
"options": {}
},
"name": "Split in Batches",
"type": "n8n-nodes-base.splitInBatches",
"position": [
560,
2688
],
"typeVersion": 3,
"id": "a5cea360-b81d-40cf-890f-a1e96a2abe8a"
},
{
"parameters": {
"url": "={{ $json[\"url\"] }}",
"options": {}
},
"name": "RSS Feed Read1",
"type": "n8n-nodes-base.rssFeedRead",
"position": [
784,
2544
],
"typeVersion": 1,
"id": "667e8401-9e0e-4a68-ae0c-039699b75b43"
},
{
"parameters": {
"jsCode": "const staticData = $getWorkflowStaticData('global');\nconst oldRSSIds = staticData.oldRSSIds || [];\n\nconst LOOKBACK_MINUTES = $('āļø Config').first().json.LOOKBACK_MINUTES || 60;\nconst NOW = new Date();\nconst CUTOFF = new Date(NOW.getTime() - LOOKBACK_MINUTES * 60 * 1000);\n\nconst actualNewRSS = items.filter((item) => {\n const itemId = item.json.link || item.json.isoDate || item.json.pubDate;\n const itemDate = new Date(item.json.isoDate || item.json.pubDate || 0);\n const hasTitle = !!(item.json.title || '').trim();\n const hasLink = !!item.json.link;\n return !!itemId && hasTitle && hasLink && !oldRSSIds.includes(itemId) && itemDate >= CUTOFF;\n});\n\nreturn actualNewRSS.length > 0\n ? actualNewRSS.map(item => ({\n json: { ...item.json, _rssId: item.json.link || item.json.isoDate || item.json.pubDate }\n }))\n : [{ json: { skipped: true, _rssId: null } }];"
},
"name": "Only get new RSS1",
"type": "n8n-nodes-base.code",
"position": [
1008,
2544
],
"typeVersion": 2,
"id": "38233468-6e6a-4c85-8024-e1207c4804ca"
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ !$json.skipped }}",
"value2": true
}
]
}
},
"name": "Has New Items?",
"type": "n8n-nodes-base.if",
"position": [
1232,
2544
],
"typeVersion": 1,
"id": "has-new-items-remote-001"
},
{
"parameters": {
"jsCode": "function stripHTML(text) {\n if (!text) return '';\n return text.replace(/<[^>]*>/g, '').replace(/&[a-z]+;/gi, ' ').trim();\n}\nfunction truncate(text, max) {\n if (!text) return '';\n return text.length > max ? text.substring(0, max) + '...' : text;\n}\nreturn items.map(item => {\n const title = stripHTML(item.json.title || 'No Title');\n const link = item.json.link || '';\n const pubDate = item.json.pubDate || item.json.isoDate || '';\n const source = stripHTML(item.json.creator || item.json['dc:creator'] || item.json.feedTitle || 'Unknown');\n const rawContent = item.json.contentSnippet || item.json.content || item.json.summary || '';\n const content = truncate(stripHTML(rawContent), 200);\n return {\n json: {\n ...item.json,\n telegramMessage: `š° <b>${title}</b>\\nš ${link}\\nš
${pubDate}\\nāļø ${source}\\n\\n${content ? content + '\\n\\n' : ''}`\n }\n };\n});"
},
"name": "Format Message",
"type": "n8n-nodes-base.code",
"position": [
1456,
2544
],
"typeVersion": 2,
"id": "00b76384-8ae0-499a-8430-5e970b570439"
},
{
"parameters": {
"chatId": "={{ $('āļø Config').first().json.CHAT_ID }}",
"text": "={{ $json.telegramMessage }}",
"additionalFields": {
"parse_mode": "HTML"
}
},
"name": "Personal RSS TG1",
"type": "n8n-nodes-base.telegram",
"position": [
1680,
2544
],
"typeVersion": 1,
"id": "6bff0ca7-2e73-4b49-af18-f838e5e6ea69",
"webhookId": "840e17f6-1a93-4706-b123-128d349b09c0",
"credentials": {}
},
{
"parameters": {
"jsCode": "const staticData = $getWorkflowStaticData('global');\nconst oldRSSIds = staticData.oldRSSIds || [];\nconst MAX_STORED = 500;\n\nconst sentIds = items\n .map(item => item.json._rssId)\n .filter(id => !!id);\n\nstaticData.oldRSSIds = [...new Set([...sentIds, ...oldRSSIds])].slice(0, MAX_STORED);\nreturn items;"
},
"name": "Mark as Seen",
"type": "n8n-nodes-base.code",
"position": [
1904,
2616
],
"typeVersion": 2,
"id": "c7fa6b35-4d80-4e19-ade2-7bd2984d7f9e"
},
{
"parameters": {
"triggerTimes": {
"item": [
{
"mode": "everyX",
"value": 15,
"unit": "minutes"
}
]
}
},
"name": "Cron1",
"type": "n8n-nodes-base.cron",
"position": [
-112,
2568
],
"typeVersion": 1,
"id": "1f7ec88f-af23-495a-ba98-e86161b1490f"
},
{
"parameters": {
"content": "## Workflow by [Praveen Deshpande](https://pravw.in/)",
"height": 80,
"width": 400,
"color": 5
},
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
752,
2352
],
"id": "d7219584-501c-485f-a221-b578066ac45a",
"name": "Sticky Note"
}
],
"connections": {
"Manual Trigger": {
"main": [
[
{
"node": "āļø Config",
"type": "main",
"index": 0
}
]
]
},
"āļø Config": {
"main": [
[
{
"node": "RSS Sources1",
"type": "main",
"index": 0
}
]
]
},
"RSS Sources1": {
"main": [
[
{
"node": "Split in Batches",
"type": "main",
"index": 0
}
]
]
},
"Split in Batches": {
"main": [
[],
[
{
"node": "RSS Feed Read1",
"type": "main",
"index": 0
}
]
]
},
"RSS Feed Read1": {
"main": [
[
{
"node": "Only get new RSS1",
"type": "main",
"index": 0
}
]
]
},
"Only get new RSS1": {
"main": [
[
{
"node": "Has New Items?",
"type": "main",
"index": 0
}
]
]
},
"Has New Items?": {
"main": [
[
{
"node": "Format Message",
"type": "main",
"index": 0
}
],
[
{
"node": "Split in Batches",
"type": "main",
"index": 0
}
]
]
},
"Format Message": {
"main": [
[
{
"node": "Personal RSS TG1",
"type": "main",
"index": 0
}
]
]
},
"Personal RSS TG1": {
"main": [
[
{
"node": "Mark as Seen",
"type": "main",
"index": 0
}
]
]
},
"Mark as Seen": {
"main": [
[
{
"node": "Split in Batches",
"type": "main",
"index": 0
}
]
]
},
"Cron1": {
"main": [
[
{
"node": "āļø Config",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "YOUR_N8N_INSTANCE_ID"
}
}