AutomationFlowsData & Sheets › Aggregate Tech Trend Signals From RSS Feeds Into Google Sheets and Slack

Aggregate Tech Trend Signals From RSS Feeds Into Google Sheets and Slack

ByVeena Pandian @veenapandian on n8n.io

Founders, product managers, content strategists, indie hackers, and anyone who wants to automatically monitor tech industry trends across multiple sources — without manually browsing Hacker News and Product Hunt every day.

Cron / scheduled trigger★★★★☆ complexity21 nodesHTTP RequestGoogle SheetsSlackGmail
Data & Sheets Trigger: Cron / scheduled Nodes: 21 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #13839 — we link there as the canonical source.

This workflow follows the Gmail → 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 →

Download .json
{
  "id": "CprHW1jo93EjF1wy",
  "name": "Aggregate trend signals to Google Sheets and Slack",
  "tags": [],
  "nodes": [
    {
      "id": "e0aa7c36-3e3b-4433-8fe9-2df2a24bbfda",
      "name": "Daily Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        736,
        432
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 24
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "44403338-5ecd-4d8f-a423-4794ed804449",
      "name": "Fetch Hacker News RSS",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        960,
        336
      ],
      "parameters": {
        "url": "https://hnrss.org/newest?points=50&count=30",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "856fee05-7dd2-4bbe-8580-b8e10986238e",
      "name": "Fetch Product Hunt RSS",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        960,
        528
      ],
      "parameters": {
        "url": "https://www.producthunt.com/feed",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "5e540b05-7780-41e7-aa16-88bf0a7cbedd",
      "name": "Wait for All Feeds",
      "type": "n8n-nodes-base.merge",
      "position": [
        1184,
        432
      ],
      "parameters": {},
      "typeVersion": 3
    },
    {
      "id": "80c7a7f6-84fe-4d81-be24-498bc4a0e87a",
      "name": "Parse All RSS Feeds",
      "type": "n8n-nodes-base.code",
      "position": [
        1408,
        432
      ],
      "parameters": {
        "jsCode": "// Parse RSS/Atom feeds into normalized items\n// Grab data from each named feed node directly\n\nconst rawFeeds = [];\n\ntry {\n  const hn = $('Fetch Hacker News RSS').all();\n  for (const item of hn) {\n    const str = typeof item.json === 'string' ? item.json : (item.json.data || JSON.stringify(item.json));\n    rawFeeds.push({ name: 'hackernews', weight: 1.5, xml: str });\n  }\n} catch(e) {}\n\ntry {\n  const ph = $('Fetch Product Hunt RSS').all();\n  for (const item of ph) {\n    const str = typeof item.json === 'string' ? item.json : (item.json.data || JSON.stringify(item.json));\n    rawFeeds.push({ name: 'producthunt', weight: 1.3, xml: str });\n  }\n} catch(e) {}\n\nfunction extractItems(xml, sourceName, weight) {\n  const items = [];\n  if (!xml || xml.length < 50) return items;\n  \n  const itemRegex = /<(?:item|entry)[^>]*>([\\s\\S]*?)<\\/(?:item|entry)>/gi;\n  let match;\n  \n  while ((match = itemRegex.exec(xml)) !== null) {\n    const block = match[1];\n    \n    const getTag = (tag) => {\n      const r = new RegExp(\n        '<' + tag + '[^>]*><!\\\\[CDATA\\\\[([\\\\s\\\\S]*?)\\\\]\\\\]><\\\\/' + tag + '>|' +\n        '<' + tag + '[^>]*>([\\\\s\\\\S]*?)<\\\\/' + tag + '>',\n        'i'\n      );\n      const m = block.match(r);\n      return m ? (m[1] || m[2] || '').trim() : '';\n    };\n    \n    let link = getTag('link');\n    if (!link) {\n      const hrefMatch = block.match(/href=\"([^\"]+)\"/i);\n      if (hrefMatch) link = hrefMatch[1];\n    }\n    \n    const title = getTag('title').replace(/<[^>]+>/g, '').trim();\n    const desc = getTag('description').replace(/<[^>]+>/g, '').substring(0, 300);\n    const content = getTag('content').replace(/<[^>]+>/g, '').substring(0, 300) || desc;\n    const pubDate = getTag('pubDate') || getTag('published') || getTag('updated') || '';\n    \n    if (!title) continue;\n    \n    items.push({\n      source: sourceName,\n      source_weight: weight,\n      title: title,\n      url: link,\n      description: (content || desc).substring(0, 200),\n      published: pubDate,\n      raw_text: (title + ' ' + desc + ' ' + content).toLowerCase()\n    });\n  }\n  \n  return items;\n}\n\nlet allItems = [];\n\nfor (const feed of rawFeeds) {\n  try {\n    const items = extractItems(feed.xml, feed.name, feed.weight);\n    allItems = allItems.concat(items);\n  } catch(e) {}\n}\n\nif (allItems.length === 0) {\n  return [{ json: { items: [], total_fetched: 0, feeds_processed: rawFeeds.length, error: 'No items parsed from any feed.' } }];\n}\n\nreturn [{ json: { items: allItems, total_fetched: allItems.length, feeds_processed: rawFeeds.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "e44a28b9-0a58-4298-9213-ebe8f72c3871",
      "name": "Score and Classify Signals",
      "type": "n8n-nodes-base.code",
      "position": [
        1632,
        432
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst items = data.items;\n\nif (!items || items.length === 0) {\n  return [{ json: { scored_items: [], total_signals: 0, total_fetched: 0, error: 'No items to score' } }];\n}\n\n// ============================================\n// CUSTOMIZE YOUR KEYWORD GROUPS BELOW\n// Add, remove, or adjust keywords and weights\n// to match your industry and interests\n// ============================================\nconst keywordGroups = {\n  'ai_automation': {\n    keywords: ['ai agent', 'ai automation', 'llm', 'gpt', 'claude', 'copilot', 'ai tool', 'artificial intelligence', 'machine learning', 'generative ai', 'ai-powered', 'langchain', 'rag'],\n    weight: 2.0,\n    category: 'AI & Automation'\n  },\n  'no_code': {\n    keywords: ['no-code', 'nocode', 'low-code', 'lowcode', 'n8n', 'zapier', 'make.com', 'automation platform', 'workflow automation', 'airtable'],\n    weight: 1.8,\n    category: 'No-Code / Low-Code'\n  },\n  'saas_business': {\n    keywords: ['saas', 'mrr', 'arr', 'churn', 'b2b saas', 'micro-saas', 'bootstrapped', 'indie hacker', 'recurring revenue', 'subscription'],\n    weight: 1.5,\n    category: 'SaaS & Business'\n  },\n  'developer_tools': {\n    keywords: ['developer tool', 'dev tool', 'api', 'sdk', 'open source', 'cli tool', 'developer experience', 'dx', 'devops', 'infrastructure'],\n    weight: 1.3,\n    category: 'Developer Tools'\n  },\n  'marketing_growth': {\n    keywords: ['seo', 'content marketing', 'growth hack', 'conversion', 'landing page', 'email marketing', 'organic traffic', 'product-led', 'plg', 'go-to-market'],\n    weight: 1.4,\n    category: 'Marketing & Growth'\n  },\n  'pricing_monetization': {\n    keywords: ['pricing', 'monetization', 'freemium', 'paywall', 'revenue model', 'billing', 'payment', 'stripe', 'pricing strategy'],\n    weight: 1.6,\n    category: 'Pricing & Monetization'\n  },\n  'remote_productivity': {\n    keywords: ['remote work', 'async', 'productivity', 'collaboration tool', 'project management', 'notion', 'obsidian', 'second brain', 'knowledge management'],\n    weight: 1.1,\n    category: 'Remote & Productivity'\n  }\n};\n\nconst scoredItems = [];\n\nfor (const item of items) {\n  const text = item.raw_text || '';\n  let totalScore = 0;\n  const matchedGroups = [];\n  const matchedKeywords = [];\n  \n  for (const [groupId, group] of Object.entries(keywordGroups)) {\n    let groupHits = 0;\n    \n    for (const kw of group.keywords) {\n      if (text.includes(kw)) {\n        groupHits++;\n        matchedKeywords.push(kw);\n      }\n    }\n    \n    if (groupHits > 0) {\n      const groupScore = groupHits * group.weight;\n      totalScore += groupScore;\n      matchedGroups.push({\n        group: group.category,\n        hits: groupHits,\n        score: parseFloat(groupScore.toFixed(2))\n      });\n    }\n  }\n  \n  totalScore = totalScore * (item.source_weight || 1);\n  \n  const titleText = (item.title || '').toLowerCase();\n  let titleBonus = 0;\n  for (const [groupId, group] of Object.entries(keywordGroups)) {\n    for (const kw of group.keywords) {\n      if (titleText.includes(kw)) {\n        titleBonus += 1.5;\n      }\n    }\n  }\n  totalScore += titleBonus;\n  \n  if (totalScore > 0) {\n    scoredItems.push({\n      title: item.title,\n      url: item.url,\n      source: item.source,\n      description: item.description,\n      published: item.published,\n      signal_score: parseFloat(totalScore.toFixed(2)),\n      matched_groups: matchedGroups,\n      matched_keywords: [...new Set(matchedKeywords)],\n      primary_category: matchedGroups.length > 0 \n        ? matchedGroups.sort((a, b) => b.score - a.score)[0].group \n        : 'General',\n      title_match: titleBonus > 0\n    });\n  }\n}\n\nscoredItems.sort((a, b) => b.signal_score - a.signal_score);\n\nreturn [{ json: { scored_items: scoredItems, total_signals: scoredItems.length, total_fetched: items.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "5c8edc2d-9950-40b3-b722-5d372f13cd04",
      "name": "Aggregate Signals into Themes",
      "type": "n8n-nodes-base.code",
      "position": [
        1856,
        432
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst items = data.scored_items || [];\n\nif (items.length === 0) {\n  const today = new Date().toISOString().split('T')[0];\n  return [{\n    json: {\n      date: today,\n      themes: [],\n      total_signals: 0,\n      total_fetched: data.total_fetched || 0,\n      top_items: [],\n      note: 'No keyword matches found today.'\n    }\n  }];\n}\n\nconst themes = {};\n\nfor (const item of items) {\n  const cat = item.primary_category;\n  if (!themes[cat]) {\n    themes[cat] = {\n      category: cat,\n      items: [],\n      total_score: 0,\n      source_diversity: new Set(),\n      all_keywords: []\n    };\n  }\n  themes[cat].items.push(item);\n  themes[cat].total_score += item.signal_score;\n  themes[cat].source_diversity.add(item.source);\n  themes[cat].all_keywords.push(...(item.matched_keywords || []));\n}\n\nconst themeResults = [];\n\nfor (const [cat, theme] of Object.entries(themes)) {\n  const itemCount = theme.items.length;\n  const sourceDiversity = theme.source_diversity.size;\n  const avgScore = theme.total_score / itemCount;\n  \n  const diversityBonus = 1 + (sourceDiversity - 1) * 0.3;\n  const volumeBonus = 1 + Math.log2(Math.max(itemCount, 1)) * 0.2;\n  const themeStrength = parseFloat((theme.total_score * diversityBonus * volumeBonus).toFixed(2));\n  \n  let signalLevel;\n  if (themeStrength >= 30) signalLevel = 'VERY_STRONG';\n  else if (themeStrength >= 15) signalLevel = 'STRONG';\n  else if (themeStrength >= 8) signalLevel = 'MODERATE';\n  else signalLevel = 'WEAK';\n  \n  const kwFreq = {};\n  for (const kw of theme.all_keywords) {\n    kwFreq[kw] = (kwFreq[kw] || 0) + 1;\n  }\n  const topKeywords = Object.entries(kwFreq)\n    .sort((a, b) => b[1] - a[1])\n    .slice(0, 5)\n    .map(([kw, count]) => kw + ' (' + count + ')');\n  \n  themeResults.push({\n    category: cat,\n    signal_level: signalLevel,\n    theme_strength: themeStrength,\n    item_count: itemCount,\n    sources: [...theme.source_diversity],\n    source_count: sourceDiversity,\n    avg_item_score: parseFloat(avgScore.toFixed(2)),\n    top_keywords: topKeywords,\n    top_items: theme.items\n      .sort((a, b) => b.signal_score - a.signal_score)\n      .slice(0, 5)\n      .map(i => ({\n        title: i.title,\n        source: i.source,\n        score: i.signal_score,\n        url: i.url\n      }))\n  });\n}\n\nthemeResults.sort((a, b) => b.theme_strength - a.theme_strength);\n\nconst today = new Date().toISOString().split('T')[0];\n\nreturn [{\n  json: {\n    date: today,\n    themes: themeResults,\n    total_signals: data.total_signals,\n    total_fetched: data.total_fetched,\n    top_items: items.slice(0, 15).map(i => ({\n      title: i.title,\n      source: i.source,\n      score: i.signal_score,\n      category: i.primary_category,\n      url: i.url\n    }))\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "7a566c27-041c-4164-9d7b-be035c22a036",
      "name": "Build Intelligence Report",
      "type": "n8n-nodes-base.code",
      "position": [
        2080,
        432
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst themes = data.themes || [];\nconst topItems = data.top_items || [];\nconst today = data.date || new Date().toISOString().split('T')[0];\n\nif (themes.length === 0 && topItems.length === 0) {\n  const report = 'SIGNAL INTELLIGENCE REPORT \u2014 ' + today + '\\n\\nSources scanned: ' + (data.total_fetched || 0) + ' items\\nNo matching signals found today.\\n\\nTip: Broaden keywords in the Score node, or add more RSS sources.';\n  return [{\n    json: {\n      report: report,\n      report_html: '<h2>Signal Report \u2014 ' + today + '</h2><p>No matching signals found today.</p>',\n      date: today,\n      themes: [],\n      top_items: [],\n      strong_theme_count: 0,\n      total_signals: 0\n    }\n  }];\n}\n\nlet report = 'SIGNAL INTELLIGENCE REPORT \u2014 ' + today + '\\n';\nreport += 'Sources scanned: ' + data.total_fetched + ' items \u2192 ' + data.total_signals + ' signals detected\\n';\nreport += '\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n\\n';\nreport += 'THEME STRENGTH RANKING:\\n\\n';\n\nfor (const theme of themes) {\n  report += theme.signal_level + '  ' + theme.category + '\\n';\n  report += '   Strength: ' + theme.theme_strength + ' | Items: ' + theme.item_count + ' | Sources: ' + theme.sources.join(', ') + '\\n';\n  report += '   Keywords: ' + theme.top_keywords.join(', ') + '\\n\\n';\n}\n\nreport += '\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n';\nreport += 'TOP 10 STRONGEST SIGNALS:\\n\\n';\n\nfor (let i = 0; i < Math.min(topItems.length, 10); i++) {\n  const item = topItems[i];\n  report += (i + 1) + '. ' + item.title + '\\n';\n  report += '   Score: ' + item.score + ' | Source: ' + item.source + ' | Theme: ' + item.category + '\\n';\n  if (item.url) report += '   ' + item.url + '\\n';\n  report += '\\n';\n}\n\nconst strongThemes = themes.filter(t => t.theme_strength >= 15);\nif (strongThemes.length > 0) {\n  report += '\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n';\n  report += 'ACTION ITEMS \u2014 Strong signals detected:\\n\\n';\n  for (const t of strongThemes) {\n    report += '\u2022 ' + t.category + ' is surging across ' + t.source_count + ' sources. ';\n    report += 'Top keyword: ' + (t.top_keywords[0] || 'n/a') + '. ';\n    report += 'Consider creating content or building in this space.\\n';\n  }\n}\n\nlet html = '<h2>Signal Intelligence Report \u2014 ' + today + '</h2>';\nhtml += '<p>Sources scanned: ' + data.total_fetched + ' items \u2192 <strong>' + data.total_signals + ' signals</strong></p><hr/>';\nhtml += '<h3>Theme Strength</h3><table style=\"border-collapse:collapse;width:100%;\">';\nhtml += '<tr style=\"background:#f0f0f0;\"><th style=\"padding:8px;text-align:left;border:1px solid #ddd;\">Theme</th><th style=\"padding:8px;border:1px solid #ddd;\">Signal</th><th style=\"padding:8px;border:1px solid #ddd;\">Strength</th><th style=\"padding:8px;border:1px solid #ddd;\">Items</th><th style=\"padding:8px;border:1px solid #ddd;\">Sources</th></tr>';\nfor (const theme of themes) {\n  html += '<tr><td style=\"padding:8px;border:1px solid #ddd;\">' + theme.category + '</td>';\n  html += '<td style=\"padding:8px;border:1px solid #ddd;\">' + theme.signal_level + '</td>';\n  html += '<td style=\"padding:8px;border:1px solid #ddd;\">' + theme.theme_strength + '</td>';\n  html += '<td style=\"padding:8px;border:1px solid #ddd;\">' + theme.item_count + '</td>';\n  html += '<td style=\"padding:8px;border:1px solid #ddd;\">' + theme.sources.join(', ') + '</td></tr>';\n}\nhtml += '</table>';\nhtml += '<h3>Top Signals</h3><ol>';\nfor (let i = 0; i < Math.min(topItems.length, 10); i++) {\n  const item = topItems[i];\n  html += '<li><strong>' + item.title + '</strong><br/>Score: ' + item.score + ' | ' + item.source + ' | ' + item.category;\n  if (item.url) html += ' | <a href=\"' + item.url + '\">Link</a>';\n  html += '</li>';\n}\nhtml += '</ol>';\n\nreturn [{\n  json: {\n    report: report,\n    report_html: html,\n    date: today,\n    themes: themes,\n    top_items: topItems,\n    strong_theme_count: strongThemes.length,\n    total_signals: data.total_signals\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "9ca1e3b4-a677-44f0-9f04-7fcf6d480dad",
      "name": "Flatten Top Signals for Sheet",
      "type": "n8n-nodes-base.code",
      "position": [
        2304,
        336
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst items = data.top_items || [];\n\nif (items.length === 0) {\n  return [{ json: { date: data.date, title: 'No signals detected', source: '-', score: 0, category: '-', url: '-' } }];\n}\n\nreturn items.map(item => ({\n  json: {\n    date: data.date,\n    title: item.title || '',\n    source: item.source || '',\n    score: item.score || 0,\n    category: item.category || '',\n    url: item.url || ''\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "ea3b55cb-bf55-4fea-a5db-4b5cc8f4b2d8",
      "name": "Log Signals to Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2528,
        336
      ],
      "parameters": {
        "columns": {
          "value": {
            "date": "={{ $json.date }}",
            "category": "={{ $json.category }}"
          },
          "schema": [
            {
              "id": "date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "category",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "signal_level",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "signal_level",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "theme_strength",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "theme_strength",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "item_count",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "item_count",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sources",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "sources",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "top_keywords",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "top_keywords",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_SIGNALS_SHEET_GID",
          "cachedResultUrl": "YOUR_SIGNALS_SHEET_URL",
          "cachedResultName": "signal"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultUrl": "YOUR_GOOGLE_SHEET_URL",
          "cachedResultName": "Your Signal Tracking Sheet"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "a13b13c1-42c3-4f3e-97a5-c61611d51f4e",
      "name": "Post Report to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        2304,
        528
      ],
      "parameters": {
        "text": "={{ $json.report }}",
        "otherOptions": {}
      },
      "typeVersion": 2.2
    },
    {
      "id": "487352c5-339f-4210-8bb6-8aee9fef77e8",
      "name": "Prepare Themes for Sheet",
      "type": "n8n-nodes-base.code",
      "position": [
        2304,
        144
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst themes = data.themes || [];\n\nif (themes.length === 0) {\n  return [{ json: { date: data.date, category: 'No themes', signal_level: '-', theme_strength: 0, item_count: 0, sources: '-', top_keywords: '-' } }];\n}\n\nreturn themes.map(theme => ({\n  json: {\n    date: data.date,\n    category: theme.category,\n    signal_level: theme.signal_level,\n    theme_strength: theme.theme_strength,\n    item_count: theme.item_count,\n    sources: theme.sources.join(', '),\n    top_keywords: theme.top_keywords.join(', ')\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "0c0c570b-35c1-4025-b039-02fbc3bc68e5",
      "name": "Log Themes to Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2528,
        144
      ],
      "parameters": {
        "columns": {
          "value": {
            "date": "={{ $json.date }}",
            "sources": "={{ $json.sources }}",
            "category": "={{ $json.category }}",
            "item_count": "={{ $json.item_count }}",
            "signal_level": "={{ $json.signal_level }}",
            "top_keywords": "={{ $json.top_keywords }}",
            "theme_strength": "={{ $json.theme_strength }}"
          },
          "schema": [
            {
              "id": "date",
              "type": "string",
              "required": false,
              "displayName": "date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "category",
              "type": "string",
              "required": false,
              "displayName": "category",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "signal_level",
              "type": "string",
              "required": false,
              "displayName": "signal_level",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "theme_strength",
              "type": "string",
              "required": false,
              "displayName": "theme_strength",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "item_count",
              "type": "string",
              "required": false,
              "displayName": "item_count",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sources",
              "type": "string",
              "required": false,
              "displayName": "sources",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "top_keywords",
              "type": "string",
              "required": false,
              "displayName": "top_keywords",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_THEMES_SHEET_GID",
          "cachedResultUrl": "YOUR_THEMES_SHEET_URL",
          "cachedResultName": "themes"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultUrl": "YOUR_GOOGLE_SHEET_URL",
          "cachedResultName": "Your Signal Tracking Sheet"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "677fd5a7-a775-4c41-8250-70795d778306",
      "name": "Email Daily Report",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2304,
        720
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "={{ $json.report_html }}",
        "options": {},
        "subject": "=Signal Report \u2014 {{ $json.date }} | {{ $json.total_signals }} signals, {{ $json.strong_theme_count }} strong themes"
      },
      "typeVersion": 2.2
    },
    {
      "id": "6783701c-801b-4908-b97c-04ae73afa864",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -288,
        -496
      ],
      "parameters": {
        "color": "#80883F",
        "width": 680,
        "height": 960,
        "content": "## Aggregate trend signals to Google Sheets and Slack\n\nThis workflow scans multiple RSS feeds daily, scores each item against configurable keyword groups, clusters them into themes, and delivers a prioritized intelligence report.\n\n## How it works\n1. **Daily trigger** fetches RSS feeds from Hacker News (50+ points) and Product Hunt in parallel\n2. **Merge node** waits for all feeds to complete before proceeding\n3. **RSS parser** extracts and normalizes titles, descriptions, URLs, and dates from all feeds\n4. **Keyword scorer** matches each item against 7 configurable keyword groups (AI, No-Code, SaaS, Dev Tools, Marketing, Pricing, Productivity) with weighted scoring\n5. **Theme aggregator** clusters scored items into themes, calculates theme strength using source diversity and volume bonuses, and classifies as VERY_STRONG / STRONG / MODERATE / WEAK\n6. **Report builder** generates a complete intelligence report (plain text for Slack, HTML for email)\n7. **Outputs** \u2014 sends report to Slack + email, logs individual signals and themes to separate Google Sheet tabs\n\n## Setup steps\n1. Connect **Google Sheets OAuth2** credentials and update the Sheet ID in both \"Log Signals to Sheet\" and \"Log Themes to Sheet\" nodes\n2. Create a Google Sheet with two tabs:\n   - `signal` \u2014 headers: `date`, `title`, `source`, `score`, `category`, `url`\n   - `themes` \u2014 headers: `date`, `category`, `signal_level`, `theme_strength`, `item_count`, `sources`, `top_keywords`\n3. Connect **Slack OAuth2** credentials and configure the target channel\n4. Connect **Gmail OAuth2** credentials and update the recipient email address\n5. Activate the workflow\n\n## Customization\n- **Add more RSS feeds** \u2014 duplicate a feed node, connect it to the Merge node, and add parsing logic in the \"Parse All RSS Feeds\" node\n- **Edit keyword groups** \u2014 modify the `keywordGroups` object in the \"Score and Classify Signals\" node to match your industry\n- **Adjust source weights** \u2014 change the weight multipliers (Hacker News: 1.5x, Product Hunt: 1.3x) in the parser\n- **Theme thresholds** \u2014 modify the strength cutoffs (30/15/8) in the aggregator node\n- **Schedule** \u2014 change from daily to hourly or weekly in the trigger node"
      },
      "typeVersion": 1
    },
    {
      "id": "9b9a6837-a077-4c0c-ad82-ae2cecec182c",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        880,
        128
      ],
      "parameters": {
        "color": "#564343",
        "width": 284,
        "height": 164,
        "content": "## 1. Fetch RSS Feeds\nTrigger fires daily. Hacker News and Product Hunt feeds are fetched in parallel. Merge node waits for both to complete before passing data forward."
      },
      "typeVersion": 1
    },
    {
      "id": "7f4bab62-2c1e-4b09-b5d0-61de8a8b01f3",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1408,
        192
      ],
      "parameters": {
        "color": "#564343",
        "width": 292,
        "height": 164,
        "content": "## 2. Parse and Score\nRSS XML is parsed into normalized items. Each item is scored against 7 keyword groups with configurable weights. Title matches get a bonus multiplier."
      },
      "typeVersion": 1
    },
    {
      "id": "24c55502-0595-4c20-84a2-7aa9189786be",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1888,
        144
      ],
      "parameters": {
        "color": "#564343",
        "width": 324,
        "height": 196,
        "content": "## 3. Aggregate Themes and Build Report\nSignals are clustered into themes with strength scores (using source diversity and volume bonuses). A full intelligence report is generated in both plain text and HTML."
      },
      "typeVersion": 1
    },
    {
      "id": "9805b4b7-5bf6-41f3-a5e0-0023ba8b5025",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2352,
        -80
      ],
      "parameters": {
        "color": "#564343",
        "width": 308,
        "height": 164,
        "content": "## 4. Deliver and Log\nReport is posted to Slack and emailed. Individual signals and theme summaries are logged to separate Google Sheet tabs for historical analysis."
      },
      "typeVersion": 1
    },
    {
      "id": "efedef54-8924-49a0-a487-42594a15fd1e",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2480,
        832
      ],
      "parameters": {
        "color": 3,
        "width": 340,
        "height": 80,
        "content": "\u26a0\ufe0f **Update the email address** in this node to your own recipient before activating."
      },
      "typeVersion": 1
    },
    {
      "id": "5fe3987d-3e12-4165-8e68-6bbc7155d71c",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        496,
        656
      ],
      "parameters": {
        "color": "#564343",
        "width": 388,
        "height": 332,
        "content": "## Keyword Group Reference\nEdit these in the \"Score and Classify Signals\" node:\n\n| Group | Weight |\n|---|---|\n| AI & Automation | 2.0 |\n| No-Code / Low-Code | 1.8 |\n| Pricing & Monetization | 1.6 |\n| SaaS & Business | 1.5 |\n| Marketing & Growth | 1.4 |\n| Developer Tools | 1.3 |\n| Remote & Productivity | 1.1 |\n\n**Source Weights:**\n- Hacker News: 1.5x\n- Product Hunt: 1.3x"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "81e0d68c-faac-4d6f-b955-e0a06184a893",
  "connections": {
    "Wait for All Feeds": {
      "main": [
        [
          {
            "node": "Parse All RSS Feeds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse All RSS Feeds": {
      "main": [
        [
          {
            "node": "Score and Classify Signals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Hacker News RSS": {
      "main": [
        [
          {
            "node": "Wait for All Feeds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily Schedule Trigger": {
      "main": [
        [
          {
            "node": "Fetch Hacker News RSS",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Product Hunt RSS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Product Hunt RSS": {
      "main": [
        [
          {
            "node": "Wait for All Feeds",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Prepare Themes for Sheet": {
      "main": [
        [
          {
            "node": "Log Themes to Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Intelligence Report": {
      "main": [
        [
          {
            "node": "Flatten Top Signals for Sheet",
            "type": "main",
            "index": 0
          },
          {
            "node": "Prepare Themes for Sheet",
            "type": "main",
            "index": 0
          },
          {
            "node": "Email Daily Report",
            "type": "main",
            "index": 0
          },
          {
            "node": "Post Report to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score and Classify Signals": {
      "main": [
        [
          {
            "node": "Aggregate Signals into Themes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Signals into Themes": {
      "main": [
        [
          {
            "node": "Build Intelligence Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flatten Top Signals for Sheet": {
      "main": [
        [
          {
            "node": "Log Signals to Sheet",
            "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

Founders, product managers, content strategists, indie hackers, and anyone who wants to automatically monitor tech industry trends across multiple sources — without manually browsing Hacker News and Product Hunt every day.

Source: https://n8n.io/workflows/13839/ — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

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

Data & Sheets

This n8n workflow automates the end-to-end client onboarding process: capturing client details, validating emails, assigning tiers, generating welcome packs, creating tasks, notifying teams, archiving

Google Sheets, Gmail, Airtable +5
Data & Sheets

Daily Business Report Generator. Uses googleSheets, httpRequest, slack, gmail. Scheduled trigger; 17 nodes.

Google Sheets, HTTP Request, Slack +3
Data & Sheets

Revenue operations teams, SaaS growth managers, and sales directors who need automated weekly insights from their Stripe payment data. Perfect for small to medium businesses tracking subscription reve

HTTP Request, Google Sheets, Google Gemini Chat +4
Data & Sheets

This workflow triggers when a HubSpot deal stage changes to Closed Won and automatically generates an invoice. It collects deal and contact data, builds a styled invoice, converts it into a PDF, and s

HubSpot Trigger, HTTP Request, Google Sheets +4
Data & Sheets

This guide will walk you through setting up your n8n workflow. By the end, you'll have a fully automated system for managing your recruitment pipeline.

Google Calendar Trigger, Slack, HTTP Request +4