{
  "id": "mnJRUxcvZ7yC5njR",
  "name": "Summarize an RSS feed with Claude and post the digest to Slack",
  "tags": [],
  "nodes": [
    {
      "id": "pub003-sticky-main",
      "name": "Sticky Note: Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -896,
        -1136
      ],
      "parameters": {
        "width": 3000,
        "height": 520,
        "content": "## RSS digest with Claude \u2014 scheduled fetch, summarize, deliver\n\nOn a schedule, this workflow fetches an RSS feed, asks Claude to write a short digest of the latest items, and posts that digest to Slack. Useful for a daily/weekly \"what's new\" summary of any blog, news source, or changelog that publishes an RSS feed.\n\n### Before you activate\n1. Add an Anthropic API key as a credential in n8n (Credential type: Anthropic), and assign it in the \"Call Claude (Anthropic) API\" node.\n2. Open the \"Set: feed URL & Slack webhook\" node and paste the RSS feed URL you want summarized, and your Slack Incoming Webhook URL (no Slack app/OAuth needed).\n3. Adjust the Schedule Trigger to whatever cadence you want (defaults to daily).\n\n### Known limitation\nThe RSS parsing step is written for standard RSS 2.0 feeds (`<rss><channel><item>`). Atom feeds or heavily non-standard RSS use a different structure and may need the \"Extract feed items\" node adjusted."
      },
      "typeVersion": 1
    },
    {
      "id": "pub003-sticky-step1",
      "name": "Sticky Note: Step 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -896,
        -528
      ],
      "parameters": {
        "width": 650,
        "height": 220,
        "content": "Step 1 \u2014 Fetch & parse the feed\n\nOn a schedule, downloads the RSS feed as XML, converts it to JSON, and extracts the latest items (title, link, publish date, summary) into a clean list, capped at 10 items."
      },
      "typeVersion": 1
    },
    {
      "id": "pub003-sticky-step2",
      "name": "Sticky Note: Step 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -144,
        -528
      ],
      "parameters": {
        "width": 650,
        "height": 220,
        "content": "Step 2 \u2014 Summarize with Claude\n\nSends the extracted items to the Anthropic Claude Messages API with instructions to write a short, readable digest, with automatic retries on transient failures."
      },
      "typeVersion": 1
    },
    {
      "id": "pub003-sticky-step3",
      "name": "Sticky Note: Step 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        656,
        -528
      ],
      "parameters": {
        "width": 650,
        "height": 220,
        "content": "Step 3 \u2014 Deliver to Slack\n\nPosts the digest to Slack via an Incoming Webhook. Checks the actual response body (Slack returns the text \"ok\" on success) rather than just the absence of an error, so a misconfigured webhook is never mistaken for a successful send."
      },
      "typeVersion": 1
    },
    {
      "id": "pub003-schedule",
      "name": "Schedule Trigger (daily)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -800,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 24
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "pub003-set-config",
      "name": "Set: feed URL & Slack webhook",
      "type": "n8n-nodes-base.set",
      "notes": "Placeholder values. Paste the RSS feed URL you want summarized, and your own Slack Incoming Webhook URL, before activating.",
      "position": [
        -560,
        0
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-feed-url",
              "name": "feedUrl",
              "type": "string",
              "value": "https://blog.n8n.io/rss/"
            },
            {
              "id": "cfg-slack-url",
              "name": "slackWebhookUrl",
              "type": "string",
              "value": "https://hooks.slack.com/services/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"
            },
            {
              "id": "cfg-max-items",
              "name": "maxItems",
              "type": "number",
              "value": 10
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "pub003-fetch-feed",
      "name": "Fetch RSS feed",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Sends explicit Accept and User-Agent headers because some RSS feeds return HTTP 406 to requests that look non-browser-like (a known issue with n8n's built-in RSS Read node on certain feeds) \u2014 using HTTP Request directly avoids depending on that node.",
      "maxTries": 3,
      "position": [
        -320,
        0
      ],
      "parameters": {
        "url": "={{ $json.feedUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/rss+xml, application/xml;q=0.9, text/xml;q=0.8, */*;q=0.7"
            },
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (compatible; n8n-rss-digest-template/1.0)"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2,
      "continueOnFail": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "pub003-if-fetch-ok",
      "name": "Feed fetched OK?",
      "type": "n8n-nodes-base.if",
      "position": [
        -80,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "pub003-cond-fetch-ok",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.error ? 'error' : 'ok' }}",
              "rightValue": "ok"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "pub003-xml-to-json",
      "name": "Convert XML to JSON",
      "type": "n8n-nodes-base.xml",
      "position": [
        160,
        -144
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "pub003-extract-items",
      "name": "Extract feed items",
      "type": "n8n-nodes-base.code",
      "notes": "Written for standard RSS 2.0 (<rss><channel><item>...). See the Overview sticky note for the Atom-feed limitation. Field shapes below (no array-wrapping on singular elements, top-level output not nested under \"data\") were confirmed against a real feed (blog.n8n.io/rss/) via n8n's XML node output, 2026-08-12 \u2014 n8n's XML-to-JSON conversion does not array-wrap single child elements the way raw xml2js defaults do.",
      "position": [
        400,
        -144
      ],
      "parameters": {
        "jsCode": "const parsed = $input.first().json;\nconst config = $('Set: feed URL & Slack webhook').first().json;\n\nconst channel = parsed && parsed.rss && parsed.rss.channel;\nif (!channel) {\n  throw new Error('Extract feed items: could not find rss.channel in the parsed feed. This workflow expects a standard RSS 2.0 feed (see the Overview sticky note for the Atom limitation).');\n}\n\nconst feedTitle = channel.title || 'Untitled feed';\nconst rawItems = Array.isArray(channel.item) ? channel.item : (channel.item ? [channel.item] : []);\nconst maxItems = Number.isFinite(config.maxItems) ? config.maxItems : 10;\n\nconst items = rawItems.slice(0, maxItems).map((item) => ({\n  title: item.title || '(no title)',\n  link: item.link || '',\n  pubDate: item.pubDate || '',\n  summary: item.description || ''\n}));\n\nif (items.length === 0) {\n  throw new Error('Extract feed items: the feed was parsed but contained no <item> entries.');\n}\n\nreturn [{ json: { feedTitle, items, slackWebhookUrl: config.slackWebhookUrl } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "pub003-build-prompt",
      "name": "Build Claude prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        640,
        -144
      ],
      "parameters": {
        "jsCode": "const d = $input.first().json;\n\nconst systemPrompt = 'You write short, factual digests of RSS feed items. Only summarize what is actually present in the provided items \u2014 do not invent facts, dates, or details not present in the source data. If an item has no summary text, say so rather than guessing its content.';\n\nconst itemsText = d.items.map((it, i) => `${i + 1}. ${it.title}${it.pubDate ? ' (' + it.pubDate + ')' : ''}\\n${it.summary || '(no summary provided)'}\\n${it.link}`).join('\\n\\n');\n\nconst userPrompt = `Write a short digest (a few bullet points) of the latest items from \"${d.feedTitle}\". Use only the information below \u2014 do not add facts that aren't here.\\n\\n${itemsText}`;\n\nreturn [{ json: { systemPrompt, userPrompt, slackWebhookUrl: d.slackWebhookUrl, feedTitle: d.feedTitle } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "pub003-call-claude",
      "name": "Call Claude (Anthropic) API",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "No credential ID is embedded \u2014 assign your own Anthropic credential in n8n before activating.",
      "maxTries": 3,
      "position": [
        880,
        -144
      ],
      "parameters": {
        "url": "https://api.anthropic.com/v1/messages",
        "method": "POST",
        "options": {
          "timeout": 60000
        },
        "jsonBody": "={{ { model: 'claude-sonnet-5', max_tokens: 1024, system: $json.systemPrompt, messages: [ { role: 'user', content: $json.userPrompt } ] } }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        },
        "nodeCredentialType": "anthropicApi"
      },
      "credentials": {
        "anthropicApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.2,
      "continueOnFail": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "pub003-if-claude-ok",
      "name": "Claude call succeeded?",
      "type": "n8n-nodes-base.if",
      "position": [
        1120,
        -144
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "pub003-cond-claude-ok",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.error ? 'error' : 'ok' }}",
              "rightValue": "ok"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "pub003-build-slack",
      "name": "Build Slack message",
      "type": "n8n-nodes-base.code",
      "position": [
        1408,
        -288
      ],
      "parameters": {
        "jsCode": "const resp = $input.first().json;\nconst promptCtx = $('Build Claude prompt').first().json;\n\nconst textBlock = (resp.content || []).find((b) => b.type === 'text');\nconst digest = (textBlock && textBlock.text) || '(no digest text returned)';\n\nreturn [{\n  json: {\n    slackWebhookUrl: promptCtx.slackWebhookUrl,\n    slackRequestBody: { text: `:newspaper: *${promptCtx.feedTitle} \u2014 digest*\\n${digest}` }\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "pub003-notify-slack",
      "name": "Post digest to Slack",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1600,
        -288
      ],
      "parameters": {
        "url": "={{ $json.slackWebhookUrl }}",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ $json.slackRequestBody }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "pub003-if-slack-ok",
      "name": "Slack post succeeded?",
      "type": "n8n-nodes-base.if",
      "notes": "Checks the response body equals \"ok\" (Slack's documented literal success response) rather than just the absence of a thrown error \u2014 a lesson learned the hard way in an earlier template (PUB-002): an invalid webhook URL can return HTTP 200 with an unrelated HTML page instead of a clean error.",
      "position": [
        1840,
        -288
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "pub003-cond-slack-ok",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.error ? 'error' : $json.data }}",
              "rightValue": "ok"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "pub003-noop-done",
      "name": "Done: digest posted",
      "type": "n8n-nodes-base.noOp",
      "position": [
        2080,
        -288
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "pub003-throw-slack-error",
      "name": "Error: Slack post failed",
      "type": "n8n-nodes-base.code",
      "position": [
        2080,
        0
      ],
      "parameters": {
        "jsCode": "const d = $input.first().json;\nthrow new Error('PUB-003: posting the digest to Slack failed or the webhook URL is misconfigured. detail=' + JSON.stringify(d).slice(0, 500));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "pub003-throw-claude-error",
      "name": "Error: Claude call failed",
      "type": "n8n-nodes-base.code",
      "position": [
        1360,
        64
      ],
      "parameters": {
        "jsCode": "const err = $input.first().json.error || $input.first().json;\nthrow new Error('PUB-003: Claude API call failed after retries. detail=' + (typeof err === 'string' ? err : JSON.stringify(err)));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "pub003-throw-fetch-error",
      "name": "Error: feed fetch failed",
      "type": "n8n-nodes-base.code",
      "position": [
        160,
        144
      ],
      "parameters": {
        "jsCode": "const err = $input.first().json.error || $input.first().json;\nthrow new Error('PUB-003: fetching the RSS feed failed after retries. Check the feedUrl value in \"Set: feed URL & Slack webhook\". detail=' + (typeof err === 'string' ? err : JSON.stringify(err)));\n"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "33cc36b1-00d5-471c-8dfa-18234366d472",
  "nodeGroups": [],
  "connections": {
    "Fetch RSS feed": {
      "main": [
        [
          {
            "node": "Feed fetched OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Feed fetched OK?": {
      "main": [
        [
          {
            "node": "Convert XML to JSON",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error: feed fetch failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract feed items": {
      "main": [
        [
          {
            "node": "Build Claude prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Claude prompt": {
      "main": [
        [
          {
            "node": "Call Claude (Anthropic) API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Slack message": {
      "main": [
        [
          {
            "node": "Post digest to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert XML to JSON": {
      "main": [
        [
          {
            "node": "Extract feed items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post digest to Slack": {
      "main": [
        [
          {
            "node": "Slack post succeeded?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack post succeeded?": {
      "main": [
        [
          {
            "node": "Done: digest posted",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error: Slack post failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude call succeeded?": {
      "main": [
        [
          {
            "node": "Build Slack message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error: Claude call failed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger (daily)": {
      "main": [
        [
          {
            "node": "Set: feed URL & Slack webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Claude (Anthropic) API": {
      "main": [
        [
          {
            "node": "Claude call succeeded?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set: feed URL & Slack webhook": {
      "main": [
        [
          {
            "node": "Fetch RSS feed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}