AutomationFlowsWeb Scraping › Pulsemosaic V2 Single

Pulsemosaic V2 Single

PulseMosaic-v2-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 25 nodes.

Event trigger★★★★☆ complexity25 nodesRSS Feed ReadHTTP Request
Web Scraping Trigger: Event Nodes: 25 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "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
          }
        ]
      ]
    }
  }
}
Pro

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 →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

GoldPulse-v1.1-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 33 nodes.

RSS Feed Read, HTTP Request
Web Scraping

AegisPulse-v1.1-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 31 nodes.

RSS Feed Read, HTTP Request
Web Scraping

QuorumPulse-v1.1-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 29 nodes.

RSS Feed Read, HTTP Request
Web Scraping

PulseMosaic-v3.1-Single. Uses rssFeedRead, httpRequest. Event-driven trigger; 25 nodes.

RSS Feed Read, HTTP Request
Web Scraping

SignalForge-v2.1-gold. Uses httpRequest, rssFeedRead. Event-driven trigger; 24 nodes.

HTTP Request, RSS Feed Read