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 →
{
"nodes": [
{
"parameters": {
"numberInputs": 3
},
"id": "98cdcf1b-a95e-4804-a414-89dcfb3c64e6",
"name": "Merge All Sources",
"type": "n8n-nodes-base.merge",
"typeVersion": 3,
"position": [
-5536,
9664
]
},
{
"parameters": {
"jsCode": "// Fetch and parse all RSS feeds with categories\nconst feeds = [\n { url: 'https://arstechnica.com/feed/', category: 'Tech News' },\n { url: 'https://noted.lol/rss', category: 'Homelab' },\n { url: 'https://omgubuntu.co.uk/feed', category: 'Linux' },\n { url: 'https://9to5linux.com/feed', category: 'Linux' },\n { url: 'https://www.cyberciti.com/atom/atom.xml', category: 'Linux' }\n];\n\n// Date filter: previous calendar day only\nconst now = new Date();\nconst yesterdayStart = new Date(now);\nyesterdayStart.setDate(yesterdayStart.getDate() - 1);\nyesterdayStart.setHours(0, 0, 0, 0);\n\nconst yesterdayEnd = new Date(now);\nyesterdayEnd.setDate(yesterdayEnd.getDate() - 1);\nyesterdayEnd.setHours(23, 59, 59, 999);\n\nconst allArticles = [];\n\nfor (const feed of feeds) {\n try {\n const response = await this.helpers.httpRequest({\n method: 'GET',\n url: feed.url,\n returnFullResponse: false\n });\n \n const xmlData = typeof response === 'string' ? response : (response.data || response.body || '');\n \n if (!xmlData) continue;\n \n // Detect Atom vs RSS\n const isAtom = xmlData.includes('<feed') && xmlData.includes('<entry');\n \n if (isAtom) {\n // Parse Atom\n const entryRegex = /<entry[^>]*>([\\s\\S]*?)<\\/entry>/gi;\n let match;\n let count = 0;\n \n while ((match = entryRegex.exec(xmlData)) !== null && count < 3) {\n const entryXml = match[1];\n \n const linkMatch = entryXml.match(/<link[^>]*href=[\"']([^\"']+)[\"']/i);\n const link = linkMatch ? linkMatch[1] : '';\n \n const titleMatch = entryXml.match(/<title[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]><\\/title>/i) ||\n entryXml.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i);\n const title = titleMatch ? titleMatch[1].replace(/<[^>]+>/g, '').trim() : 'No title';\n \n const dateMatch = entryXml.match(/<published>([^<]+)<\\/published>/i) ||\n entryXml.match(/<updated>([^<]+)<\\/updated>/i);\n const pubDate = dateMatch ? new Date(dateMatch[1]) : null;\n \n if (link && pubDate && pubDate >= yesterdayStart && pubDate <= yesterdayEnd) {\n allArticles.push({ title, url: link, category: feed.category, pubDate: pubDate.toISOString() });\n count++;\n }\n }\n } else {\n // Parse RSS\n const itemRegex = /<item[^>]*>([\\s\\S]*?)<\\/item>/gi;\n let match;\n let count = 0;\n \n while ((match = itemRegex.exec(xmlData)) !== null && count < 3) {\n const itemXml = match[1];\n \n const getTag = (tag) => {\n const regex = new RegExp(`<${tag}[^>]*><!\\\\[CDATA\\\\[([\\\\s\\\\S]*?)\\\\]\\\\]><\\/${tag}>|<${tag}[^>]*>([\\\\s\\\\S]*?)<\\/${tag}>`, 'i');\n const m = itemXml.match(regex);\n return m ? (m[1] || m[2] || '').trim() : '';\n };\n \n const title = getTag('title') || 'No title';\n const link = getTag('link');\n const pubDateStr = getTag('pubDate');\n const pubDate = pubDateStr ? new Date(pubDateStr) : null;\n \n if (link && pubDate && pubDate >= yesterdayStart && pubDate <= yesterdayEnd) {\n allArticles.push({ title, url: link, category: feed.category, pubDate: pubDate.toISOString() });\n count++;\n }\n }\n }\n } catch (error) {\n continue;\n }\n}\n\nif (allArticles.length === 0) {\n return [{ json: { empty: true, message: 'No recent articles found' } }];\n}\n\nreturn allArticles.map(a => ({ json: a }));"
},
"id": "4a3b0190-c8ba-45b9-a7ae-e0c71bdf47a8",
"name": "Fetch & Parse All Feeds",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-7392,
9696
]
},
{
"parameters": {
"options": {}
},
"id": "17dcef4d-b39e-49c6-8672-c3d3c3e2c1c1",
"name": "Loop Over Articles",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
-7216,
9696
]
},
{
"parameters": {
"operation": "scrape",
"url": "={{ $json.url }}",
"requestOptions": {}
},
"type": "@mendable/n8n-nodes-firecrawl.firecrawl",
"typeVersion": 1,
"position": [
-7008,
9776
],
"id": "b398aca6-1f9a-4aad-bd83-bd1f49081f37",
"name": "Scrape URL",
"credentials": {
"firecrawlApi": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "// Combine scraped content with original metadata\nconst scraped = $('Scrape URL').first().json;\nconst original = $('Loop Over Articles').first().json;\n\nif (!scraped || scraped.error) {\n return [{\n json: {\n title: original.title || 'No title',\n url: original.url,\n category: original.category,\n content: 'Content could not be scraped.'\n }\n }];\n}\n\nconst data = scraped.data || scraped;\n\nreturn [{\n json: {\n title: data.metadata?.title || original.title || 'No title',\n url: original.url,\n category: original.category,\n content: data.markdown || data.content || 'No content available'\n }\n}];"
},
"id": "64ecf827-8072-412a-8f54-6ea7114450c9",
"name": "Format Content",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-6832,
9776
]
},
{
"parameters": {
"promptType": "define",
"text": "=Summarize this news article:\n\n**Title:** {{ $json.title }}\n**Category:** {{ $json.category }}\n**URL:** {{ $json.url }}\n\n**Content:**\n{{ $json.content }}\n\n---\n\nProvide your summary in EXACTLY this format:\n\n**TLDR:** [One to two sentences summarizing the article]\n\n**Key Points:**\n- [First key point]\n- [Second key point]\n- [Third key point]\n\nIMPORTANT: List exactly 3 key points. No more, no less. Stop after the third point. These should be with - and not numbers.",
"options": {
"systemMessage": "You are a concise news summarization assistant. Output ONLY the formatted summary, nothing else."
}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 2.2,
"position": [
-6640,
9776
],
"id": "7733d1c1-80c8-4e2d-8658-3cc079972ba6",
"name": "AI Summary Agent"
},
{
"parameters": {
"jsCode": "const summary = $json.output || '';\nconst original = $('Format Content').first().json;\n\nreturn [{\n json: {\n category: original.category,\n url: original.url,\n title: original.title,\n summary: summary\n }\n}];"
},
"id": "3e3c020c-c060-4a30-b029-fa3d8c9d6529",
"name": "Store Summary",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-6352,
9776
]
},
{
"parameters": {
"amount": 3
},
"id": "0546a3f8-934d-4869-bd2b-5a6809a90c06",
"name": "Wait 3 Seconds",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
-6160,
9872
]
},
{
"parameters": {
"jsCode": "// Aggregate all summaries into final report\nconst items = $input.all();\n\nconst categories = [\n { key: 'Tech News', emoji: '\ud83d\udcbb' },\n { key: 'Homelab', emoji: '\ud83d\udda5\ufe0f' },\n { key: 'Linux', emoji: '\ud83d\udc27' },\n { key: 'GitHub Trending', emoji: '\u2b50' },\n { key: 'Hacker News', emoji: '\ud83d\udd25' }\n];\n\nconst today = new Date();\nconst dateFormatted = today.toLocaleDateString('en-US', { \n weekday: 'long', \n year: 'numeric', \n month: 'long', \n day: 'numeric' \n});\n\nlet report = `# Daily News Summary\\n`;\nreport += `*Generated on ${dateFormatted}*\\n\\n`;\n\nlet hasContent = false;\nfor (const cat of categories) {\n const catItems = items.filter(i => i.json.category === cat.key);\n if (catItems.length > 0) {\n hasContent = true;\n report += `## ${cat.emoji} ${cat.key}\\n\\n`;\n \n if (cat.key === 'GitHub Trending') {\n for (const item of catItems) {\n const title = item.json.title || 'Repository';\n const lang = item.json.language ? ` (${item.json.language})` : '';\n report += `### [${title}](${item.json.url})${lang}\\n\\n`;\n report += item.json.summary + '\\n\\n---\\n';\n }\n } else if (cat.key === 'Hacker News') {\n for (const item of catItems) {\n const title = item.json.title || 'Story';\n const points = item.json.points ? ` (${item.json.points} points)` : '';\n const priority = item.json.priority ? ' \ud83c\udfaf' : '';\n report += `### [${title}](${item.json.url})${points}${priority}\\n\\n`;\n report += item.json.summary + '\\n';\n if (item.json.hnUrl) {\n report += `\\n[Discuss on HN](${item.json.hnUrl})\\n`;\n }\n report += '\\n---\\n';\n }\n } else {\n for (const item of catItems) {\n const title = item.json.title || 'Article';\n report += `### [${title}](${item.json.url})\\n\\n`;\n report += item.json.summary + '\\n\\n---\\n';\n }\n }\n report += '\\n';\n }\n}\n\nif (!hasContent) {\n report += 'No articles were found.\\n';\n}\n\nreturn [{ json: { output: report } }];"
},
"id": "639e72ea-2e06-4f60-8ca0-2528056db797",
"name": "Aggregate Summaries",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-5360,
9680
]
},
{
"parameters": {
"jsCode": "// Convert markdown to clean HTML for email\nconst markdown = $json.output || '';\n\nlet html = markdown\n .replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\" style=\"color: #1a73e8; text-decoration: none;\">$1</a>')\n .replace(/^### (.+)$/gm, '<h3 style=\"font-weight: 600; font-size: 15px; margin: 12px 0 4px 0;\">\ud83d\udcc4 $1</h3>')\n .replace(/^# Daily News Summary$/gm, '<h1 style=\"font-weight: 600; font-size: 24px; margin: 0 0 2px 0;\">\ud83d\udcf0 Daily News Summary</h1>')\n .replace(/^## (\ud83d\udcbb|\ud83d\udda5\ufe0f|\ud83d\udc27|\u2b50|\ud83d\udd25) (.+)$/gm, '<h2 style=\"font-weight: 600; font-size: 17px; margin: 20px 0 8px 0;\">$1 $2</h2>')\n .replace(/^# (.+)$/gm, '<h1 style=\"font-weight: 600; font-size: 24px; margin: 0 0 2px 0;\">$1</h1>')\n .replace(/^## (.+)$/gm, '<h2 style=\"font-weight: 600; font-size: 17px; margin: 20px 0 8px 0;\">$2</h2>')\n .replace(/\\*\\*TLDR:\\*\\*/g, '<p><strong>\ud83d\udca1 TLDR:</strong>')\n .replace(/\\*\\*Key Points:\\*\\*/g, '</p><p><strong>\ud83d\udd11 Key Points:</strong></p>')\n .replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')\n .replace(/\ud83c\udfaf/g, '<span style=\"background: #fef3c7; padding: 2px 6px; border-radius: 4px; font-size: 12px;\">\ud83c\udfaf Priority</span>')\n .replace(/^- (.+)$/gm, '<li style=\"margin: 1px 0;\">$1</li>')\n .replace(/^\\* (.+)$/gm, '<li style=\"margin: 1px 0;\">$1</li>')\n .replace(/^\\d+\\.\\s+(.+)$/gm, '<li style=\"margin: 1px 0;\">$1</li>')\n .replace(/---/g, '<hr style=\"border: none; border-top: 1px solid #eee; margin: 10px 0;\">')\n .replace(/\\n/g, '');\n\nhtml = html.replace(/(<li[^>]*>.*?<\\/li>)+/g, '<ul style=\"padding-left: 18px; margin: 2px 0;\">$&</ul>');\n\nconst emailHtml = `<!DOCTYPE html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"></head><body style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.5; color: #333; max-width: 640px; margin: 0 auto; padding: 24px 16px;\">${html}<p style=\"color: #999; font-size: 11px; margin-top: 20px; padding-top: 10px; border-top: 1px solid #eee;\">\u2728 Generated automatically</p></body></html>`;\n\nreturn [{ json: { output: $json.output, htmlOutput: emailHtml } }];"
},
"id": "360d70ec-1172-49ee-aa12-88e32699ebab",
"name": "Convert to HTML",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-5136,
9680
]
},
{
"parameters": {
"sendTo": "brandon@hopkins.sh",
"subject": "=\ud83d\udcf0 Daily News Summary - {{ $now.format('EEEE, MMMM d, yyyy') }}",
"message": "={{ $json.htmlOutput }}",
"options": {}
},
"id": "4cbeab91-b891-408e-9141-e7cdfae69b8b",
"name": "Send Email Report",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
-4912,
9680
],
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"model": "mistral:latest",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatOllama",
"typeVersion": 1,
"position": [
-6640,
9936
],
"id": "bf5bdf7e-e142-4ea2-9cb2-f4db4807bb50",
"name": "Ollama Chat Model",
"credentials": {
"ollamaApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Extract and normalize metadata from RSS items\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const json = item.json;\n \n const hnLink = json.comments || json.link || '';\n const storyIdMatch = hnLink.match(/id=(\\d+)/);\n const storyId = storyIdMatch ? storyIdMatch[1] : '';\n \n const description = json.description || json.content || '';\n \n let points = 0;\n let commentCount = 0;\n \n const pointsMatch = description.match(/Points:\\s*(\\d+)/i);\n if (pointsMatch) {\n points = parseInt(pointsMatch[1], 10);\n }\n \n const commentsMatch = description.match(/Comments:\\s*(\\d+)/i);\n if (commentsMatch) {\n commentCount = parseInt(commentsMatch[1], 10);\n }\n \n results.push({\n json: {\n title: json.title || '',\n url: json.link || '',\n hnLink: json.comments || hnLink,\n storyId: storyId,\n points: points,\n commentCount: commentCount,\n pubDate: json.pubDate || json.isoDate || new Date().toISOString()\n }\n });\n}\n\nreturn results;"
},
"id": "f23a679d-c8a2-4f03-8254-cb182db5b4f3",
"name": "Extract HN Metadata",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-7216,
10128
]
},
{
"parameters": {
"jsCode": "// Filter out stories we've already processed\nconst incomingStories = $('Extract HN Metadata').all();\nconst processedData = $('Get Processed IDs1').all();\n\nconst processedIds = new Set();\nfor (const row of processedData) {\n if (row.json.story_id) {\n processedIds.add(String(row.json.story_id));\n }\n}\n\nconst newStories = [];\nfor (const story of incomingStories) {\n const storyId = String(story.json.storyId || '');\n if (storyId && !processedIds.has(storyId)) {\n newStories.push(story);\n }\n}\n\nif (newStories.length === 0) {\n return [{ json: { _empty: true, message: 'No new stories to process' } }];\n}\n\nreturn newStories;"
},
"id": "ae918942-d5c8-4a8d-956e-14a708f0d5e5",
"name": "Remove HN Duplicates",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-6816,
10128
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "condition-not-empty",
"leftValue": "={{ $json._empty }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "notEquals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "78a917e6-7778-4c12-9049-f4c54d36d69f",
"name": "Has New HN Stories?",
"type": "n8n-nodes-base.filter",
"typeVersion": 2.2,
"position": [
-6608,
10128
]
},
{
"parameters": {
"maxItems": 10
},
"id": "958305ed-b9d4-40a9-b7c3-b6fe34b2b7c7",
"name": "Limit HN to Top 10",
"type": "n8n-nodes-base.limit",
"typeVersion": 1,
"position": [
-6400,
10128
]
},
{
"parameters": {
"options": {}
},
"id": "3d5894dd-6582-4508-a410-c1f3c02ac134",
"name": "Loop Over HN Stories",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
-6192,
10128
]
},
{
"parameters": {
"operation": "scrape",
"url": "={{ $json.url }}",
"requestOptions": {}
},
"type": "@mendable/n8n-nodes-firecrawl.firecrawl",
"typeVersion": 1,
"position": [
-5760,
10128
],
"id": "34e1ad7b-b2bb-4f9c-932b-ad600dfbf514",
"name": "Scrape HN URL",
"credentials": {
"firecrawlApi": {
"name": "<your credential>"
}
},
"onError": "continueRegularOutput"
},
{
"parameters": {
"jsCode": "// Combine scraped content with original HN metadata\nconst scraped = $('Scrape HN URL').first().json;\nconst original = $('Loop Over HN Stories').first().json;\n\nlet content = 'Content could not be scraped.';\n\nif (scraped && !scraped.error) {\n const data = scraped.data || scraped;\n content = data.markdown || data.content || 'No content available';\n}\n\nreturn [{\n json: {\n title: original.title || 'No title',\n url: original.url,\n hnLink: original.hnLink,\n storyId: original.storyId,\n points: original.points,\n commentCount: original.commentCount,\n content: content\n }\n}];"
},
"id": "ae66b063-9fcd-402e-b040-a66c565a718b",
"name": "Format HN Content",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-5536,
10128
]
},
{
"parameters": {
"promptType": "define",
"text": "=Summarize this Hacker News article:\n\n**Title:** {{ $json.title }}\n**Points:** {{ $json.points }} | **Comments:** {{ $json.commentCount }}\n**URL:** {{ $json.url }}\n**HN Discussion:** {{ $json.hnLink }}\n\n**Content:**\n{{ $json.content }}\n\n---\n\nProvide your summary in EXACTLY this format:\n\n**TLDR:** [One to two sentences summarizing the article]\n\n**Why It Matters:** [One sentence on relevance to developers, sysadmins, or the tech community]",
"options": {
"systemMessage": "You are a concise tech news summarization assistant focused on developers, system administrators, and tech enthusiasts interested in Linux, self-hosting, homelabs, and open-source software. Flag content as priority if it relates to these topics. Output ONLY the formatted summary."
}
},
"type": "@n8n/n8n-nodes-langchain.agent",
"typeVersion": 2.2,
"position": [
-5344,
10128
],
"id": "611abb67-552f-4698-affb-f59d6bb00818",
"name": "AI Agent"
},
{
"parameters": {
"model": "mistral:latest",
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatOllama",
"typeVersion": 1,
"position": [
-5440,
10288
],
"id": "0afafe0d-edaf-4c72-9b11-f0e5566c9d9f",
"name": "Ollama HN Model",
"credentials": {
"ollamaApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Store HN summary with metadata\nconst summary = $json.output || '';\nconst original = $('Format HN Content').first().json;\n\n// Check if it's priority content based on keywords\nconst priorityKeywords = ['linux', 'open source', 'self-host', 'homelab', 'privacy', 'docker', 'kubernetes', 'proxmox', 'truenas', 'raspberry pi', 'ansible', 'terraform'];\nconst lowerTitle = (original.title || '').toLowerCase();\nconst lowerContent = (original.content || '').toLowerCase();\nconst isPriority = priorityKeywords.some(kw => lowerTitle.includes(kw) || lowerContent.includes(kw));\n\nreturn [{\n json: {\n category: 'Hacker News',\n url: original.url,\n hnUrl: original.hnLink,\n title: original.title,\n storyId: original.storyId,\n points: original.points,\n commentCount: original.commentCount,\n summary: summary,\n priority: isPriority\n }\n}];"
},
"id": "5ea65959-7de5-4b47-b790-d84b95ba65f2",
"name": "Store HN Summary",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-5088,
10128
]
},
{
"parameters": {
"amount": 3
},
"id": "dd8f7825-bf38-4564-841f-e0ecbe3d4e4e",
"name": "Wait 3 Seconds HN",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
-4864,
10224
]
},
{
"parameters": {
"jsCode": "// Prepare INSERT query for processed stories\nconst items = $input.all();\n\nconst validStories = items.filter(i => i.json.storyId && i.json.category === 'Hacker News');\n\nif (validStories.length === 0) {\n return [{ json: { _skip: true, message: 'No HN stories to insert' } }];\n}\n\nconst today = new Date().toISOString().split('T')[0];\n\nconst values = validStories.map(item => {\n const story = item.json;\n const storyId = String(story.storyId).replace(/'/g, \"''\");\n const title = String(story.title).replace(/'/g, \"''\");\n const url = String(story.url).replace(/'/g, \"''\");\n const category = 'Hacker News';\n const points = parseInt(story.points) || 0;\n const priority = story.priority ? 'TRUE' : 'FALSE';\n \n return `('${storyId}', '${title}', '${url}', ${points}, '${category}', '${today}', ${priority})`;\n}).join(',\\n');\n\nconst query = `INSERT INTO processed_stories (story_id, title, url, points, category, processed_date, is_priority)\nVALUES\n${values}\nON CONFLICT (story_id) DO UPDATE SET\n points = EXCLUDED.points,\n category = EXCLUDED.category,\n is_priority = EXCLUDED.is_priority`;\n\nreturn [{ json: { query: query, storyCount: validStories.length } }];"
},
"id": "e4711b70-29de-4a76-8418-cfc98ba2babf",
"name": "Build HN Insert",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-5136,
9856
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ $json.query }}",
"options": {}
},
"id": "171babfa-363e-40f9-9989-6da2339a2dea",
"name": "Insert HN Stories",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
-4912,
9856
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "filter-hn-valid",
"leftValue": "={{ $json.category }}",
"rightValue": "Hacker News",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "f0022c65-70e3-4744-908c-38e53142aba5",
"name": "Filter HN for Merge",
"type": "n8n-nodes-base.filter",
"typeVersion": 2.2,
"position": [
-5824,
9856
]
},
{
"parameters": {
"operation": "extractHtmlContent",
"extractionValues": {
"values": [
{
"key": "box",
"cssSelector": "div.Box",
"returnValue": "html"
}
]
},
"options": {}
},
"id": "94558806-acda-4124-9261-d2d1ada4efe6",
"name": "Extract Box1",
"type": "n8n-nodes-base.html",
"position": [
-7216,
9424
],
"typeVersion": 1.2
},
{
"parameters": {
"url": "https://github.com/trending?since=daily",
"options": {}
},
"id": "9c9e5adf-1f7b-4406-a5a8-56aad1653f70",
"name": "Request to Github Trend1",
"type": "n8n-nodes-base.httpRequest",
"position": [
-7392,
9424
],
"typeVersion": 4.2
},
{
"parameters": {
"fieldToSplitOut": "repositories",
"options": {}
},
"id": "c4d5a63b-8233-4a36-9968-f0e01c522aa6",
"name": "Turn to a list1",
"type": "n8n-nodes-base.splitOut",
"position": [
-6832,
9424
],
"typeVersion": 1
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "a0e76646-60d7-44a6-af77-33f27fb465cb",
"name": "author",
"type": "string",
"value": "={{ $json.repository.split('/')[0].trim() }}"
},
{
"id": "a2bd790a-784e-4d72-9a4e-92be22edea8f",
"name": "title",
"type": "string",
"value": "={{ $json.repository.split('/')[1].trim() }}"
},
{
"id": "22f1518a-7081-4417-ab9d-88f26a7b5cfe",
"name": "repository",
"type": "string",
"value": "={{ $json.repository }}"
},
{
"id": "baff9a9f-020a-4968-bb80-a4a91a94144a",
"name": "url",
"type": "string",
"value": "=https://github.com/{{ $json.repository.replaceAll(' ','') }}"
},
{
"id": "f5c48a02-b55d-4167-a823-53ac1d851ee5",
"name": "created_at",
"type": "string",
"value": "={{$now}}"
},
{
"id": "27a44ce9-4b5b-44b2-94d9-eb5b2ae81dcd",
"name": "description",
"type": "string",
"value": "={{ $json.description }}"
},
{
"id": "b1c2d3e4-f5g6-7h8i-9j0k-l1m2n3o4p5q6",
"name": "language",
"type": "string",
"value": "={{ $json.language }}"
}
]
},
"options": {}
},
"id": "7b1ecfa2-a30e-41f6-b554-d8a0df159112",
"name": "Set Result Variables1",
"type": "n8n-nodes-base.set",
"position": [
-6384,
9424
],
"typeVersion": 3.4
},
{
"parameters": {
"operation": "extractHtmlContent",
"dataPropertyName": "repositories",
"extractionValues": {
"values": [
{
"key": "repository",
"cssSelector": "a.Link"
},
{
"key": "language",
"cssSelector": "span[itemprop='programmingLanguage']"
},
{
"key": "description",
"cssSelector": "p"
}
]
},
"options": {}
},
"id": "012d9ad8-5c52-425e-9c54-3b399c2e4d18",
"name": "Extract repository data1",
"type": "n8n-nodes-base.html",
"position": [
-6608,
9424
],
"typeVersion": 1.2
},
{
"parameters": {
"operation": "extractHtmlContent",
"dataPropertyName": "box",
"extractionValues": {
"values": [
{
"key": "repositories",
"cssSelector": "article.Box-row",
"returnValue": "html",
"returnArray": true
}
]
},
"options": {
"trimValues": true,
"cleanUpText": true
}
},
"id": "c967ddbc-003a-4185-a1d4-e1f8ebb59515",
"name": "Extract all repositories1",
"type": "n8n-nodes-base.html",
"position": [
-7040,
9424
],
"typeVersion": 1.2
},
{
"parameters": {
"maxItems": 10
},
"id": "b0a0ee57-026e-43e8-ab62-3b6850d15e9d",
"name": "Limit to Top ",
"type": "n8n-nodes-base.limit",
"position": [
-6160,
9424
],
"typeVersion": 1
},
{
"parameters": {
"jsCode": "// Format GitHub repos to match news article structure\nreturn $input.all().map(item => ({\n json: {\n category: 'GitHub Trending',\n url: item.json.url,\n title: `${item.json.author}/${item.json.title}`,\n summary: item.json.description || 'No description available.',\n language: item.json.language || null\n }\n}));"
},
"id": "24443d7b-067b-463f-8f2a-1490d760ef51",
"name": "Format GitHub Items1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-5936,
9424
]
},
{
"parameters": {
"url": "https://hnrss.org/frontpage?points=75&count=25",
"options": {}
},
"id": "c2b1a7c2-00b1-4f59-8b01-ba50afe4aeef",
"name": "Fetch HN Trending1",
"type": "n8n-nodes-base.rssFeedRead",
"typeVersion": 1.1,
"position": [
-7392,
10128
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT story_id FROM processed_stories WHERE processed_date > CURRENT_DATE - INTERVAL '7 days'",
"options": {}
},
"id": "1a996401-56e5-4e02-8878-ca1cfdb4c595",
"name": "Get Processed IDs1",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
-7024,
10128
],
"alwaysOutputData": true,
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 7,
"triggerAtMinute": 30
}
]
}
},
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
-7776,
9696
],
"id": "4c1536ac-a3ab-4345-93c2-937366d3b6f2",
"name": "Run every day at 7AM"
},
{
"parameters": {
"sendTo": "nima@netbird.io",
"subject": "=\ud83d\udcf0 Daily News Summary - {{ $now.format('EEEE, MMMM d, yyyy') }}",
"message": "={{ $json.htmlOutput }}",
"options": {}
},
"id": "c69aefad-3122-4c5e-90f1-a08eb24bdc0e",
"name": "Send Email Report1",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
-4912,
9536
],
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Merge All Sources": {
"main": [
[
{
"node": "Aggregate Summaries",
"type": "main",
"index": 0
}
]
]
},
"Fetch & Parse All Feeds": {
"main": [
[
{
"node": "Loop Over Articles",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Articles": {
"main": [
[
{
"node": "Merge All Sources",
"type": "main",
"index": 1
}
],
[
{
"node": "Scrape URL",
"type": "main",
"index": 0
}
]
]
},
"Scrape URL": {
"main": [
[
{
"node": "Format Content",
"type": "main",
"index": 0
}
]
]
},
"Format Content": {
"main": [
[
{
"node": "AI Summary Agent",
"type": "main",
"index": 0
}
]
]
},
"AI Summary Agent": {
"main": [
[
{
"node": "Store Summary",
"type": "main",
"index": 0
}
]
]
},
"Store Summary": {
"main": [
[
{
"node": "Wait 3 Seconds",
"type": "main",
"index": 0
}
]
]
},
"Wait 3 Seconds": {
"main": [
[
{
"node": "Loop Over Articles",
"type": "main",
"index": 0
}
]
]
},
"Aggregate Summaries": {
"main": [
[
{
"node": "Convert to HTML",
"type": "main",
"index": 0
}
]
]
},
"Convert to HTML": {
"main": [
[
{
"node": "Send Email Report",
"type": "main",
"index": 0
},
{
"node": "Send Email Report1",
"type": "main",
"index": 0
}
]
]
},
"Ollama Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Summary Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Extract HN Metadata": {
"main": [
[
{
"node": "Get Processed IDs1",
"type": "main",
"index": 0
}
]
]
},
"Remove HN Duplicates": {
"main": [
[
{
"node": "Has New HN Stories?",
"type": "main",
"index": 0
}
]
]
},
"Has New HN Stories?": {
"main": [
[
{
"node": "Limit HN to Top 10",
"type": "main",
"index": 0
}
]
]
},
"Limit HN to Top 10": {
"main": [
[
{
"node": "Loop Over HN Stories",
"type": "main",
"index": 0
}
]
]
},
"Loop Over HN Stories": {
"main": [
[
{
"node": "Build HN Insert",
"type": "main",
"index": 0
},
{
"node": "Filter HN for Merge",
"type": "main",
"index": 0
}
],
[
{
"node": "Scrape HN URL",
"type": "main",
"index": 0
}
]
]
},
"Scrape HN URL": {
"main": [
[
{
"node": "Format HN Content",
"type": "main",
"index": 0
}
]
]
},
"Format HN Content": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"AI Agent": {
"main": [
[
{
"node": "Store HN Summary",
"type": "main",
"index": 0
}
]
]
},
"Ollama HN Model": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Store HN Summary": {
"main": [
[
{
"node": "Wait 3 Seconds HN",
"type": "main",
"index": 0
}
]
]
},
"Wait 3 Seconds HN": {
"main": [
[
{
"node": "Loop Over HN Stories",
"type": "main",
"index": 0
}
]
]
},
"Build HN Insert": {
"main": [
[
{
"node": "Insert HN Stories",
"type": "main",
"index": 0
}
]
]
},
"Filter HN for Merge": {
"main": [
[
{
"node": "Merge All Sources",
"type": "main",
"index": 2
}
]
]
},
"Extract Box1": {
"main": [
[
{
"node": "Extract all repositories1",
"type": "main",
"index": 0
}
]
]
},
"Request to Github Trend1": {
"main": [
[
{
"node": "Extract Box1",
"type": "main",
"index": 0
}
]
]
},
"Turn to a list1": {
"main": [
[
{
"node": "Extract repository data1",
"type": "main",
"index": 0
}
]
]
},
"Set Result Variables1": {
"main": [
[
{
"node": "Limit to Top ",
"type": "main",
"index": 0
}
]
]
},
"Extract repository data1": {
"main": [
[
{
"node": "Set Result Variables1",
"type": "main",
"index": 0
}
]
]
},
"Extract all repositories1": {
"main": [
[
{
"node": "Turn to a list1",
"type": "main",
"index": 0
}
]
]
},
"Limit to Top ": {
"main": [
[
{
"node": "Format GitHub Items1",
"type": "main",
"index": 0
}
]
]
},
"Format GitHub Items1": {
"main": [
[
{
"node": "Merge All Sources",
"type": "main",
"index": 0
}
]
]
},
"Fetch HN Trending1": {
"main": [
[
{
"node": "Extract HN Metadata",
"type": "main",
"index": 0
}
]
]
},
"Get Processed IDs1": {
"main": [
[
{
"node": "Remove HN Duplicates",
"type": "main",
"index": 0
}
]
]
},
"Run every day at 7AM": {
"main": [
[
{
"node": "Fetch HN Trending1",
"type": "main",
"index": 0
},
{
"node": "Request to Github Trend1",
"type": "main",
"index": 0
},
{
"node": "Fetch & Parse All Feeds",
"type": "main",
"index": 0
}
]
]
}
},
"meta": {
"templateCredsSetupCompleted": true
}
}
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.
firecrawlApigmailOAuth2ollamaApipostgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Rss-Daily. Uses @mendable/n8n-nodes-firecrawl, agent, gmail, lmChatOllama. Scheduled trigger; 38 nodes.
Source: https://github.com/TechHutTV/homelab/blob/7240cc3f15a58c083276d7c7e65ec77f5cb124bc/automations/rss-daily.json — 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.
Main. Uses httpRequest, rssFeedRead, agent, lmChatAzureOpenAi. Scheduled trigger; 59 nodes.
This workflow automates the process of generating, reviewing, and publishing blog posts across multiple platforms, now enhanced with support for RSS Feeds as a content source. It streamlines the manag
Automates sales data analysis and strategic insight generation for sales managers and strategists needing actionable intelligence. Fetches multi-source data from sales, marketing, and financial system
Scheduled runs collect data from oil markets, global shipping movements, news sources, and official reports. The system performs statistical checks to detect anomalies and volatility shifts. An AI-dri
Automatically compare AI-generated email drafts against what your support team actually sent, learn from the differences, and improve future drafts over time — without any model fine-tuning.