{
  "id": "NwsRssAiDraft000001",
  "name": "Auto-draft your weekly newsletter from RSS feeds with AI",
  "nodes": [
    {
      "id": "ae6b0fcf-3eac-4950-aba4-5bf6301e8bdc",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1560,
        -80
      ],
      "parameters": {
        "width": 540,
        "height": 1180,
        "content": "## Auto-draft your weekly newsletter from RSS feeds with AI\n\n### How it works\n\nThis workflow runs weekly, pulls recent articles from configured RSS feeds, and filters them to the last seven days. It uses OpenAI to draft a newsletter digest, formats the result as email-safe HTML, then creates a **draft** broadcast in Kit (`send_at: null` \u2014 it never emails your subscribers by itself; you review, edit and send). It can also notify Slack and includes an alternate branch for emailing the draft to yourself through Gmail.\n\n### Setup steps\n\n- Edit the \"Define Source Feeds\" code node with the RSS feed URLs you want to include.\n- Configure the schedule trigger if Monday at 05:00 is not the desired run time.\n- On \"Post to OpenAI API\", create a **Header Auth** credential: name `Authorization`, value `Bearer sk-\u2026`. Any OpenAI-compatible endpoint works, including self-hosted Ollama \u2014 just change the URL.\n- On \"Post to Kit API\", create a second **Header Auth** credential: name `X-Kit-Api-Key`, value = your API key from Kit \u2192 Settings \u2192 Developer.\n- Edit the \"Create AI Prompt\" node to set the newsletter name, tone, model and any editorial instructions.\n- Optional: add a Slack incoming webhook URL to the notification node.\n- Optional: connect Gmail and set the recipient if using the self-email branch. **Keep the delivery branch you use and delete the other** (or keep both).\n\n### Customization\n\nAdjust the feed list, seven-day filter window, 25-story cap, newsletter voice, and HTML wrapper. Prefer another ESP? Swap one HTTP call: beehiiv `POST /v2/publications/{id}/posts`, Mailchimp `POST /campaigns` + `/content` \u2014 or use the Gmail branch and paste the draft anywhere (Substack, Ghost, Buttondown)."
      },
      "typeVersion": 1
    },
    {
      "id": "fc6987a1-5ff7-404b-a01a-3fe37a137af1",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -960,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 680,
        "height": 360,
        "content": "## 1. Schedule and read feeds\n\nStarts the workflow every Monday morning, defines the RSS feed list to monitor, and reads items from those feeds."
      },
      "typeVersion": 1
    },
    {
      "id": "357852c1-e674-4ccb-b342-0c56deff81f6",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -240,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 360,
        "content": "## 2. Filter and prepare prompt\n\nFilters feed entries to recent stories, tidies the fields, limits the story count, and builds the AI prompt with the desired newsletter voice."
      },
      "typeVersion": 1
    },
    {
      "id": "495556f9-0d44-4681-a970-91e37c844f4f",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        260,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 460,
        "height": 360,
        "content": "## 3. Generate newsletter HTML\n\nSends the prepared prompt to OpenAI to draft the digest, then wraps the AI copy in a minimal email-safe HTML layout."
      },
      "typeVersion": 1
    },
    {
      "id": "dc569f4f-8f94-4f9f-ba40-9c9231516afc",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        740,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 520,
        "height": 360,
        "content": "## 4. Create Kit broadcast\n\nCreates a draft broadcast in Kit from the generated HTML and optionally posts a Slack notification that the draft is ready. `send_at: null` is what keeps it a draft \u2014 please keep a human review before sending."
      },
      "typeVersion": 1
    },
    {
      "id": "2b7d012d-d3b0-4fae-9fc1-cdcdad15f120",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        740,
        440
      ],
      "parameters": {
        "color": 7,
        "width": 380,
        "height": 340,
        "content": "## 5. Email draft alternative\n\nAn ESP-free branch: the generated draft lands in your own inbox via Gmail instead of (or in addition to) the Kit broadcast. Keep the delivery branch you use and delete the other."
      },
      "typeVersion": 1
    },
    {
      "id": "c4d5e6f0-0002-4b2a-9d11-bb0000000002",
      "name": "When Monday at 5am",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -900,
        180
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 5
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "c4d5e6f0-0003-4b2a-9d11-bb0000000003",
      "name": "Define Source Feeds",
      "type": "n8n-nodes-base.code",
      "position": [
        -680,
        180
      ],
      "parameters": {
        "jsCode": "// ============ EDIT ME: your source feeds ============\n// One line per RSS/Atom feed you want in the digest.\n// These two are live examples so you can run the workflow as-is \u2014\n// replace them with the feeds you actually read.\nconst FEEDS = [\n  'https://hnrss.org/frontpage',\n  'https://www.theverge.com/rss/index.xml',\n];\n// =====================================================\nreturn FEEDS.map((url) => ({ json: { url } }));"
      },
      "typeVersion": 2
    },
    {
      "id": "c4d5e6f0-0004-4b2a-9d11-bb0000000004",
      "name": "Fetch RSS Feeds",
      "type": "n8n-nodes-base.rssFeedRead",
      "position": [
        -460,
        180
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {}
      },
      "typeVersion": 1.1
    },
    {
      "id": "c4d5e6f0-0005-4b2a-9d11-bb0000000005",
      "name": "Filter Recent Stories",
      "type": "n8n-nodes-base.code",
      "position": [
        -180,
        180
      ],
      "parameters": {
        "jsCode": "// Keep only the last 7 days, tidy the fields, cap at 25 stories.\nconst cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;\nconst seen = new Set();\nconst stories = [];\n\nfor (const item of $input.all()) {\n  const j = item.json;\n  const dateStr = j.isoDate ?? j.pubDate ?? null;\n  const ts = dateStr ? Date.parse(dateStr) : NaN;\n  if (Number.isNaN(ts) || ts < cutoff) continue;\n  const link = j.link ?? '';\n  if (!link || seen.has(link)) continue;\n  seen.add(link);\n  const snippet = (j.contentSnippet ?? j.content ?? '')\n    .replace(/<[^>]+>/g, ' ')\n    .replace(/\\s+/g, ' ')\n    .trim()\n    .slice(0, 300);\n  let source = '';\n  try { source = new URL(link).hostname.replace(/^www\\./, ''); } catch {}\n  stories.push({ ts, title: j.title ?? '(untitled)', link, snippet, source });\n}\n\nstories.sort((a, b) => b.ts - a.ts);\nif (stories.length === 0) {\n  throw new Error('No stories in the last 7 days \u2014 check your feed URLs.');\n}\nreturn stories.slice(0, 25).map((s) => ({ json: s }));"
      },
      "typeVersion": 2
    },
    {
      "id": "c4d5e6f0-0006-4b2a-9d11-bb0000000006",
      "name": "Create AI Prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        40,
        180
      ],
      "parameters": {
        "jsCode": "// ============ EDIT ME: newsletter voice ============\nconst NEWSLETTER_NAME = 'My Weekly Digest';\nconst TONE = 'friendly, concise, no hype';\nconst MODEL = 'gpt-4o-mini'; // any chat model your API key can use\n// ====================================================\n\nconst stories = $input.all().map((i) => i.json);\nconst list = stories\n  .map((s, n) => `${n + 1}. [${s.source}] ${s.title}\\n${s.snippet}\\nLink: ${s.link}`)\n  .join('\\n\\n');\n\nconst system = `You draft the weekly email digest for \"${NEWSLETTER_NAME}\". Tone: ${TONE}. You write tight, scannable email copy. You never invent facts beyond the provided stories, and every story keeps its link.`;\n\nconst user = `Draft this week's digest from the stories below.\\n\\nRules:\\n- Group related stories into 3\u20135 themed sections with short <h2> headings.\\n- Each story: 1\u20132 sentences in your own words + an <a href> \"Read more\" link.\\n- Open with a 2-sentence intro of the week; close with one sign-off line.\\n- Output CLEAN HTML ONLY (h2, p, a, ul/li). No <html>/<head>/<body> wrapper, no markdown, no code fences.\\n\\nSTORIES:\\n\\n${list}`;\n\nreturn [{\n  json: {\n    body: {\n      model: MODEL,\n      messages: [\n        { role: 'system', content: system },\n        { role: 'user', content: user },\n      ],\n      temperature: 0.4,\n    },\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c4d5e6f0-0007-4b2a-9d11-bb0000000007",
      "name": "Post to OpenAI API",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        320,
        180
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/chat/completions",
        "method": "POST",
        "options": {
          "timeout": 120000
        },
        "jsonBody": "={{ JSON.stringify($json.body) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "c4d5e6f0-0008-4b2a-9d11-bb0000000008",
      "name": "Build Email HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        540,
        180
      ],
      "parameters": {
        "jsCode": "// Wrap the AI copy in a minimal, email-safe shell.\nlet html = $input.first().json.choices?.[0]?.message?.content ?? '';\nhtml = html.replace(/^```(html)?/m, '').replace(/```\\s*$/m, '').trim();\nif (!html) throw new Error('AI returned no content \u2014 check the previous node.');\n\nconst subject = `Your weekly digest \u2014 ${$now.toFormat('d LLLL')}`;\nconst content = `<div style=\"font-family:Georgia,serif;font-size:16px;line-height:1.6;color:#222;\">${html}</div>`;\n\nreturn [{\n  json: {\n    subject,\n    preview_text: 'The week, in five minutes.',\n    content,\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c4d5e6f0-0009-4b2a-9d11-bb0000000009",
      "name": "Post to Kit API",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        820,
        180
      ],
      "parameters": {
        "url": "https://api.kit.com/v4/broadcasts",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ subject: $json.subject, preview_text: $json.preview_text, content: $json.content, description: 'Auto-drafted by n8n \u2014 review before sending', public: false, send_at: null }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "c4d5e6f0-0010-4b2a-9d11-bb0000000010",
      "name": "Notify Slack",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1040,
        180
      ],
      "parameters": {
        "url": "https://hooks.slack.com/services/PASTE/YOUR/SLACK_WEBHOOK",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "text",
              "value": "=\ud83d\udcf0 This week's newsletter draft is ready in Kit \u2192 Broadcasts. Review, edit, send: https://app.kit.com/campaigns"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "c4d5e6f0-0012-4b2a-9d11-bb0000000012",
      "name": "Send Draft via Gmail",
      "type": "n8n-nodes-base.gmail",
      "position": [
        820,
        620
      ],
      "parameters": {
        "sendTo": "you@example.com",
        "message": "={{ $json.content }}",
        "options": {},
        "subject": "={{ '[DRAFT] ' + $json.subject }}"
      },
      "typeVersion": 2.1
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Fetch RSS Feeds": {
      "main": [
        [
          {
            "node": "Filter Recent Stories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post to Kit API": {
      "main": [
        [
          {
            "node": "Notify Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Email HTML": {
      "main": [
        [
          {
            "node": "Post to Kit API",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send Draft via Gmail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create AI Prompt": {
      "main": [
        [
          {
            "node": "Post to OpenAI API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post to OpenAI API": {
      "main": [
        [
          {
            "node": "Build Email HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Monday at 5am": {
      "main": [
        [
          {
            "node": "Define Source Feeds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Define Source Feeds": {
      "main": [
        [
          {
            "node": "Fetch RSS Feeds",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Recent Stories": {
      "main": [
        [
          {
            "node": "Create AI Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}