{
  "name": "AI Content Digest \u2192 Email",
  "nodes": [
    {
      "id": "sticky-01-overview",
      "name": "Overview & Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -780,
        100
      ],
      "parameters": {
        "color": 3,
        "width": 560,
        "height": 560,
        "content": "## \ud83d\udcf0 AI Content Digest \u2192 Email\n\nTurns your own list of RSS feeds into one clean, AI-curated HTML briefing in your inbox \u2014 on a schedule. Zero manual reading.\n\n### \ud83d\udc64 Who's it for\nFounders, marketers and researchers who want a personalised weekly briefing instead of doomscrolling feeds.\n\n### \u2699\ufe0f How it works\n1. **Schedule** fires weekly (Mon 08:00).\n2. **RSS Read** pulls every feed; articles are merged, deduped, filtered to the last 7 days and ranked.\n3. **OpenAI** groups them into 2\u20134 themes and writes a one-sentence summary per item as ready-to-send HTML.\n4. **Gmail** sends the branded email.\n\n### \ud83d\udd27 Set up (~5 min)\n- Add an **OpenAI** credential (default `gpt-4o-mini`) and a **Gmail OAuth2** credential.\n- Put your feeds in **Define Feed Sources** and the recipient in **Build Email HTML**.\n- Tune `LOOKBACK_DAYS` / `MAX_ARTICLES` to taste."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-01-s1",
      "name": "Section 1 \u00b7 Sources",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -150,
        100
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 360,
        "content": "### 1. Sources\nRuns weekly, then emits one item per feed. Edit the feed list in **Define Feed Sources**."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-01-s2",
      "name": "Section 2 \u00b7 Fetch & Rank",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        510,
        100
      ],
      "parameters": {
        "color": 7,
        "width": 540,
        "height": 360,
        "content": "### 2. Fetch & Rank\nRSS Read runs once per feed and fans out into articles, which are merged, filtered to the last 7 days and ranked newest-first."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-01-s3",
      "name": "Section 3 \u00b7 AI Curation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1190,
        100
      ],
      "parameters": {
        "color": 7,
        "width": 200,
        "height": 360,
        "content": "### 3. AI Curation\nOpenAI acts as an editor: groups articles into 2\u20134 themes and writes a one-sentence summary per item as HTML."
      },
      "typeVersion": 1
    },
    {
      "id": "sticky-01-s4",
      "name": "Section 4 \u00b7 Send",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1530,
        100
      ],
      "parameters": {
        "color": 7,
        "width": 540,
        "height": 360,
        "content": "### 4. Send\nThe AI HTML is wrapped in a branded email shell and sent via Gmail. Set the recipient in **Build Email HTML**."
      },
      "typeVersion": 1
    },
    {
      "id": "schedule-trigger",
      "name": "Weekly Monday 08:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -100,
        220
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 8,
              "weeksInterval": 1,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "define-feeds",
      "name": "Define Feed Sources",
      "type": "n8n-nodes-base.code",
      "position": [
        220,
        220
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// EDIT ME: your RSS feed sources.\n// Add one entry per feed you want in your digest.\n// `source` is a friendly label shown in the email; `url` is the feed.\n// The next node (RSS Read) runs ONCE PER ITEM returned here.\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst FEEDS = [\n  { source: 'TechCrunch',     url: 'https://techcrunch.com/feed/' },\n  { source: 'The Verge',      url: 'https://www.theverge.com/rss/index.xml' },\n  { source: 'Hacker News',    url: 'https://hnrss.org/frontpage' },\n];\n\nreturn FEEDS.map((feed) => ({ json: feed }));"
      },
      "typeVersion": 2
    },
    {
      "id": "rss-read",
      "name": "Read RSS Feed",
      "type": "n8n-nodes-base.rssFeedRead",
      "onError": "continueRegularOutput",
      "position": [
        560,
        220
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "merge-rank",
      "name": "Merge, Filter & Rank Articles",
      "type": "n8n-nodes-base.code",
      "position": [
        900,
        220
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Combine every article from every feed, keep only recent ones,\n// rank newest-first, take the top N, and build a compact text block\n// that the AI can summarize cheaply.\n\nconst LOOKBACK_DAYS = 7;   // only keep articles newer than this\nconst MAX_ARTICLES  = 15;  // cap to control token cost\n\nconst cutoff = Date.now() - LOOKBACK_DAYS * 24 * 60 * 60 * 1000;\n\nconst stripHtml = (s) => String(s || '').replace(/<[^>]*>/g, ' ').replace(/\\s+/g, ' ').trim();\n\nconst seen = new Set();\nconst articles = [];\n\nfor (const item of $input.all()) {\n  const a = item.json || {};\n  const title = stripHtml(a.title);\n  const link = a.link || a.guid || '';\n  if (!title || !link) continue;\n\n  // Dedupe by link, then by lowercased title\n  const key = String(link).split('?')[0].toLowerCase();\n  const titleKey = title.toLowerCase();\n  if (seen.has(key) || seen.has(titleKey)) continue;\n  seen.add(key);\n  seen.add(titleKey);\n\n  // Parse publish date; if missing/invalid, keep the article (treat as 'now')\n  const raw = a.isoDate || a.pubDate || a.published || a.date;\n  const ts = raw ? Date.parse(raw) : NaN;\n  const ms = Number.isNaN(ts) ? Date.now() : ts;\n  if (!Number.isNaN(ts) && ms < cutoff) continue;\n\n  const source = a.source || a.creator || (a.meta && a.meta.title) || 'Source';\n  const snippet = stripHtml(a.contentSnippet || a.content || a.summary || a.description).slice(0, 220);\n\n  articles.push({ title, link, source, snippet, ms });\n}\n\narticles.sort((x, y) => y.ms - x.ms);\nconst top = articles.slice(0, MAX_ARTICLES);\n\nconst articlesText = top\n  .map((a, i) => `${i + 1}. ${a.title} \u2014 ${a.source} \u2014 ${a.link}\\n   ${a.snippet}`)\n  .join('\\n\\n');\n\nreturn [{\n  json: {\n    articleCount: top.length,\n    generatedFor: new Date().toISOString().slice(0, 10),\n    articlesText: articlesText || 'No new articles were found in the lookback window.',\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "openai-curate",
      "name": "Curate & Summarize Digest",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1240,
        220
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "options": {
          "maxTokens": 1800,
          "temperature": 0.4,
          "instructions": "You are an expert newsletter editor. From the provided list of articles, produce a concise weekly digest as VALID HTML (a fragment, no <html>/<body> wrapper).\n\nRequirements:\n- Start with a single <p> one-line intro summarizing the week's theme.\n- Group the items into 2\u20134 thematic sections. Each section starts with an <h2> heading.\n- Under each heading, output a <ul> where every <li> is: <a href=\"LINK\">Title</a> \u2014 a one-sentence summary in your own words, then the source in parentheses.\n- Drop near-duplicate stories; keep the strongest version.\n- Do NOT invent links or facts; only use the provided articles.\n- Output ONLY the HTML fragment, with no markdown code fences and no commentary."
        },
        "simplify": true,
        "responses": {
          "values": [
            {
              "role": "user",
              "type": "text",
              "content": "=Here are this week's candidate articles ({{ $json.articleCount }} items). Curate them into the HTML digest as instructed.\n\n--- ARTICLES ---\n{{ $json.articlesText }}\n--- END ARTICLES ---"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "build-email",
      "name": "Build Email HTML",
      "type": "n8n-nodes-base.set",
      "position": [
        1580,
        220
      ],
      "parameters": {
        "mode": "manual",
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "assign-recipient",
              "name": "recipient",
              "type": "string",
              "value": "you@example.com"
            },
            {
              "id": "assign-digestDate",
              "name": "digestDate",
              "type": "string",
              "value": "={{ $now.format('cccc, dd LLLL yyyy') }}"
            },
            {
              "id": "assign-html",
              "name": "emailHtml",
              "type": "string",
              "value": "=<!DOCTYPE html>\n<html>\n<body style=\"margin:0;padding:0;background:#f4f5f7;font-family:Arial,Helvetica,sans-serif;color:#1f2933;\">\n  <table role=\"presentation\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#f4f5f7;padding:24px 0;\">\n    <tr><td align=\"center\">\n      <table role=\"presentation\" width=\"600\" cellpadding=\"0\" cellspacing=\"0\" style=\"background:#ffffff;border-radius:12px;overflow:hidden;\">\n        <tr><td style=\"background:#0f172a;padding:24px 32px;\">\n          <div style=\"color:#ffffff;font-size:20px;font-weight:bold;\">\ud83d\udcf0 Your Weekly Content Digest</div>\n          <div style=\"color:#94a3b8;font-size:13px;margin-top:4px;\">{{ $json.digestDate }}</div>\n        </td></tr>\n        <tr><td style=\"padding:28px 32px;font-size:15px;line-height:1.6;\">\n          {{ $('Curate & Summarize Digest').item.json.content }}\n        </td></tr>\n        <tr><td style=\"padding:18px 32px;border-top:1px solid #e5e7eb;color:#94a3b8;font-size:12px;line-height:1.5;\">\n          Curated automatically with n8n + OpenAI. To change the feeds or schedule, edit your workflow.\n        </td></tr>\n      </table>\n    </td></tr>\n  </table>\n</body>\n</html>"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "send-email",
      "name": "Send Digest Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1920,
        220
      ],
      "parameters": {
        "sendTo": "={{ $json.recipient }}",
        "message": "={{ $json.emailHtml }}",
        "options": {
          "appendAttribution": false
        },
        "subject": "=\ud83d\udcf0 Your Weekly Content Digest \u2014 {{ $json.digestDate }}",
        "resource": "message",
        "emailType": "html",
        "operation": "send"
      },
      "typeVersion": 2.2
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Read RSS Feed": {
      "main": [
        [
          {
            "node": "Merge, Filter & Rank Articles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Email HTML": {
      "main": [
        [
          {
            "node": "Send Digest Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Define Feed Sources": {
      "main": [
        [
          {
            "node": "Read RSS Feed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Weekly Monday 08:00": {
      "main": [
        [
          {
            "node": "Define Feed Sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Curate & Summarize Digest": {
      "main": [
        [
          {
            "node": "Build Email HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge, Filter & Rank Articles": {
      "main": [
        [
          {
            "node": "Curate & Summarize Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}