This workflow follows the HTTP Request → RSS Feed Read 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 →
{
"name": "PulseMosaic-v2-Single",
"nodes": [
{
"id": "manual-trigger",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
240,
300
],
"parameters": {}
},
{
"id": "schedule-trigger",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
240,
420
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 4
}
]
}
}
},
{
"id": "init-code",
"name": "Init",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
360
],
"parameters": {
"language": "javaScript",
"jsCode": "// Configuration\nconst rss_feeds = [\n \"https://hnrss.org/frontpage\",\n \"https://www.nasa.gov/rss/dyn/breaking_news.rss\"\n];\n\nconst topics = [\n \"ai safety\",\n \"model context protocol\",\n \"n8n automation\",\n \"vector databases\",\n \"prompt injection\"\n];\n\nconst config = {\n cache_ttl_minutes: 45,\n dedupe_ttl_days: 10,\n min_score: 5,\n max_items_total: 30,\n enable_llm_summary: true,\n ZAI_API_KEY: $env.ZAI_API_KEY || \"\",\n ZAI_BASE_URL: $env.ZAI_BASE_URL || \"https://api.z.ai/api/paas/v4\",\n ZAI_MODEL: $env.ZAI_MODEL || \"glm-5\"\n};\n\nreturn [{ json: { rss_feeds, topics, config } }];"
}
},
{
"id": "split-rss",
"name": "Split Out RSS Feeds",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
680,
240
],
"parameters": {
"fieldToSplitOut": "rss_feeds",
"destinationFieldName": "rss_url",
"options": {}
}
},
{
"id": "rss-read",
"name": "RSS Feed Read",
"type": "n8n-nodes-base.rssFeedRead",
"typeVersion": 1,
"position": [
900,
240
],
"parameters": {
"url": "={{ $json.rss_url }}",
"options": {}
}
},
{
"id": "norm-rss",
"name": "Normalize RSS",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1120,
240
],
"parameters": {
"language": "javaScript",
"jsCode": "const items = $input.all();\nconst results = [];\nconst feedUrl = items[0].json.rss_url;\n\nconst sourceMap = {\n \"hnrss.org\": \"hackernews\",\n \"nasa.gov\": \"nasa\"\n};\n\nlet source = \"rss\";\nfor (const [key, val] of Object.entries(sourceMap)) {\n if (feedUrl.includes(key)) source = val;\n}\n\nfor (const item of items) {\n const data = item.json;\n results.push({\n json: {\n source: source,\n topic: null,\n title: data.title || \"Untitled\",\n url: data.link || data.guid || \"\",\n published_at: data.pubDate || data.isoDate || new Date().toISOString(),\n raw_score: 0,\n score: 5,\n summary_raw: data.contentSnippet || data.description || \"\",\n summary: null,\n meta: {\n author: data.creator || data.author || null,\n venue: source,\n cached: false\n }\n }\n });\n}\n\nreturn results;"
}
},
{
"id": "split-topics",
"name": "Split Out Topics",
"type": "n8n-nodes-base.splitOut",
"typeVersion": 1,
"position": [
680,
480
],
"parameters": {
"fieldToSplitOut": "topics",
"destinationFieldName": "topic",
"options": {}
}
},
{
"id": "cache-gate",
"name": "Cache Gate",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
480
],
"parameters": {
"language": "javaScript",
"jsCode": "const topic = $json.topic;\nconst config = $('Init').first().json.config;\nconst st = $getWorkflowStaticData('global');\n\nif (!st.cache) st.cache = {};\n\nconst now = Date.now();\nconst ttl_ms = config.cache_ttl_minutes * 60 * 1000;\n\n// Prune expired\nfor (const k in st.cache) {\n if (now - st.cache[k].ts_epoch_ms > ttl_ms) {\n delete st.cache[k];\n }\n}\n\nconst cached = st.cache[topic];\nif (cached && (now - cached.ts_epoch_ms < ttl_ms)) {\n return [{ json: { topic, config, cache_hit: true, cached_items: cached.items } }];\n}\n\nreturn [{ json: { topic, config, cache_hit: false } }];"
}
},
{
"id": "if-cache",
"name": "IF Cache Hit",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1120,
480
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.cache_hit }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"id": "emit-cached",
"name": "Emit Cached Items",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
380
],
"parameters": {
"language": "javaScript",
"jsCode": "const items = $json.cached_items;\nreturn items.map(i => ({ json: { ...i, meta: { ...i.meta, cached: true } } }));"
}
},
{
"id": "fetch-so",
"name": "Fetch StackOverflow",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1340,
580
],
"parameters": {
"url": "={{ 'https://api.stackexchange.com/2.3/search/advanced?order=desc&sort=activity&site=stackoverflow&q=' + encodeURIComponent($('Cache Gate').first().json.topic) + '&pagesize=5' }}",
"method": "GET",
"options": {
"continueOnFail": true
}
}
},
{
"id": "fetch-cr",
"name": "Fetch Crossref",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1340,
700
],
"parameters": {
"url": "={{ 'https://api.crossref.org/works?query=' + encodeURIComponent($('Cache Gate').first().json.topic) + '&rows=5' }}",
"method": "GET",
"options": {
"continueOnFail": true
}
}
},
{
"id": "norm-so",
"name": "Normalize StackOverflow",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
580
],
"parameters": {
"language": "javaScript",
"jsCode": "const data = $json;\nconst topic = $('Cache Gate').first().json.topic;\nconst items = [];\n\nif (data.items && Array.isArray(data.items)) {\n for (const i of data.items) {\n items.push({\n json: {\n source: \"stackoverflow\",\n topic: topic,\n title: i.title,\n url: i.link,\n published_at: new Date(i.creation_date * 1000).toISOString(),\n raw_score: i.score || 0,\n score: (i.score || 0) + (i.answer_count || 0),\n summary_raw: i.excerpt || \"\",\n summary: null,\n meta: { author: i.owner.display_name, venue: \"StackOverflow\", cached: false }\n }\n });\n }\n}\nreturn items;"
}
},
{
"id": "norm-cr",
"name": "Normalize Crossref",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
700
],
"parameters": {
"language": "javaScript",
"jsCode": "const data = $json;\nconst topic = $('Cache Gate').first().json.topic;\nconst items = [];\n\nif (data.message && data.message.items) {\n for (const i of data.message.items) {\n const title = (Array.isArray(i.title) ? i.title[0] : i.title) || \"Untitled\";\n const url = (i.URL && i.URL.startsWith('http')) ? i.URL : `https://doi.org/${i.DOI}`;\n const refs = i['is-referenced-by-count'] || 0;\n items.push({\n json: {\n source: \"crossref\",\n topic: topic,\n title: title,\n url: url,\n published_at: i.published ? i.published['date-parts'][0].join('-') : null,\n raw_score: refs,\n score: 20 + refs,\n summary_raw: i.abstract || \"\",\n summary: null,\n meta: { author: null, venue: i['container-title'] ? i['container-title'][0] : \"Journal\", cached: false }\n }\n });\n }\n}\nreturn items;"
}
},
{
"id": "merge-sources",
"name": "Merge Append (SO+CR)",
"type": "n8n-nodes-base.merge",
"typeVersion": 3,
"position": [
1780,
640
],
"parameters": {
"mode": "combine",
"combinationMode": "mergeByPosition",
"options": {}
}
},
{
"id": "cache-save",
"name": "Cache Save",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2000,
640
],
"parameters": {
"language": "javaScript",
"jsCode": "const items = $input.all().map(i => i.json);\nconst topic = $('Cache Gate').first().json.topic;\nconst st = $getWorkflowStaticData('global');\n\nst.cache[topic] = {\n ts_epoch_ms: Date.now(),\n items: items\n};\n\nreturn items.map(i => ({ json: i }));"
}
},
{
"id": "merge-streams",
"name": "Merge Streams",
"type": "n8n-nodes-base.merge",
"typeVersion": 3,
"position": [
2220,
440
],
"parameters": {
"mode": "combine",
"combinationMode": "mergeByPosition",
"options": {}
}
},
{
"id": "finalize",
"name": "Finalize",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2440,
440
],
"parameters": {
"language": "javaScript",
"jsCode": "const config = $('Init').first().json.config;\nconst st = $getWorkflowStaticData('global');\nif (!st.seen) st.seen = {};\n\nconst now = Date.now();\nconst dedupe_ms = config.dedupe_ttl_days * 24 * 60 * 60 * 1000;\n\n// Prune seen\nfor (const k in st.seen) {\n if (now - st.seen[k] > dedupe_ms) delete st.seen[k];\n}\n\nconst allItems = $input.all().map(i => i.json);\nconst stats = {\n candidates_total: allItems.length,\n new_items: 0,\n cached_hits: 0,\n api_calls: 0,\n failed_calls: 0,\n by_source: {}\n};\n\nconst final = [];\nfor (const i of allItems) {\n stats.by_source[i.source] = (stats.by_source[i.source] || 0) + 1;\n if (i.meta.cached) stats.cached_hits++;\n \n if (i.url && !st.seen[i.url]) {\n if (i.score >= config.min_score) {\n final.push(i);\n st.seen[i.url] = now;\n stats.new_items++;\n }\n }\n}\n\nfinal.sort((a, b) => b.score - a.score);\nconst sliced = final.slice(0, config.max_items_total);\n\nreturn [{ json: {\n config,\n stats,\n items: sliced,\n run_id: $workflow.id,\n generated_at: new Date().toISOString(),\n topics: $('Init').first().json.topics\n} }];"
}
},
{
"id": "if-llm",
"name": "IF LLM",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
2660,
440
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.config.enable_llm_summary }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
},
{
"leftValue": "={{ $json.config.ZAI_API_KEY }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "isNotEmpty"
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"id": "prep-llm",
"name": "Prepare LLM",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2880,
340
],
"parameters": {
"language": "javaScript",
"jsCode": "const data = $json;\nconst topItems = data.items.slice(0, 12);\nconst content = topItems.map(i => `URL: ${i.url}\\nTitle: ${i.title}\\nRaw: ${i.summary_raw || ''}`).join('\\n\\n');\n\nreturn [{\n json: {\n ...data,\n llm_payload: {\n model: data.config.ZAI_MODEL,\n messages: [{\n role: \"user\",\n content: `Summarize the following items into a JSON object with key 'summaries', an array of objects with 'url' and 'summary' (max 20 words).\\n\\n${content}`\n }],\n temperature: 0.3\n }\n }\n}];"
}
},
{
"id": "req-llm",
"name": "HTTP Request LLM",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
3100,
340
],
"parameters": {
"authentication": "none",
"url": "={{ $json.config.ZAI_BASE_URL + '/chat/completions' }}",
"method": "POST",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $json.config.ZAI_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"jsonBody": "={{ JSON.stringify({ ...$json.llm_payload, response_format: { type: 'json_object' } }) }}",
"options": {
"continueOnFail": true
}
}
},
{
"id": "apply-llm",
"name": "Apply Summaries",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3320,
340
],
"parameters": {
"language": "javaScript",
"jsCode": "const original = $('Prepare LLM').first().json;\nlet summariesMap = {};\n\ntry {\n const content = $json.choices?.[0]?.message?.content;\n if (content) {\n const parsed = JSON.parse(content);\n if (Array.isArray(parsed.summaries)) {\n for (const s of parsed.summaries) {\n summariesMap[s.url] = s.summary;\n }\n }\n }\n} catch (e) { console.error('LLM Parse Fail', e); }\n\nconst items = original.items.map(i => ({\n json: {\n ...i,\n summary: summariesMap[i.url] || i.summary_raw || i.title\n }\n}));\n\nreturn [{ json: { ...original, items } }];"
}
},
{
"id": "fallback-llm",
"name": "Fallback Summary",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2880,
540
],
"parameters": {
"language": "javaScript",
"jsCode": "const data = $json;\nconst items = data.items.map(i => ({\n json: {\n ...i,\n summary: i.summary_raw || i.title\n }\n}));\nreturn [{ json: { ...data, items } }];"
}
},
{
"id": "qa-lint",
"name": "QA Lint",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3540,
440
],
"parameters": {
"language": "javaScript",
"jsCode": "const data = $json;\nif (!Array.isArray(data.items)) throw new Error('Invalid items');\nfor (const i of data.items) {\n if (!i.url || typeof i.score !== 'number') throw new Error('Invalid item fields');\n if (JSON.stringify(i).includes('{{')) throw new Error('Mustache detected');\n}\nreturn [{ json: data }];"
}
},
{
"id": "build-final",
"name": "Build Final",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3760,
440
],
"parameters": {
"language": "javaScript",
"jsCode": "const data = $json;\nconst groups = {};\nfor (const i of data.items) {\n if (!groups[i.source]) groups[i.source] = [];\n groups[i.source].push(i);\n}\n\nlet md = `# PulseMosaic Digest - ${data.generated_at}\\n\\n`;\nfor (const [src, items] of Object.entries(groups)) {\n md += `## ${src.toUpperCase()}\\n`;\n for (const i of items) {\n md += `- [${i.title}](${i.url}) (Score: ${i.score})\\n`;\n if (i.summary) md += ` > ${i.summary}\\n`;\n }\n md += `\\n`;\n}\n\nreturn [{ json: { ...data, digest_markdown: md } }];"
}
}
],
"connections": {
"Manual Trigger": {
"main": [
[
{
"node": "Init",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Init",
"type": "main",
"index": 0
}
]
]
},
"Init": {
"main": [
[
{
"node": "Split Out RSS Feeds",
"type": "main",
"index": 0
},
{
"node": "Split Out Topics",
"type": "main",
"index": 0
}
]
]
},
"Split Out RSS Feeds": {
"main": [
[
{
"node": "RSS Feed Read",
"type": "main",
"index": 0
}
]
]
},
"RSS Feed Read": {
"main": [
[
{
"node": "Normalize RSS",
"type": "main",
"index": 0
}
]
]
},
"Normalize RSS": {
"main": [
[
{
"node": "Merge Streams",
"type": "main",
"index": 0
}
]
]
},
"Split Out Topics": {
"main": [
[
{
"node": "Cache Gate",
"type": "main",
"index": 0
}
]
]
},
"Cache Gate": {
"main": [
[
{
"node": "IF Cache Hit",
"type": "main",
"index": 0
}
]
]
},
"IF Cache Hit": {
"main": [
[
{
"node": "Emit Cached Items",
"type": "main",
"index": 0
}
],
[
{
"node": "Fetch StackOverflow",
"type": "main",
"index": 0
},
{
"node": "Fetch Crossref",
"type": "main",
"index": 0
}
]
]
},
"Emit Cached Items": {
"main": [
[
{
"node": "Merge Streams",
"type": "main",
"index": 1
}
]
]
},
"Fetch StackOverflow": {
"main": [
[
{
"node": "Normalize StackOverflow",
"type": "main",
"index": 0
}
]
]
},
"Fetch Crossref": {
"main": [
[
{
"node": "Normalize Crossref",
"type": "main",
"index": 0
}
]
]
},
"Normalize StackOverflow": {
"main": [
[
{
"node": "Merge Append (SO+CR)",
"type": "main",
"index": 0
}
]
]
},
"Normalize Crossref": {
"main": [
[
{
"node": "Merge Append (SO+CR)",
"type": "main",
"index": 1
}
]
]
},
"Merge Append (SO+CR)": {
"main": [
[
{
"node": "Cache Save",
"type": "main",
"index": 0
}
]
]
},
"Cache Save": {
"main": [
[
{
"node": "Merge Streams",
"type": "main",
"index": 2
}
]
]
},
"Merge Streams": {
"main": [
[
{
"node": "Finalize",
"type": "main",
"index": 0
}
]
]
},
"Finalize": {
"main": [
[
{
"node": "IF LLM",
"type": "main",
"index": 0
}
]
]
},
"IF LLM": {
"main": [
[
{
"node": "Prepare LLM",
"type": "main",
"index": 0
}
],
[
{
"node": "Fallback Summary",
"type": "main",
"index": 0
}
]
]
},
"Prepare LLM": {
"main": [
[
{
"node": "HTTP Request LLM",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request LLM": {
"main": [
[
{
"node": "Apply Summaries",
"type": "main",
"index": 0
}
]
]
},
"Apply Summaries": {
"main": [
[
{
"node": "QA Lint",
"type": "main",
"index": 0
}
]
]
},
"Fallback Summary": {
"main": [
[
{
"node": "QA Lint",
"type": "main",
"index": 0
}
]
]
},
"QA Lint": {
"main": [
[
{
"node": "Build Final",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
PulseMosaic-v2-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 25 nodes.
Source: https://github.com/turtir-ai/n8n-workflow-studio/blob/main/public/fixtures/PulseMosaic-v2-Single.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.
GoldPulse-v1.1-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 33 nodes.
AegisPulse-v1.1-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 31 nodes.
QuorumPulse-v1.1-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 29 nodes.
PulseMosaic-v3.1-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 25 nodes.
SignalForge-v2.1-gold. Uses httpRequest, rssFeedRead. Event-driven trigger; 24 nodes.