{
  "id": "11dailynewsbrief",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Get a daily AI news brief from 50+ curated feeds, tagged by category and ranked by your interests",
  "tags": [],
  "nodes": [
    {
      "id": "d1fe8f7f-cccc-4133-988b-809f989f691c",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        272,
        608
      ],
      "parameters": {
        "width": 792,
        "height": 680,
        "content": "## Daily category-tagged news brief \u2192 email\n\nPick the topics you care about and get one ranked brief every morning, pulled from a built-in catalog of 50+ quality feeds and grouped by category.\n\n### How it works\nSet your categories (ai, tech, startups, jobs, dev, security, product, science, business, marketing, world) or use all. A Code node expands the baked-in catalog, a native HTTP Request node fetches every feed in those categories, and a Code node parses the results and tags each article with its category, keeps only recent items, and de-duplicates by link. A single AI call drops noise, picks the most relevant stories against your interests while keeping your category mix, rewrites tight headlines, and scores importance. The picks are grouped by category and emailed to you.\n\n### Setup\n- Edit the config node: categories, your interests, look-back window, and the send-to address.\n- Add credentials in the canvas: Anthropic on the ranking HTTP node and Gmail on the send node.\n- The Schedule Trigger runs daily; change the hour to taste.\n\n### Customization\nEdit the catalog in the expand node to add or remove feeds and categories, widen the window for a weekly edition, or swap the email for Slack or Telegram.\n\nBuilt by **nocode.expert** \u2014 done-for-you automation and AI workflows. https://nocode.expert"
      },
      "typeVersion": 1
    },
    {
      "id": "3585e065-d525-4dc0-9318-184a1230ada3",
      "name": "Section: Fetch & tag",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1424,
        880
      ],
      "parameters": {
        "color": 7,
        "width": 600,
        "height": 420,
        "content": "## 1. Fetch & tag\nPick categories in the config. The HTTP node fetches every feed in those categories from the built-in 50+ catalog; the parse node tags each article by category, keeping recent, de-duplicated items."
      },
      "typeVersion": 1
    },
    {
      "id": "b50f404a-fa72-4606-8507-61c843c4e046",
      "name": "Section: Rank with AI",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2096,
        880
      ],
      "parameters": {
        "color": 7,
        "width": 748,
        "height": 420,
        "content": "## 2. Rank vs your interests\nOne AI call drops noise, picks the most relevant stories across categories, and scores importance while keeping the tags."
      },
      "typeVersion": 1
    },
    {
      "id": "6ff819c3-090a-4e16-a354-6cf26170e5b9",
      "name": "Section: Email brief",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2912,
        896
      ],
      "parameters": {
        "color": 7,
        "width": 536,
        "height": 404,
        "content": "## 3. Email, grouped by tag\nPicks are grouped by category and emailed. Swap for Slack or Telegram if you prefer."
      },
      "typeVersion": 1
    },
    {
      "id": "92ac8b8c-ad06-4712-8dd3-cd0161c251e5",
      "name": "Every morning",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        1232,
        1120
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 7
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "df03c912-cde9-413c-9b57-17cf4a946385",
      "name": "Set config: categories & interests",
      "type": "n8n-nodes-base.set",
      "position": [
        1488,
        1120
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "categories",
              "type": "string",
              "value": "ai, tech, startups, jobs"
            },
            {
              "id": "a2",
              "name": "interests",
              "type": "string",
              "value": "practical AI/LLM tooling, funding and startup moves, remote product/eng roles, anything genuinely novel"
            },
            {
              "id": "a3",
              "name": "hoursBack",
              "type": "string",
              "value": "24"
            },
            {
              "id": "a4",
              "name": "perFeed",
              "type": "string",
              "value": "10"
            },
            {
              "id": "a5",
              "name": "maxItems",
              "type": "string",
              "value": "40"
            },
            {
              "id": "a6",
              "name": "topN",
              "type": "string",
              "value": "15"
            },
            {
              "id": "a7",
              "name": "digestTo",
              "type": "string",
              "value": "you@example.com"
            },
            {
              "id": "a8",
              "name": "model",
              "type": "string",
              "value": "claude-haiku-4-5"
            },
            {
              "id": "a9",
              "name": "anthropicKey",
              "type": "string",
              "value": ""
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "177800c4-93a0-4204-b62e-f72efb3af6e8",
      "name": "Expand selected feeds",
      "type": "n8n-nodes-base.code",
      "position": [
        1696,
        1120
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set config: categories & interests').first().json;\nconst CATALOG = {\"ai\":[\"https://huggingface.co/blog/feed.xml\",\"https://the-decoder.com/feed/\",\"https://venturebeat.com/category/ai/feed/\",\"https://simonwillison.net/atom/everything/\",\"https://openai.com/blog/rss.xml\",\"https://deepmind.google/blog/rss.xml\",\"https://bair.berkeley.edu/blog/feed.xml\",\"https://jack-clark.net/feed/\",\"https://hnrss.org/newest?q=AI+OR+LLM&points=100\"],\"tech\":[\"https://www.theverge.com/rss/index.xml\",\"https://techcrunch.com/feed/\",\"https://arstechnica.com/feed/\",\"https://www.wired.com/feed/rss\",\"https://www.engadget.com/rss.xml\",\"https://hnrss.org/frontpage\",\"https://www.technologyreview.com/feed/\",\"https://restofworld.org/feed/latest/\"],\"startups\":[\"https://techcrunch.com/category/startups/feed/\",\"https://news.crunchbase.com/feed/\",\"https://www.saastr.com/feed/\",\"https://tomtunguz.com/index.xml\",\"https://andrewchen.com/feed/\"],\"jobs\":[\"https://weworkremotely.com/remote-jobs.rss\",\"https://hnrss.org/jobs\",\"https://hnrss.org/whoishiring/jobs\",\"https://remotive.com/remote-jobs/feed\",\"https://jobicy.com/?feed=job_feed\"],\"dev\":[\"https://dev.to/feed\",\"https://css-tricks.com/feed/\",\"https://martinfowler.com/feed.atom\",\"https://stackoverflow.blog/feed/\",\"https://github.blog/feed/\",\"https://www.smashingmagazine.com/feed/\",\"https://overreacted.io/rss.xml\"],\"product_design\":[\"https://www.lennysnewsletter.com/feed\",\"https://uxdesign.cc/feed\",\"https://www.nngroup.com/feed/rss/\",\"https://uxplanet.org/feed\"],\"security\":[\"https://krebsonsecurity.com/feed/\",\"https://www.bleepingcomputer.com/feed/\",\"https://feeds.feedburner.com/TheHackersNews\",\"https://www.schneier.com/feed/atom/\",\"https://www.darkreading.com/rss.xml\"],\"business_finance\":[\"https://feeds.a.dj.com/rss/RSSMarketsMain.xml\",\"https://www.cnbc.com/id/1+1234567890/device/rss/rss.html\",\"https://feeds.bloomberg.com/technology/news.rss\",\"https://fortune.com/feed/\"],\"science\":[\"https://www.sciencedaily.com/rss/all.xml\",\"https://arstechnica.com/science/feed/\",\"https://www.quantamagazine.org/feed/\",\"https://phys.org/rss-feed/\"],\"marketing\":[\"https://searchengineland.com/feed\",\"https://moz.com/blog/feed\",\"https://www.socialmediaexaminer.com/feed/\",\"https://www.marketingdive.com/feeds/news/\"],\"world\":[\"https://feeds.bbci.co.uk/news/rss.xml\",\"https://www.aljazeera.com/xml/rss/all.xml\",\"https://feeds.npr.org/1001/rss.xml\",\"https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml\"]};\nconst wanted = String(cfg.categories || '').toLowerCase().trim();\nconst cats = (!wanted || wanted === 'all')\n  ? Object.keys(CATALOG)\n  : wanted.split(',').map((s) => s.trim()).filter((c) => CATALOG[c]);\nconst feeds = [];\nfor (const c of cats) for (const url of (CATALOG[c] || [])) feeds.push({ feedUrl: url, category: c });\nreturn feeds.map((f) => ({ json: f }));"
      },
      "typeVersion": 2
    },
    {
      "id": "b94622a5-0ab7-4511-a700-cd84f03fd1aa",
      "name": "Fetch each feed",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        1856,
        1120
      ],
      "parameters": {
        "url": "={{ $json.feedUrl }}",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
            },
            {
              "name": "Accept",
              "value": "application/rss+xml, application/atom+xml, application/xml, text/xml, */*"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "b7e61071-d5f9-4a40-bdc5-ebf7fd481c2c",
      "name": "Parse & tag articles",
      "type": "n8n-nodes-base.code",
      "position": [
        2176,
        1120
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set config: categories & interests').first().json;\nconst feedsIn = $('Expand selected feeds').all();\nconst perFeed = Number(cfg.perFeed) || 10;\nconst hours = Number(cfg.hoursBack) || 24;\nconst cutoff = Date.now() - hours * 3600 * 1000;\nconst maxItems = Number(cfg.maxItems) || 40;\n\nconst strip = (h) => String(h || '').replace(/<!\\[CDATA\\[|\\]\\]>/g, '').replace(/<[^>]+>/g, ' ').replace(/&[a-z#0-9]+;/g, ' ').replace(/\\s+/g, ' ').trim();\nconst tag = (b, t) => { const m = new RegExp('<' + t + '[^>]*>([\\\\s\\\\S]*?)</' + t + '>', 'i').exec(b); return m ? m[1] : ''; };\nfunction parse(xml) {\n  const out = []; const re = /<(item|entry)[\\s\\S]*?<\\/\\1>/gi; let m;\n  while ((m = re.exec(xml))) {\n    const b = m[0];\n    let link = strip(tag(b, 'link'));\n    if (!link) { const h = /<link[^>]*href=\"([^\"]+)\"/i.exec(b); if (h) link = h[1]; }\n    out.push({\n      title: strip(tag(b, 'title')), link,\n      snippet: strip(tag(b, 'description') || tag(b, 'summary') || tag(b, 'content')).slice(0, 400),\n      date: strip(tag(b, 'pubDate') || tag(b, 'updated') || tag(b, 'published') || tag(b, 'dc:date')),\n    });\n  }\n  return out;\n}\n// map each HTTP response back to the feed (and its category) that produced it\nconst catOf = (it, idx) => {\n  let pi = it.pairedItem;\n  let p = (typeof pi === 'number') ? pi : (Array.isArray(pi) ? (pi[0] || {}).item : (pi || {}).item);\n  if (p === undefined || p === null) p = idx;\n  return (feedsIn[p] && feedsIn[p].json.category) || 'general';\n};\n\nconst seen = new Set(); const items = [];\nconst responses = $input.all();\nfor (let idx = 0; idx < responses.length; idx++) {\n  const j = responses[idx].json || {};\n  const xml = typeof j.data === 'string' ? j.data : (typeof j.body === 'string' ? j.body : (typeof responses[idx].json === 'string' ? responses[idx].json : ''));\n  if (!xml) continue;\n  const category = catOf(responses[idx], idx);\n  for (const a of parse(xml).slice(0, perFeed)) {\n    if (!a.title || !a.link) continue;\n    const ts = Date.parse(a.date) || Date.now();\n    if (ts < cutoff) continue;\n    const key = a.link.toLowerCase();\n    if (seen.has(key)) continue;\n    seen.add(key);\n    items.push({ title: a.title, link: a.link, snippet: a.snippet, category, ts });\n  }\n}\nitems.sort((x, y) => y.ts - x.ts);\n// balance the pool round-robin across categories so a high-volume tag (e.g. tech) does not crowd out others\nconst buckets = {};\nfor (const a of items) (buckets[a.category] = buckets[a.category] || []).push(a); // already recency-sorted\nconst catKeys = Object.keys(buckets);\nconst balanced = []; let added = true;\nwhile (balanced.length < maxItems && added) {\n  added = false;\n  for (const c of catKeys) { if (buckets[c].length) { balanced.push(buckets[c].shift()); added = true; if (balanced.length >= maxItems) break; } }\n}\nconst capped = balanced.map((a, i) => ({ i, title: a.title, link: a.link, snippet: a.snippet, category: a.category }));\nreturn [{ json: { items: capped, count: capped.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "54bbbf71-77be-459d-ae58-bdf5d5a46ced",
      "name": "Build AI ranking request",
      "type": "n8n-nodes-base.code",
      "position": [
        2368,
        1120
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set config: categories & interests').first().json;\nconst items = $json.items || [];\nconst schema = { type: 'object', additionalProperties: false, required: ['overview', 'picks'], properties: {\n  overview: { type: 'string' },\n  picks: { type: 'array', items: { type: 'object', additionalProperties: false,\n    required: ['i', 'headline', 'why', 'category', 'importance'], properties: {\n      i: { type: 'integer' }, headline: { type: 'string' }, why: { type: 'string' },\n      category: { type: 'string' }, importance: { type: 'integer' } } } } } };\nconst system = 'You are a sharp editor building a personalised brief for ONE reader. Each article already has a category tag. Drop near-duplicates and low-signal noise, then pick the most relevant to the reader interests across ALL categories. For each pick: headline = a tight rewrite, why = one sentence on why it matters to this reader, category = keep the article tag, importance = 0-100. overview = a 2-sentence read of the day. Keep the mix of categories the reader selected; do not over-index on one. Be terse, no hype.';\nconst user = 'READER INTERESTS: ' + cfg.interests + '\\n\\nSelected categories present: ' + (items.length ? [...new Set(items.map((a) => a.category))].join(', ') : '') +\n  '. Pick the top ' + (cfg.topN || 15) + ' overall.\\n\\nARTICLES (reference each by index i):\\n' +\n  items.map((a) => '[' + a.i + '] (' + a.category + ') ' + a.title + '\\n' + a.snippet).join('\\n\\n');\nreturn [{ json: { body: {\n  model: cfg.model, max_tokens: 2500, system,\n  messages: [{ role: 'user', content: user }],\n  output_config: { format: { type: 'json_schema', schema } },\n} } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "e7c07a4e-6de4-4cb5-95f2-22f56244a4ea",
      "name": "Rank articles with Claude",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2576,
        1120
      ],
      "parameters": {
        "url": "https://api.anthropic.com/v1/messages",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "x-api-key",
              "value": "={{ $('Set config: categories & interests').first().json.anthropicKey }}"
            },
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "09393dc4-edbb-497b-a802-42c9f249fbb8",
      "name": "Format the brief by category",
      "type": "n8n-nodes-base.code",
      "position": [
        3008,
        1120
      ],
      "parameters": {
        "jsCode": "const cfg = $('Set config: categories & interests').first().json;\nconst items = $('Parse & tag articles').first().json.items || [];\nconst labels = {\"ai\":\"AI\",\"tech\":\"Tech\",\"startups\":\"Startups\",\"jobs\":\"Jobs\",\"dev\":\"Dev\",\"product_design\":\"Product & Design\",\"security\":\"Security\",\"business_finance\":\"Business & Finance\",\"science\":\"Science\",\"marketing\":\"Marketing\",\"world\":\"World\"};\nconst top = Number(cfg.topN) || 15;\nconst blocks = ($json.content || []);\nconst text = (blocks.find((b) => b.type === 'text') || {}).text || '{\"overview\":\"\",\"picks\":[]}';\nlet parsed = { overview: '', picks: [] };\ntry { parsed = JSON.parse(text); } catch (e) {}\nconst esc = (s) => String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\nconst picks = (parsed.picks || []).map((p) => ({ ...p, ...(items[p.i] || {}), category: p.category || (items[p.i] || {}).category }))\n  .filter((p) => p.link).sort((a, b) => b.importance - a.importance).slice(0, top);\nconst groups = {};\nfor (const p of picks) (groups[p.category] = groups[p.category] || []).push(p);\nlet body = '';\nfor (const cat of Object.keys(groups)) {\n  body += '<h3 style=\"margin:16px 0 6px;text-transform:uppercase;letter-spacing:.04em;color:#111\">' + esc(labels[cat] || cat) + '</h3><ul style=\"padding-left:18px;margin:0\">';\n  for (const p of groups[cat]) body += '<li style=\"margin:8px 0\"><a href=\"' + esc(p.link) + '\" style=\"font-weight:700\">' + esc(p.headline) + '</a> <span style=\"color:#999\">\u00b7 ' + p.importance + '</span><br><span style=\"color:#333\">' + esc(p.why) + '</span></li>';\n  body += '</ul>';\n}\nconst html = '<h2 style=\"font-family:system-ui,Arial\">Your morning brief</h2>' +\n  (parsed.overview ? '<p style=\"color:#444;font-family:system-ui,Arial\">' + esc(parsed.overview) + '</p>' : '') +\n  '<div style=\"font-family:system-ui,Arial\">' + body + '</div>' +\n  '<p style=\"color:#999;font-size:12px\">' + picks.length + ' picks from ' + items.length + ' recent articles across ' + Object.keys(groups).length + ' categories.</p>';\nconst subject = picks.length ? 'Brief: ' + picks.length + ' picks \u00b7 ' + Object.keys(groups).map((c) => labels[c] || c).join(', ') : 'Brief: quiet news day';\nreturn [{ json: { subject, html, count: picks.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "57fbe61e-bf19-4b70-809e-d5af8f94899d",
      "name": "Email the brief",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3296,
        1120
      ],
      "parameters": {
        "sendTo": "={{ $('Set config: categories & interests').first().json.digestTo }}",
        "message": "={{ $json.html }}",
        "options": {},
        "subject": "={{ $json.subject }}"
      },
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "8a4ee16a-f258-4b33-bfc5-8ad6532cf74a",
  "nodeGroups": [],
  "connections": {
    "Every morning": {
      "main": [
        [
          {
            "node": "Set config: categories & interests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch each feed": {
      "main": [
        [
          {
            "node": "Parse & tag articles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse & tag articles": {
      "main": [
        [
          {
            "node": "Build AI ranking request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Expand selected feeds": {
      "main": [
        [
          {
            "node": "Fetch each feed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build AI ranking request": {
      "main": [
        [
          {
            "node": "Rank articles with Claude",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rank articles with Claude": {
      "main": [
        [
          {
            "node": "Format the brief by category",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format the brief by category": {
      "main": [
        [
          {
            "node": "Email the brief",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set config: categories & interests": {
      "main": [
        [
          {
            "node": "Expand selected feeds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}