AutomationFlowsAI & RAG › Summarize AI News From Rss, Reddit and Hn with Claude to Discord and Slack

Summarize AI News From Rss, Reddit and Hn with Claude to Discord and Slack

BynXsi @dyllank on n8n.io

This n8n template builds an automated daily news digest powered by Claude AI.

Cron / scheduled trigger★★★★★ complexityAI-powered39 nodesPostgresHTTP RequestChain LlmAnthropic ChatAgentSlack
AI & RAG Trigger: Cron / scheduled Nodes: 39 Complexity: ★★★★★ AI nodes: yes Added:
Summarize AI News From Rss, Reddit and Hn with Claude to Discord and Slack — n8n workflow card showing Postgres, HTTP Request, Chain Llm integration

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

This workflow follows the Agent → Chainllm 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": "naUjq0csvOzATYan",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Summarize RSS feeds into a daily AI news digest with Claude and deliver to Discord",
  "tags": [],
  "nodes": [
    {
      "id": "15d9d02a-f5b2-41c3-a02f-718d31cf3fe4",
      "name": "Initialize run timestamp",
      "type": "n8n-nodes-base.code",
      "position": [
        1168,
        784
      ],
      "parameters": {
        "jsCode": "var now = new Date();\nvar hex = function() { return Math.floor(Math.random() * 0x10000).toString(16).padStart(4, '0'); };\nvar runId = hex() + hex() + '-' + hex() + '-' + hex() + '-' + hex() + '-' + hex() + hex() + hex();\nreturn [{ json: { runDate: now.toISOString().split('T')[0], startedAt: now.toISOString(), runId: runId } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "00e58581-a2bb-489d-8064-6c3ff13d6c91",
      "name": "Create run record",
      "type": "n8n-nodes-base.postgres",
      "position": [
        1456,
        784
      ],
      "parameters": {
        "query": "={{ $(\"\u2699\ufe0f Configure digest settings\").first().json.use_postgres ? \"INSERT INTO digest_runs (run_date) VALUES ('\" + $json.runDate + \"') RETURNING id, run_date, started_at\" : \"SELECT '\" + $json.runId + \"' AS id, '\" + $json.runDate + \"' AS run_date, NOW() AS started_at\" }}",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5,
      "alwaysOutputData": true
    },
    {
      "id": "b59ce09a-69aa-4468-afb9-e00b66510ab1",
      "name": "Build feed source list",
      "type": "n8n-nodes-base.code",
      "position": [
        1728,
        784
      ],
      "parameters": {
        "jsCode": "var items = $input.all();\nvar runId = items[0].json.id;\nvar feeds = [\n  { url: 'https://hn.algolia.com/api/v1/search_by_date?tags=story&query=AI+LLM+GPT+Claude&numericFilters=points>20', name: 'Hacker News \u2014 AI', category: 'ai-models', feedType: 'hn_api' },\n  { url: 'https://techcrunch.com/category/artificial-intelligence/feed/', name: 'TechCrunch AI', category: 'ai-tools', feedType: 'rss' },\n  { url: 'https://www.theverge.com/rss/ai-artificial-intelligence/index.xml', name: 'The Verge AI', category: 'ai-research', feedType: 'rss' },\n  { url: 'https://feeds.arstechnica.com/arstechnica/index', name: 'Ars Technica', category: 'tech-general', feedType: 'rss' },\n  { url: 'https://simonwillison.net/atom/everything/', name: 'Simon Willison', category: 'ai-tools', feedType: 'atom' },\n  { url: 'https://www.technologyreview.com/feed/', name: 'MIT Technology Review', category: 'ai-research', feedType: 'rss' },\n  { url: 'https://www.reddit.com/r/LocalLLaMA/.json?limit=25&sort=hot', name: 'r/LocalLLaMA', category: 'ai-models', feedType: 'reddit' },\n  { url: 'https://www.reddit.com/r/selfhosted/.json?limit=25&sort=hot', name: 'r/selfhosted', category: 'open-source', feedType: 'reddit' },\n  { url: 'https://www.anthropic.com/rss.xml', name: 'Anthropic Blog', category: 'ai-models', feedType: 'rss' },\n  { url: 'https://openai.com/blog/rss.xml', name: 'OpenAI Blog', category: 'ai-models', feedType: 'rss' }\n];\nvar result = [];\nfor (var i = 0; i < feeds.length; i++) {\n  feeds[i].runId = runId;\n  result.push({ json: feeds[i] });\n}\nreturn result;"
      },
      "typeVersion": 2
    },
    {
      "id": "b757855c-1658-41dd-b7ea-fc741ca2de47",
      "name": "Fetch feed content",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        2000,
        784
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "AI-News-Digest/1.0"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d19d6bfb-14ff-4906-8b23-74e7e79dd3ff",
      "name": "Parse articles from feeds",
      "type": "n8n-nodes-base.code",
      "position": [
        2288,
        784
      ],
      "parameters": {
        "jsCode": "var httpResults = $input.all();\nvar feedMeta = $('Build feed source list').all();\nvar articles = [];\nvar now = Date.now();\nvar lookbackHours = $('\u2699\ufe0f Configure digest settings').first().json.feed_lookback_hours || 24;\nvar oneDayAgo = now - (lookbackHours * 60 * 60 * 1000);\nvar errors = [];\nvar runId = feedMeta[0].json.runId;\n\nfor (var i = 0; i < httpResults.length; i++) {\n  var response = httpResults[i].json;\n  var feed = feedMeta[i].json;\n  var feedName = feed.name;\n  var feedCategory = feed.category;\n  var feedType = feed.feedType;\n\n  try {\n    var body = '';\n    if (typeof response === 'string') {\n      body = response;\n    } else if (response.data && typeof response.data === 'string') {\n      body = response.data;\n    } else {\n      body = JSON.stringify(response);\n    }\n\n    if (feedType === 'hn_api') {\n      var hnData = typeof response === 'object' && response.hits ? response : JSON.parse(body);\n      var hits = hnData.hits || [];\n      for (var h = 0; h < hits.length; h++) {\n        var hit = hits[h];\n        var hitDate = new Date(hit.created_at).getTime();\n        if (hitDate < oneDayAgo) continue;\n        if (!hit.url) continue;\n        articles.push({ json: {\n          title: hit.title || '', url: hit.url, source: feedName,\n          category: feedCategory, publishedAt: hit.created_at,\n          socialScore: (hit.points || 0) + (hit.num_comments || 0) * 2,\n          excerpt: '', runId: runId\n        }});\n      }\n    } else if (feedType === 'reddit') {\n      var rd = typeof response === 'object' && response.data ? response : JSON.parse(body);\n      var children = (rd.data && rd.data.children) || [];\n      for (var r = 0; r < children.length; r++) {\n        var post = children[r].data;\n        if (!post) continue;\n        var postDate = (post.created_utc || 0) * 1000;\n        if (postDate < oneDayAgo) continue;\n        var postUrl = post.url_overridden_by_dest || post.url || '';\n        if (postUrl.indexOf('reddit.com') !== -1) postUrl = 'https://www.reddit.com' + post.permalink;\n        articles.push({ json: {\n          title: post.title || '', url: postUrl, source: feedName,\n          category: feedCategory,\n          publishedAt: new Date(postDate).toISOString(),\n          socialScore: (post.score || 0) + (post.num_comments || 0) * 2,\n          excerpt: (post.selftext || '').substring(0, 300), runId: runId\n        }});\n      }\n    } else if (feedType === 'atom') {\n      var entryRegex = /<entry[\\s\\S]*?<\\/entry>/gi;\n      var entries = body.match(entryRegex) || [];\n      for (var a = 0; a < entries.length; a++) {\n        var entry = entries[a];\n        var tM = entry.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i);\n        var lM = entry.match(/<link[^>]*href=[\"']([^\"']+)[\"'][^>]*>/i);\n        var pM = entry.match(/<published[^>]*>([\\s\\S]*?)<\\/published>/i) || entry.match(/<updated[^>]*>([\\s\\S]*?)<\\/updated>/i);\n        var sM = entry.match(/<summary[^>]*>([\\s\\S]*?)<\\/summary>/i) || entry.match(/<content[^>]*>([\\s\\S]*?)<\\/content>/i);\n        var pubDate = pM ? new Date(pM[1].trim()).getTime() : 0;\n        if (pubDate && pubDate < oneDayAgo) continue;\n        var eTitle = tM ? tM[1].replace(/<[^>]+>/g, '').trim() : '';\n        var eUrl = lM ? lM[1] : '';\n        if (!eUrl || !eTitle) continue;\n        articles.push({ json: {\n          title: eTitle, url: eUrl, source: feedName,\n          category: feedCategory,\n          publishedAt: pM ? pM[1].trim() : '',\n          socialScore: 0,\n          excerpt: sM ? sM[1].replace(/<[^>]+>/g, '').trim().substring(0, 300) : '',\n          runId: runId\n        }});\n      }\n    } else {\n      var itemRegex = /<item[\\s\\S]*?<\\/item>/gi;\n      var rssItems = body.match(itemRegex) || [];\n      for (var j = 0; j < rssItems.length; j++) {\n        var item = rssItems[j];\n        var tR = item.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i);\n        var lR = item.match(/<link[^>]*>([\\s\\S]*?)<\\/link>/i);\n        var pR = item.match(/<pubDate[^>]*>([\\s\\S]*?)<\\/pubDate>/i);\n        var dR = item.match(/<description[^>]*>([\\s\\S]*?)<\\/description>/i);\n        var rssDate = pR ? new Date(pR[1].trim()).getTime() : 0;\n        if (rssDate && rssDate < oneDayAgo) continue;\n        var rT = tR ? tR[1].replace(/<!\\[CDATA\\[|\\]\\]>/g, '').replace(/<[^>]+>/g, '').trim() : '';\n        var rL = lR ? lR[1].replace(/<!\\[CDATA\\[|\\]\\]>/g, '').trim() : '';\n        if (!rL || !rT) continue;\n        articles.push({ json: {\n          title: rT, url: rL, source: feedName,\n          category: feedCategory,\n          publishedAt: pR ? pR[1].trim() : '',\n          socialScore: 0,\n          excerpt: dR ? dR[1].replace(/<!\\[CDATA\\[|\\]\\]>/g, '').replace(/<[^>]+>/g, '').trim().substring(0, 300) : '',\n          runId: runId\n        }});\n      }\n    }\n  } catch (e) {\n    errors.push({ feed: feedName, error: e.message });\n  }\n}\n\narticles.sort(function(a, b) { return (b.json.socialScore || 0) - (a.json.socialScore || 0); });\n\nif (articles.length === 0) {\n  return [{ json: { url: '', _noArticles: true, articleCount: 0, runId: runId, errors: errors } }];\n}\nif (errors.length > 0) articles[0].json._feedErrors = errors;\nreturn articles;"
      },
      "typeVersion": 2
    },
    {
      "id": "2a4aeb63-b347-49cd-a976-76ddee8fd72b",
      "name": "Any articles found?",
      "type": "n8n-nodes-base.if",
      "position": [
        2560,
        784
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "afb369bf",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              },
              "leftValue": "={{ $json.url }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a33cf456-8543-4018-97a4-68a1fa8a71c8",
      "name": "Mark run as empty",
      "type": "n8n-nodes-base.postgres",
      "position": [
        2848,
        576
      ],
      "parameters": {
        "query": "={{ $(\"\u2699\ufe0f Configure digest settings\").first().json.use_postgres ? \"UPDATE digest_runs SET status = 'no_articles', completed_at = NOW(), duration_seconds = EXTRACT(EPOCH FROM (NOW() - started_at))::int WHERE id = '\" + $('Create run record').first().json.id + \"'\" : \"SELECT 1\" }}",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5,
      "alwaysOutputData": true
    },
    {
      "id": "a4996912-f948-4cb7-8fe6-59ee3f33671f",
      "name": "Deduplicate by URL and title",
      "type": "n8n-nodes-base.code",
      "position": [
        2848,
        784
      ],
      "parameters": {
        "jsCode": "function djb2(str) {\n  var hash = 5381;\n  for (var i = 0; i < str.length; i++) {\n    hash = ((hash << 5) + hash) + str.charCodeAt(i);\n    hash = hash & hash;\n  }\n  return Math.abs(hash).toString(16).padStart(8, '0');\n}\n\nfunction levenshtein(a, b) {\n  if (a.length === 0) return b.length;\n  if (b.length === 0) return a.length;\n  var matrix = [];\n  for (var i = 0; i <= b.length; i++) matrix[i] = [i];\n  for (var j = 0; j <= a.length; j++) matrix[0][j] = j;\n  for (var i = 1; i <= b.length; i++) {\n    for (var j = 1; j <= a.length; j++) {\n      if (b.charAt(i - 1) === a.charAt(j - 1)) {\n        matrix[i][j] = matrix[i - 1][j - 1];\n      } else {\n        matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);\n      }\n    }\n  }\n  return matrix[b.length][a.length];\n}\n\nfunction normalizeTitle(title) {\n  return title.toLowerCase().replace(/[^a-z0-9\\s]/g, '').replace(/\\s+/g, ' ').trim();\n}\n\nvar allItems = $input.all();\nvar seen = {};\nvar seenTitles = [];\nvar unique = [];\nvar runId = allItems[0].json.runId;\n\nfor (var i = 0; i < allItems.length; i++) {\n  var article = allItems[i].json;\n  if (!article.url) continue;\n  var urlHash = djb2(article.url);\n  var normTitle = normalizeTitle(article.title || '');\n\n  if (seen[urlHash]) continue;\n\n  var isDupe = false;\n  for (var j = 0; j < seenTitles.length; j++) {\n    var maxLen = Math.max(normTitle.length, seenTitles[j].length);\n    if (maxLen === 0) continue;\n    var dist = levenshtein(normTitle, seenTitles[j]);\n    if (dist / maxLen < 0.2) { isDupe = true; break; }\n  }\n  if (isDupe) continue;\n\n  seen[urlHash] = true;\n  seenTitles.push(normTitle);\n  article.urlHash = urlHash;\n  article.titleNormalized = normTitle;\n  unique.push(article);\n}\n\nvar hashListSql = unique.map(function(a) { return \"'\" + a.urlHash + \"'\"; }).join(',');\nif (!hashListSql) hashListSql = \"''\";\n\nreturn [{ json: { articles: unique, hashListSql: hashListSql, count: unique.length, runId: runId } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "6c6c84cf-6795-4427-bca7-a6b96a72e77c",
      "name": "Check database for recent duplicates",
      "type": "n8n-nodes-base.postgres",
      "position": [
        3120,
        784
      ],
      "parameters": {
        "query": "={{ $(\"\u2699\ufe0f Configure digest settings\").first().json.use_postgres ? \"SELECT url_hash FROM digest_articles WHERE url_hash IN (\" + $json.hashListSql + \") AND created_at > NOW() - INTERVAL '\" + ($(\"\u2699\ufe0f Configure digest settings\").first().json.dedup_lookback_days || 7) + \" days'\" : \"SELECT '' AS url_hash WHERE false\" }}",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5,
      "alwaysOutputData": true
    },
    {
      "id": "d18869bc-2d20-4236-9dc9-ac0a3d288f07",
      "name": "Remove already-seen articles",
      "type": "n8n-nodes-base.code",
      "position": [
        3408,
        784
      ],
      "parameters": {
        "jsCode": "var pgResults = $input.all();\nvar knownHashes = {};\nfor (var i = 0; i < pgResults.length; i++) {\n  var hash = pgResults[i].json.url_hash;\n  if (hash) knownHashes[hash] = true;\n}\nvar dedupData = $('Deduplicate by URL and title').first().json;\nvar articles = dedupData.articles;\nvar fresh = [];\nfor (var i = 0; i < articles.length; i++) {\n  if (!knownHashes[articles[i].urlHash]) fresh.push(articles[i]);\n}\nreturn [{ json: { articles: fresh, count: fresh.length, runId: dedupData.runId } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "eac5f273-fd2f-4695-aa5e-6c226d538fc7",
      "name": "Select top articles for extraction",
      "type": "n8n-nodes-base.code",
      "position": [
        3680,
        784
      ],
      "parameters": {
        "jsCode": "var data = $input.first().json;\nvar articles = data.articles;\narticles.sort(function(a, b) { return (b.socialScore || 0) - (a.socialScore || 0); });\nvar maxArticles = $('\u2699\ufe0f Configure digest settings').first().json.max_extract_articles || 25;\nvar selected = articles.slice(0, maxArticles);\nvar result = [];\nfor (var i = 0; i < selected.length; i++) {\n  result.push({ json: selected[i] });\n}\nif (result.length === 0) result.push({ json: { _empty: true, runId: data.runId } });\nreturn result;"
      },
      "typeVersion": 2
    },
    {
      "id": "d01169be-bcf1-41dd-9b3a-cb1708505e31",
      "name": "Extract full text via Jina",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        3968,
        784
      ],
      "parameters": {
        "url": "=https://r.jina.ai/{{ $json.url }}",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/json"
            },
            {
              "name": "X-Return-Format",
              "value": "markdown"
            }
          ]
        }
      },
      "retryOnFail": false,
      "typeVersion": 4.2
    },
    {
      "id": "ef32355d-2617-4692-9506-3e7fce611019",
      "name": "Assemble extracted content",
      "type": "n8n-nodes-base.code",
      "position": [
        4240,
        784
      ],
      "parameters": {
        "jsCode": "var jinaResults = $input.all();\nvar articleItems = $('Select top articles for extraction').all();\nvar totalJinaTokens = 0;\nvar articles = [];\n\nfor (var i = 0; i < articleItems.length; i++) {\n  var article = articleItems[i].json;\n  if (article._empty) continue;\n  var jina = jinaResults[i] ? jinaResults[i].json : {};\n  var fullText = '';\n  var hasFullText = false;\n\n  if (jina.data && jina.data.content) {\n    fullText = jina.data.content.substring(0, 8000);\n    hasFullText = true;\n    if (jina.data.usage) totalJinaTokens += jina.data.usage.tokens || 0;\n  } else if (jina.content) {\n    fullText = String(jina.content).substring(0, 8000);\n    hasFullText = true;\n  } else {\n    fullText = article.excerpt || '';\n  }\n\n  articles.push({\n    title: article.title, url: article.url, source: article.source,\n    category: article.category, publishedAt: article.publishedAt,\n    socialScore: article.socialScore, urlHash: article.urlHash,\n    titleNormalized: article.titleNormalized,\n    fullText: fullText, hasFullText: hasFullText, runId: article.runId\n  });\n}\n\nreturn [{ json: {\n  articles: articles, jinaTokens: totalJinaTokens,\n  count: articles.length, runId: articles.length > 0 ? articles[0].runId : ''\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f824391b-0d86-4cfc-8b99-cdb1b934a238",
      "name": "Prepare analysis prompts",
      "type": "n8n-nodes-base.code",
      "position": [
        4528,
        784
      ],
      "parameters": {
        "jsCode": "var data = $input.first().json;\nvar articles = data.articles;\nvar jinaTokens = data.jinaTokens;\nvar runId = data.runId;\n\nvar result = [];\nfor (var i = 0; i < articles.length; i++) {\n  var a = articles[i];\n  var prompt = 'Analyze this article and return ONLY valid JSON:\\n\\nTITLE: ' + a.title + '\\nSOURCE: ' + a.source + '\\nCATEGORY: ' + a.category + '\\nURL: ' + a.url + '\\n\\nFULL TEXT:\\n' + (a.fullText || '(no text available)').substring(0, 8000) + '\\n\\nReturn this exact JSON structure:\\n{\"summary\":\"2-3 sentence summary\",\"importance\":<integer 1-10>,\"categories\":[\"primary\",\"secondary\"],\"sentiment\":\"positive|negative|neutral|mixed\",\"key_entities\":[\"entity1\",\"entity2\"],\"why_it_matters\":\"One sentence on practical impact\",\"reading_time_min\":<integer>}';\n  result.push({ json: {\n    prompt: prompt, _articleIndex: i, _article: a,\n    _jinaTokens: jinaTokens, _runId: runId, _totalArticles: articles.length\n  }});\n}\nif (result.length === 0) result.push({ json: { prompt: 'Return {\"summary\":\"no articles\",\"importance\":1,\"categories\":[],\"sentiment\":\"neutral\",\"key_entities\":[],\"why_it_matters\":\"none\",\"reading_time_min\":0}', _article: {}, _jinaTokens: 0, _runId: runId, _totalArticles: 0 } });\nreturn result;"
      },
      "typeVersion": 2
    },
    {
      "id": "d6e50e51-2678-4eb1-a4ec-6147c93569e5",
      "name": "Analyze article with Claude",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "onError": "continueRegularOutput",
      "maxTries": 2,
      "position": [
        4800,
        784
      ],
      "parameters": {
        "text": "={{ $json.prompt }}",
        "batching": {},
        "messages": {
          "messageValues": [
            {
              "message": "You are an expert news analyst. You analyze technology articles and output structured JSON assessments.\n\nSCORING GUIDE (importance 1-10):\n10: Industry-reshaping announcement (new major model, regulation, acquisition >$1B)\n8-9: Significant development with broad impact (major product launch, critical vulnerability)\n6-7: Notable development in a specific domain (new tool, interesting research, meaningful open source release)\n4-5: Incremental update or niche development (version bump, minor feature, limited audience)\n1-3: Routine news, announcements, or opinion pieces with no actionable insight\n\nCATEGORIES (use these exact strings):\nai-models, ai-tools, ai-research, ai-agents, security, devops, open-source, cloud, hardware, startups, regulation, programming\n\nRULES:\n- Return ONLY valid JSON. No markdown formatting, no backticks, no explanation.\n- If the article text is missing, score importance lower (max 5) and note \"limited analysis\" in summary.\n- Be ruthlessly honest about importance. Most articles are 4-6. Reserve 8+ for genuinely significant events.\n- The \"why_it_matters\" should be actionable: \"means X for developers\" not \"this is interesting.\""
            }
          ]
        },
        "promptType": "define"
      },
      "retryOnFail": true,
      "typeVersion": 1.7,
      "waitBetweenTries": 3000
    },
    {
      "id": "8da6dc3e-8d64-4596-b0a9-199b11a6ccb1",
      "name": "Score and rank articles",
      "type": "n8n-nodes-base.code",
      "position": [
        5088,
        784
      ],
      "parameters": {
        "jsCode": "var aiOutputs = $input.all();\nvar batchItems = $('Prepare analysis prompts').all();\nvar topicWeights = {\n  'ai-models': 1.5, 'ai-agents': 1.5, 'ai-tools': 1.3,\n  'ai-research': 1.2, 'security': 1.3, 'open-source': 1.2, 'devops': 1.1\n};\nvar articles = [];\nvar totalInputTokens = 0;\nvar totalOutputTokens = 0;\n\nfor (var i = 0; i < aiOutputs.length; i++) {\n  var aiOut = aiOutputs[i].json;\n  var batch = batchItems[i].json;\n  var article = batch._article;\n\n  if (aiOut.tokenUsageEstimate) {\n    totalInputTokens += aiOut.tokenUsageEstimate.promptTokens || 0;\n    totalOutputTokens += aiOut.tokenUsageEstimate.completionTokens || 0;\n  }\n\n  // Basic LLM Chain: {text: \"...\"}, AI Agent: {output: \"...\"}\n  var responseText = aiOut.text || aiOut.output || '';\n\n  var analysis = null;\n  try {\n    var cleaned = responseText.replace(/```json\\n?/g, '').replace(/```\\n?/g, '').trim();\n    var js = cleaned.indexOf('{');\n    var je = cleaned.lastIndexOf('}');\n    if (js !== -1 && je !== -1) analysis = JSON.parse(cleaned.substring(js, je + 1));\n  } catch (e) {\n    try {\n      var partial = cleaned.substring(cleaned.indexOf('{'));\n      var opens = (partial.match(/\\{/g) || []).length;\n      var closes = (partial.match(/\\}/g) || []).length;\n      while (closes < opens) { partial += '}'; closes++; }\n      analysis = JSON.parse(partial);\n    } catch (e2) {\n      analysis = { summary: 'Analysis failed', importance: 3, categories: [article.category || 'tech-general'], sentiment: 'neutral', key_entities: [], why_it_matters: 'Unable to analyze', reading_time_min: 3 };\n    }\n  }\n\n  if (!analysis) {\n    analysis = { summary: 'Analysis unavailable', importance: 3, categories: [article.category || 'tech-general'], sentiment: 'neutral', key_entities: [], why_it_matters: 'Unable to analyze', reading_time_min: 3 };\n  }\n  var baseScore = analysis.importance || 5;\n  var maxWeight = 1.0;\n  var cats = analysis.categories || [article.category || 'tech-general'];\n  for (var c = 0; c < cats.length; c++) {\n    var w = topicWeights[cats[c]] || 1.0;\n    if (w > maxWeight) maxWeight = w;\n  }\n  var weighted = baseScore * maxWeight;\n  var socialBoost = Math.min((article.socialScore || 0) / 200, 2);\n  weighted += socialBoost;\n\n  articles.push({\n    title: article.title, url: article.url, source: article.source,\n    category: article.category, publishedAt: article.publishedAt,\n    socialScore: article.socialScore, urlHash: article.urlHash,\n    titleNormalized: article.titleNormalized,\n    fullText: article.fullText, hasFullText: article.hasFullText,\n    summary: analysis.summary, importance: analysis.importance,\n    categories: cats, sentiment: analysis.sentiment,\n    keyEntities: analysis.key_entities || [],\n    whyItMatters: analysis.why_it_matters,\n    readingTimeMin: analysis.reading_time_min || 3,\n    weightedScore: Math.round(weighted * 100) / 100\n  });\n}\n\narticles.sort(function(a, b) { return b.weightedScore - a.weightedScore; });\n\n// Fallback: estimate tokens when tokenUsageEstimate unavailable (Basic LLM Chain)\nif (totalInputTokens === 0) {\n  for (var j = 0; j < aiOutputs.length; j++) {\n    var artJ = batchItems[j].json._article;\n    totalInputTokens += Math.ceil(((artJ.fullText || artJ.summary || '').length + 1800) / 4);\n    totalOutputTokens += Math.ceil((aiOutputs[j].json.text || aiOutputs[j].json.output || '').length / 4);\n  }\n}\n\nreturn [{ json: {\n  articles: articles,\n  analysisInputTokens: totalInputTokens,\n  analysisOutputTokens: totalOutputTokens,\n  jinaTokens: batchItems[0].json._jinaTokens,\n  runId: batchItems[0].json._runId,\n  count: articles.length\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ab298c48-0aa1-494a-a884-67a2c33a4f07",
      "name": "Select articles for digest",
      "type": "n8n-nodes-base.code",
      "position": [
        5360,
        784
      ],
      "parameters": {
        "jsCode": "var data = $input.first().json;\nvar articles = data.articles;\nvar maxDigest = $('\u2699\ufe0f Configure digest settings').first().json.max_digest_articles || 12;\nvar selected = articles.slice(0, maxDigest);\nvar lead = selected.length > 0 ? selected[0] : null;\nvar topStories = selected.slice(1, 5);\nvar quickHits = selected.slice(5);\n\nvar compilationData = '';\nif (lead) {\n  compilationData += 'LEAD STORY:\\nTitle: ' + lead.title + '\\nSource: ' + lead.source + '\\nImportance: ' + lead.importance + '/10\\nSummary: ' + lead.summary + '\\nWhy it matters: ' + lead.whyItMatters + '\\n\\n';\n}\ncompilationData += 'TOP STORIES:\\n';\nfor (var i = 0; i < topStories.length; i++) {\n  var s = topStories[i];\n  compilationData += (i + 1) + '. ' + s.title + ' [' + s.source + ', ' + s.importance + '/10]\\n   ' + s.summary + '\\n   Why: ' + s.whyItMatters + '\\n\\n';\n}\ncompilationData += 'QUICK HITS:\\n';\nfor (var i = 0; i < quickHits.length; i++) {\n  var q = quickHits[i];\n  compilationData += '- ' + q.title + ' [' + q.source + ', ' + q.importance + '/10]: ' + q.summary + '\\n';\n}\n\nvar userPrompt = 'Compile today\\'s AI & tech digest from these analyzed articles. Output ONLY valid JSON.\\n\\n' + compilationData + '\\n\\nReturn this exact JSON structure:\\n{\"subject_line\":\"Email subject (under 60 chars)\",\"lead_analysis\":\"2-3 paragraphs of genuine insight about the lead story\",\"lead_why\":\"Single actionable sentence\",\"top_stories\":[{\"title\":\"...\",\"summary\":\"Exactly 2 sentences\",\"why\":\"Actionable insight\"}],\"quick_hits\":[{\"title\":\"...\",\"one_liner\":\"Key fact, one line\"}],\"trend_note\":\"Trend across stories or empty string\"}';\n\nreturn [{ json: {\n  prompt: userPrompt,\n  _articles: selected, _lead: lead,\n  _topStories: topStories, _quickHits: quickHits,\n  _analysisInputTokens: data.analysisInputTokens,\n  _analysisOutputTokens: data.analysisOutputTokens,\n  _jinaTokens: data.jinaTokens, _runId: data.runId,\n  _allArticles: articles, _articlesFound: articles.length\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "443ea580-1b92-49d9-a27d-136e1f3e914d",
      "name": "Claude Sonnet (compiler)",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        5344,
        1040
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-sonnet-4-5-20250929"
        },
        "options": {
          "temperature": 0.4,
          "maxTokensToSample": 8192
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "a5f54991-eb5c-4ecf-9782-a6e5612990ee",
      "name": "Compile digest with Claude",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "maxTries": 3,
      "position": [
        5648,
        784
      ],
      "parameters": {
        "text": "={{ $json.prompt }}",
        "agent": "conversationalAgent",
        "options": {
          "systemMessage": "You compile daily AI & tech news digests. Your output is the final digest that gets sent to Discord, Slack, and email.\n\nVOICE: Professional but approachable. Write like a knowledgeable colleague summarizing what they read today, not a news anchor. Be direct and opinionated about why things matter.\n\nRULES:\n- Lead analysis should have genuine insight, not just restate the headline\n- \"Why it matters\" must be actionable: what should the reader know or do differently\n- Top story summaries: exactly 2 sentences each, no fluff\n- Quick hits: maximum one line each, start with the key fact\n- If you spot a trend across stories, mention it in trend_note\n- Return ONLY valid JSON"
        },
        "promptType": "define"
      },
      "retryOnFail": true,
      "typeVersion": 1.7,
      "waitBetweenTries": 5000
    },
    {
      "id": "33120460-8e96-404c-a3ec-89c7d1e8143c",
      "name": "Format multi-channel outputs",
      "type": "n8n-nodes-base.code",
      "position": [
        5920,
        784
      ],
      "parameters": {
        "jsCode": "var compileOut = $input.first().json;\nvar selData = $('Select articles for digest').first().json;\n\nvar responseText = compileOut.output || compileOut.text || '';\nvar digest = null;\ntry {\n  var cleaned = responseText.replace(/```json\\n?/g, '').replace(/```\\n?/g, '').trim();\n  var js = cleaned.indexOf('{');\n  var je = cleaned.lastIndexOf('}');\n  if (js !== -1 && je !== -1) digest = JSON.parse(cleaned.substring(js, je + 1));\n} catch (e) {\n  digest = { subject_line: 'AI News Digest', lead_analysis: responseText.substring(0, 500), lead_why: '', top_stories: [], quick_hits: [], trend_note: '' };\n}\nif (!digest) digest = {};\ndigest.subject_line = digest.subject_line || 'AI News Digest';\ndigest.lead_analysis = digest.lead_analysis || '';\ndigest.lead_why = digest.lead_why || '';\ndigest.top_stories = digest.top_stories || [];\ndigest.quick_hits = digest.quick_hits || [];\ndigest.trend_note = digest.trend_note || '';\n\nvar compIn = 0, compOut = 0;\nif (compileOut.tokenUsageEstimate) {\n  compIn = compileOut.tokenUsageEstimate.promptTokens || 0;\n  compOut = compileOut.tokenUsageEstimate.completionTokens || 0;\n}\nvar aIn = selData._analysisInputTokens || 0;\nvar aOut = selData._analysisOutputTokens || 0;\n// Fallback: estimate compilation tokens from content length\nif (compIn === 0 && compOut === 0) {\n  compIn = Math.ceil(((selData.prompt || '').length + 1200) / 4);\n  compOut = Math.ceil(responseText.length / 4);\n}\nvar totalIn = aIn + compIn;\nvar totalOut = aOut + compOut;\nvar haikuCost = (aIn * 1 + aOut * 5) / 1000000;\nvar sonnetCost = (compIn * 3 + compOut * 15) / 1000000;\nvar totalCost = Math.round((haikuCost + sonnetCost) * 1000000) / 1000000;\n\nvar lead = selData._lead;\nvar topStories = digest.top_stories || [];\nvar quickHits = digest.quick_hits || [];\nvar articles = selData._articles || [];\nvar allArticles = selData._allArticles || [];\n\n// === DISCORD EMBEDS ===\nvar embeds = [];\nif (lead) {\n  embeds.push({\n    title: lead.title, url: lead.url,\n    description: (digest.lead_analysis || lead.summary || '').substring(0, 4000),\n    color: lead.importance >= 9 ? 0xFF0000 : lead.importance >= 7 ? 0xFF8C00 : 0x3498DB,\n    fields: [\n      { name: 'Why It Matters', value: digest.lead_why || lead.whyItMatters || 'N/A', inline: false },\n      { name: 'Source', value: lead.source || '', inline: true },\n      { name: 'Score', value: (lead.importance || '?') + '/10', inline: true }\n    ],\n    footer: { text: 'Lead Story' }\n  });\n}\nfor (var i = 0; i < topStories.length && i < 4; i++) {\n  var ts = topStories[i];\n  var ma = articles[i + 1] || {};\n  embeds.push({\n    title: ts.title || ma.title || '', url: ma.url || '',\n    description: (ts.summary || '').substring(0, 2000),\n    color: (ma.importance || 5) >= 7 ? 0xFF8C00 : 0x3498DB,\n    fields: [{ name: 'Why', value: ts.why || 'N/A', inline: false }],\n    footer: { text: (ma.source || '') + ' | ' + (ma.importance || '?') + '/10' }\n  });\n}\nif (quickHits.length > 0) {\n  var qhText = '';\n  for (var i = 0; i < quickHits.length; i++) {\n    var qhArticle = articles[5 + i] || {};\n    var qhUrl = qhArticle.url || '';\n    var qhTitle = quickHits[i].title || qhArticle.title || '';\n    if (qhUrl) {\n      qhText += '\\u2022 [' + qhTitle + '](' + qhUrl + '): ' + (quickHits[i].one_liner || '') + '\\n';\n    } else {\n      qhText += '\\u2022 **' + qhTitle + '**: ' + (quickHits[i].one_liner || '') + '\\n';\n    }\n  }\n  embeds.push({ title: 'Quick Hits', description: qhText.substring(0, 4000), color: 0x95A5A6 });\n}\nembeds.push({\n  description: allArticles.length + ' articles analyzed | ' + articles.length + ' selected | Cost: $' + totalCost.toFixed(4),\n  color: 0x2C3E50, footer: { text: 'AI News Digest | Powered by Claude' }\n});\nembeds = embeds.slice(0, 10);\n\n// === SLACK BLOCKS ===\nvar blocks = [];\nblocks.push({ type: 'header', text: { type: 'plain_text', text: digest.subject_line || 'AI & Tech News Digest', emoji: true } });\nblocks.push({ type: 'context', elements: [{ type: 'mrkdwn', text: allArticles.length + ' articles analyzed | ' + articles.length + ' selected | $' + totalCost.toFixed(4) }] });\nblocks.push({ type: 'divider' });\nif (lead) {\n  blocks.push({ type: 'section', text: { type: 'mrkdwn', text: '*Lead: <' + lead.url + '|' + lead.title + '>*\\n' + (digest.lead_analysis || lead.summary || '').substring(0, 2900) } });\n  if (digest.lead_why) blocks.push({ type: 'context', elements: [{ type: 'mrkdwn', text: digest.lead_why }] });\n  blocks.push({ type: 'divider' });\n}\nfor (var i = 0; i < topStories.length; i++) {\n  var ts = topStories[i]; var ma = articles[i + 1] || {};\n  blocks.push({ type: 'section', text: { type: 'mrkdwn', text: '*<' + (ma.url || '') + '|' + (ts.title || ma.title || '') + '>*\\n' + (ts.summary || '') } });\n  if (ts.why) blocks.push({ type: 'context', elements: [{ type: 'mrkdwn', text: ts.why }] });\n}\nif (quickHits.length > 0) {\n  blocks.push({ type: 'divider' });\n  blocks.push({ type: 'section', text: { type: 'mrkdwn', text: '*Quick Hits*' } });\n  var qhLines = '';\n  for (var i = 0; i < quickHits.length; i++) {\n    var qhArticle = articles[5 + i] || {};\n    var qhUrl = qhArticle.url || '';\n    var qhTitle = quickHits[i].title || qhArticle.title || '';\n    if (qhUrl) {\n      qhLines += '\\u2022 <' + qhUrl + '|' + qhTitle + '>: ' + (quickHits[i].one_liner || '') + '\\n';\n    } else {\n      qhLines += '\\u2022 *' + qhTitle + '*: ' + (quickHits[i].one_liner || '') + '\\n';\n    }\n  }\n  blocks.push({ type: 'section', text: { type: 'mrkdwn', text: qhLines.substring(0, 2900) } });\n}\nif (digest.trend_note) {\n  blocks.push({ type: 'divider' });\n  blocks.push({ type: 'context', elements: [{ type: 'mrkdwn', text: '*Trend:* ' + digest.trend_note }] });\n}\n\n// === MARKDOWN ===\nvar md = '# ' + (digest.subject_line || 'AI & Tech News Digest') + '\\n\\n';\nif (lead) {\n  md += '## Lead Story\\n\\n**[' + lead.title + '](' + lead.url + ')** \u2014 ' + lead.source + ' (' + lead.importance + '/10)\\n\\n';\n  md += (digest.lead_analysis || lead.summary || '') + '\\n\\n';\n  if (digest.lead_why) md += '> ' + digest.lead_why + '\\n\\n';\n}\nif (topStories.length > 0) {\n  md += '## Top Stories\\n\\n';\n  for (var i = 0; i < topStories.length; i++) {\n    var ts = topStories[i]; var ma = articles[i + 1] || {};\n    md += '### [' + (ts.title || ma.title || '') + '](' + (ma.url || '') + ')\\n' + (ts.summary || '') + '\\n';\n    if (ts.why) md += '> ' + ts.why + '\\n';\n    md += '\\n';\n  }\n}\nif (quickHits.length > 0) {\n  md += '## Quick Hits\\n\\n';\n  for (var i = 0; i < quickHits.length; i++) {\n    var qhArticle = articles[5 + i] || {};\n    var qhUrl = qhArticle.url || '';\n    var qhTitle = quickHits[i].title || qhArticle.title || '';\n    if (qhUrl) {\n      md += '- **[' + qhTitle + '](' + qhUrl + ')**: ' + (quickHits[i].one_liner || '') + '\\n';\n    } else {\n      md += '- **' + qhTitle + '**: ' + (quickHits[i].one_liner || '') + '\\n';\n    }\n  }\n  md += '\\n';\n}\nif (digest.trend_note) md += '---\\n\\n**Trend:** ' + digest.trend_note + '\\n\\n';\nmd += '---\\n\\n_' + allArticles.length + ' articles analyzed | ' + articles.length + ' selected | $' + totalCost.toFixed(4) + ' API cost_\\n';\n\n// === SAVE DATA ===\nvar saveArticles = [];\nfor (var i = 0; i < articles.length; i++) {\n  var a = articles[i];\n  saveArticles.push({\n    urlHash: a.urlHash, titleNormalized: a.titleNormalized,\n    url: a.url, title: a.title, source: a.source,\n    publishedAt: a.publishedAt,\n    fullText: a.fullText ? a.fullText.substring(0, 50000) : '',\n    summary: a.summary || '', importance: a.importance || 5,\n    categories: a.categories || [], sentiment: a.sentiment || 'neutral',\n    keyEntities: a.keyEntities || [], whyItMatters: a.whyItMatters || '',\n    readingTimeMin: a.readingTimeMin || 3, includedInDigest: true\n  });\n}\n\nreturn [{ json: {\n  discordEmbeds: embeds, slackBlocks: blocks,\n  markdown: md, subjectLine: digest.subject_line || 'AI & Tech News Digest',\n  costUsd: totalCost, costInputTokens: totalIn, costOutputTokens: totalOut,\n  jinaTokens: selData._jinaTokens || 0, runId: selData._runId,\n  articlesFound: allArticles.length, articlesSelected: articles.length,\n  leadStoryTitle: lead ? lead.title : '', saveArticles: saveArticles,\n  trendNote: digest.trend_note || ''\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f7793af1-2496-4fbd-b9fd-9e80f4cae36f",
      "name": "Send digest to Discord",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        6208,
        608
      ],
      "parameters": {
        "url": "={{ $('\u2699\ufe0f Configure digest settings').first().json.discord_webhook_url }}",
        "method": "POST",
        "options": {
          "timeout": 10000
        },
        "jsonBody": "={{ JSON.stringify({ embeds: $json.discordEmbeds }) }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "41cf330d-7627-498c-99d0-c1de256afc06",
      "name": "Send digest to Slack",
      "type": "n8n-nodes-base.slack",
      "onError": "continueRegularOutput",
      "position": [
        6208,
        784
      ],
      "parameters": {
        "text": "={{ $json.subjectLine }}",
        "select": "channel",
        "blocksUi": "={{ JSON.stringify({ blocks: $json.slackBlocks }) }}",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('\u2699\ufe0f Configure digest settings').first().json.slack_channel_id }}"
        },
        "messageType": "block",
        "otherOptions": {
          "includeLinkToWorkflow": false
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "072a0613-8ada-4707-b056-aafb641431c2",
      "name": "Build article save query",
      "type": "n8n-nodes-base.code",
      "position": [
        6208,
        976
      ],
      "parameters": {
        "jsCode": "var data = $input.first().json;\nvar usePg = $('\u2699\ufe0f Configure digest settings').first().json.use_postgres;\nif (!usePg) {\n  return [{ json: { query: \"SELECT 1\", _runData: data } }];\n}\nvar articles = data.saveArticles || [];\nvar runId = data.runId;\n\nfunction esc(str) {\n  if (str === null || str === undefined) return 'NULL';\n  return \"'\" + String(str).replace(/'/g, \"''\") + \"'\";\n}\n\nif (articles.length === 0) {\n  return [{ json: { query: \"SELECT 'no articles to save'\", _runData: data } }];\n}\n\nvar values = [];\nfor (var i = 0; i < articles.length; i++) {\n  var a = articles[i];\n  values.push('(' +\n    esc(a.urlHash) + ',' + esc(a.titleNormalized) + ',' +\n    esc(a.url) + ',' + esc(a.title) + ',' + esc(a.source) + ',' +\n    (a.publishedAt ? esc(a.publishedAt) : 'NULL') + ',' +\n    esc((a.fullText || '').substring(0, 50000)) + ',' +\n    esc(a.summary) + ',' + (a.importance || 5) + ',' +\n    esc(JSON.stringify(a.categories || [])) + '::jsonb,' +\n    esc(a.sentiment) + ',' +\n    esc(JSON.stringify(a.keyEntities || [])) + '::jsonb,' +\n    esc(a.whyItMatters) + ',' + (a.readingTimeMin || 3) + ',' +\n    esc(runId) + ',' + (a.includedInDigest ? 'true' : 'false') + ')');\n}\n\nvar query = 'INSERT INTO digest_articles (url_hash, title_normalized, url, title, source_name, published_at, full_text, summary, importance_score, categories, sentiment, key_entities, why_it_matters, reading_time_min, digest_id, included_in_digest) VALUES ' + values.join(',') + ' ON CONFLICT (url_hash) DO NOTHING';\n\nreturn [{ json: { query: query, _runData: data } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "7e5072f5-ab02-4009-adc6-2aaf6e14e435",
      "name": "Save articles to database",
      "type": "n8n-nodes-base.postgres",
      "position": [
        6480,
        976
      ],
      "parameters": {
        "query": "={{ $json.query }}",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5,
      "alwaysOutputData": true
    },
    {
      "id": "6a5864ab-dcac-48a5-9a40-d88470440c5e",
      "name": "Mark run as completed",
      "type": "n8n-nodes-base.postgres",
      "position": [
        6736,
        976
      ],
      "parameters": {
        "query": "={{ $(\"\u2699\ufe0f Configure digest settings\").first().json.use_postgres ? \"UPDATE digest_runs SET status = 'completed', feeds_checked = 10, articles_found = \" + $('Format multi-channel outputs').first().json.articlesFound + \", articles_after_dedup = \" + $('Format multi-channel outputs').first().json.articlesFound + \", articles_selected = \" + $('Format multi-channel outputs').first().json.articlesSelected + \", lead_story_title = \" + ($('Format multi-channel outputs').first().json.leadStoryTitle ? \"'\" + $('Format multi-channel outputs').first().json.leadStoryTitle.replace(/'/g, \"''\") + \"'\" : \"NULL\") + \", cost_input_tokens = \" + $('Format multi-channel outputs').first().json.costInputTokens + \", cost_output_tokens = \" + $('Format multi-channel outputs').first().json.costOutputTokens + \", cost_usd = \" + $('Format multi-channel outputs').first().json.costUsd + \", jina_tokens_used = \" + $('Format multi-channel outputs').first().json.jinaTokens + \", completed_at = NOW(), duration_seconds = EXTRACT(EPOCH FROM (NOW() - started_at))::int WHERE id = '\" + $('Format multi-channel outputs').first().json.runId + \"'\" : \"SELECT 1\" }}",
        "options": {},
        "operation": "executeQuery"
      },
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.5,
      "alwaysOutputData": true
    },
    {
      "id": "5e659dc8-9dff-4e71-aa92-574d6d48f430",
      "name": "Run daily at 6 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        608,
        784
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 6 * * *"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "8fed8e62-681d-4eb2-8373-a1948983d503",
      "name": "Claude Haiku (analyzer)",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        5024,
        1056
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "claude-haiku-4-5-20251001"
        },
        "options": {
          "temperature": 0.2,
          "maxTokensToSample": 2048
        }
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "ae1b7fe1-3933-4ce8-8471-d7216e61f499",
      "name": "\u2699\ufe0f Configure digest settings",
      "type": "n8n-nodes-base.set",
      "position": [
        880,
        784
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1b2c3d4",
              "name": "discord_webhook_url",
              "type": "string",
              "value": "https://discord.com/api/webhooks/YOUR_DISCORD_WEBHOOK_URL"
            },
            {
              "id": "e5f6a7b8",
              "name": "slack_channel_id",
              "type": "string",
              "value": "YOUR_SLACK_CHANNEL_ID"
            },
            {
              "id": "c9d0e1f2",
              "name": "max_extract_articles",
              "type": "number",
              "value": 25
            },
            {
              "id": "a3b4c5d6",
              "name": "max_digest_articles",
              "type": "number",
              "value": 12
            },
            {
              "id": "e7f8a9b0",
              "name": "dedup_lookback_days",
              "type": "number",
              "value": 7
            },
            {
              "id": "c1d2e3f4",
              "name": "feed_lookback_hours",
              "type": "number",
              "value": 24
            },
            {
              "id": "pg_toggle_01",
              "name": "use_postgres",
              "type": "boolean",
              "value": false
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "3c774194-45a4-4f0a-b443-9585691b78d3",
      "name": "Overview \u2014 AI News Digest",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        96
      ],
      "parameters": {
        "width": 560,
        "height": 880,
        "content": "# AI News Digest\n\nAutomated daily digest that monitors RSS/API sources, extracts full article text, analyzes each with Claude AI, scores by importance, and delivers a formatted briefing to Discord, Slack, or email.\n\n### How it works\n1. Triggers on a daily schedule (default: 6 AM)\n2. Fetches articles from configurable sources (RSS, Atom, Reddit JSON, Hacker News API)\n3. Deduplicates by URL hash and fuzzy title matching (+ database lookback if Postgres enabled)\n4. Extracts full article text using [Jina Reader API](https://jina.ai/reader/)\n5. Claude Haiku analyzes each article (importance 1-10, categories, sentiment)\n6. Articles are scored with topic weighting and social signal boosting\n7. Claude Sonnet compiles a polished digest (lead story, top stories, quick hits, trend note)\n8. Delivers to Discord (embeds), Slack (blocks), or both\n\n### Quick start\n1. Add your **Anthropic API key** as an n8n credential ([setup guide](https://nxsi.io/guides/claude-api-setup))\n2. Open **\u2699\ufe0f Configure digest settings** and set your Discord webhook URL\n3. Activate the workflow \u2014 that's it!\n\n### Optional: PostgreSQL for history\nSet `use_postgres` to **true** in the config node to enable run tracking, article storage, and cross-day deduplication. Create the tables using the schema in the template description. The workflow runs fine without it.\n\n### Customization\n- Edit the feed list in **Build feed source list** to add/remove sources\n- Adjust topic weights in **Score and rank articles**\n- Change `max_extract_articles` and `max_digest_articles` in the config node\n- Modify the digest tone in the **Compile digest with Claude** system prompt\n- Swap Claude models: Haiku for cheaper runs, Opus for deeper analysis"
      },
      "typeVersion": 1
    },
    {
      "id": "df43ca14-d5df-4610-aa6e-aab8f63c3f46",
      "name": "Section \u2014 Configuration",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        576,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 452,
        "content": "##  Configuration\nAll user settings in one place. Edit this node to set your webhook URLs, channel IDs, processing limits, and the `use_postgres` toggle.\n\n[Discord webhook setup](https://support.discord.com/hc/en-us/articles/228383668) | [Slack webhook guide](https://api.slack.com/messaging/webhooks)"
      },
      "typeVersion": 1
    },
    {
      "id": "6ba9dcbd-b802-4050-a610-0d105dc54fa7",
      "name": "Section \u2014 Run Tracking",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1120,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 452,
        "content": "##  Run Tracking\nCreates a timestamped database record to track progress, article counts, and API costs per run.\n\n(Optional)\nPostgreSQL enables run tracking, article history, and cross-day dedup. Set `use_postgres` to **true** in the config node, then create the tables from the template description.\n\nNot required -  the workflow runs fully without a database."
      },
      "typeVersion": 1
    },
    {
      "id": "c8b57b6f-5d75-4e7c-bb74-6fed24b99d28",
      "name": "Section \u2014 Feed Collection",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1664,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 776,
        "height": 452,
        "content": "##  Feed Collection\nFetches and parses articles from RSS, Atom, Reddit JSON, and Hacker News API. Handles all feed formats automatically."
      },
      "typeVersion": 1
    },
    {
      "id": "4922558c-4424-4bc9-99dd-31474e2b0649",
      "name": "Section \u2014 Deduplication",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2496,
        432
      ],
      "parameters": {
        "color": 7,
        "width": 1080,
        "height": 548,
        "content": "##  Deduplication\nThree layers: URL hash matching, fuzzy title comparison (Levenshtein distance), and database lookback to skip recently-covered stories.\n\nDatabase dedup is automatic when Postgres is enabled. Without it, URL hash and title matching still prevent duplicates within each run."
      },
      "typeVersion": 1
    },
    {
      "id": "35dc639a-7bbd-44d4-9bf2-a63963b325a3",
      "name": "Section \u2014 Content Extraction",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3632,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 744,
        "height": 452,
        "content": "##  Content Extraction\nPulls full article text via [Jina Reader API](https://jina.ai/reader/) for deeper AI analysis beyond just titles and excerpts. Free tier handles most daily digest volumes."
      },
      "typeVersion": 1
    },
    {
      "id": "2da857f5-2fb9-43fe-84d3-b3e002b8227f",
      "name": "Section \u2014 AI Analysis",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4464,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 792,
        "height": 452,
        "content": "##  AI Analysis\nClaude Haiku scores each article 1-10 for importance, assigns categories, detects sentiment, and writes a practical \"why it matters\" for each."
      },
      "typeVersion": 1
    },
    {
      "id": "d39a1269-00a8-42c5-a164-14ec88ea9797",
      "name": "Section \u2014 Digest Compilation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5312,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 728,
        "height": 452,
        "content": "##  Digest Compilation\nClaude Sonnet compiles top articles into a structured digest: lead story with analysis, top stories, quick hits, and trend detection."
      },
      "typeVersion": 1
    },
    {
      "id": "4f971704-db51-4830-bb8a-af215aaee8c6",
      "name": "Section \u2014 Delivery and Storage",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6144,
        448
      ],
      "parameters": {
        "color": 7,
        "width": 744,
        "height": 716,
        "content": "##  Delivery & Storage\nSends formatted digest to Discord (embeds) and Slack (blocks). When Postgres is enabled, saves all analyzed articles and logs run completion stats.\n\nWithout Postgres, delivery works the same - the database nodes are safely skipped."
      },
      "typeVersion": 1
    },
    {
      "id": "0d60f0c8-aead-4180-b649-5a5b813558ac",
      "name": "Warning \u2014 Anthropic API Cost",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4992,
        1008
      ],
      "parameters": {
        "color": 3,
        "width": 464,
        "height": 328,
        "content": "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\u26a0\ufe0f **Requires Anthropic API key**\nAdd your key via n8n Credentials \u2192 Anthropic.\n\nQuick setup: [console.anthropic.com](https://console.anthropic.com) \u2192 API Keys \u2192 Create Key\nFull guide: [nxsi.io/guides/claude-api-setup](https://nxsi.io/guides/claude-api-setup)"
      },
      "typeVersion": 1
    },
    {
      "id": "077bd980-e9fc-464e-a343-94b6f5a97168",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        992
      ],
      "parameters": {
        "width": 784,
        "height": 864,
        "content": "## Optional: PostgreSQL\n\n  Set `use_postgres` to **true** in the config node to enable run tracking, article storage, and cross-day deduplication. The workflow runs fully without a database -- all five Postgres nodes are safely skipped when disabled.\n\n  If you enable it, create these tables first:\n\n  `CREATE TABLE IF NOT EXISTS digest_runs (\n    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),`\n    `run_date DATE NOT NULL,`\n    `started_at TIMESTAMPTZ DEFAULT now(),`\n    `completed_at TIMESTAMPTZ,`\n    `articles_found INTEGER DEFAULT 0,`\n    `articles_selected INTEGER DEFAULT 0,`\n    `cost_input_tokens INTEGER DEFAULT 0,`\n    `cost_output_tokens INTEGER DEFAULT 0,`\n    `cost_usd NUMERIC(10,6) DEFAULT 0,`\n    `status VARCHAR(20) DEFAULT 'running'`\n  `);`\n\n  `CREATE TABLE IF NOT EXISTS digest_articles (\n    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),`\n    `run_id UUID REFERENCES digest_runs(id),`\n    `url_hash VARCHAR(64) NOT NULL,`\n    `title_normalized VARCHAR(500),`\n    `url TEXT,`\n    `title TEXT,`\n    `source VARCHAR(100),`\n    `published_at TIMESTAMPTZ,`\n    `full_text TEXT,`\n    `summary TEXT,`\n    `importance INTEGER,`\n    `categories TEXT[],`\n    `sentiment VARCHAR(20),`\n    `key_entities TEXT[],`\n    `why_it_matters TEXT,`\n    `reading_time_min INTEGER,`\n    `included_in_digest BOOLEAN DEFAULT false,`\n    `created_at TIMESTAMPTZ DEFAULT now()`\n  `);`\n\n  `CREATE INDEX idx_digest_articles_url_hash ON digest_articles(url_hash);`\n  `CREATE INDEX idx_digest_articles_title ON digest_articles(title_normalized);`\n  `CREATE INDEX idx_digest_articles_created ON digest_articles(created_at);`"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "4db1ab82-a1e5-4f1e-a2da-45a80accced2",
  "connections": {
    "Create run record": {
      "main": [
        [
          {
            "node": "Build feed source list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run daily at 6 AM": {
      "main": [
        [
          {
            "node": "\u2699\ufe0f Configure digest settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch feed content": {
      "main": [
        [
          {
            "node": "Parse articles from feeds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Any articles found?": {
      "main": [
        [
          {
            "node": "Deduplicate by URL and title",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mark run as empty",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build feed source list": {
      "main": [
        [
          {
            "node": "Fetch feed content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Haiku (analyzer)": {
      "ai_languageModel": [
        [
          {
            "node": "Analyze article with Claude",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Score and rank articles": {
      "main": [
        [
          {
            "node": "Select articles for digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build article save query": {
      "main": [
        [
          {
            "node": "Save articles to database",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude Sonnet (compiler)": {
      "ai_languageModel": [
        [
          {
            "node": "Compile digest with Claude",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Initialize run timestamp": {
      "main": [
        [
          {
            "node": "Create run record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare analysis prompts": {
      "main": [
        [
          {
            "node": "Analyze article with Claude",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse articles from feeds": {
      "main": [
        [
          {
            "node": "Any articles found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save articles to database": {
      "main": [
        [
          {
            "node": "Mark run as completed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Assemble extracted content": {
      "main": [
        [
          {
            "node": "Prepare analysis prompts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compile digest with Claude": {
      "main": [
        [
          {
            "node": "Format multi-channel outputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract full text via Jina": {
      "main": [
        [
          {
            "node": "Assemble extracted content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select articles for digest": {
      "main": [
        [
          {
            "node": "Compile digest with Claude",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze article with Claude": {
      "main": [
        [
        

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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This n8n template builds an automated daily news digest powered by Claude AI.

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

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

Complete PostgreSQL-backed system: Keyword scoring → AI research → Multi-part content generation → fal.ai Nano Banana image generation → WordPress publishing

WordPress, OpenAI, Perplexity +8
AI & RAG

This workflow empowers app developers and community management teams by automating the generation and posting of responses to user reviews on the Apple App Store. Designed to streamline the engagement

Jwt, HTTP Request, Slack +5
AI & RAG

The Recap AI - Short Form News Script Generator. Uses httpRequest, s3, chainLlm, slack. Scheduled trigger; 42 nodes.

HTTP Request, S3, Chain Llm +3
AI & RAG

This workflow is the AI analysis and alerting engine for a complete social media monitoring system. It's designed to work with data scraped from X (formerly Twitter) using a tool like the Apify Tweet

Google Sheets, Google Gemini Chat, Google Sheets Tool +4
AI & RAG

This workflow automates engineering governance by deploying a multi-agent AI system that validates designs, checks compliance, optimises safety, and predicts maintenance needs. Designed for engineerin

HTTP Request, Agent, Agent Tool +3