A daily competitor digest from RSS feeds, summarized into Slack

Build an n8n workflow that reads competitors' blog and changelog feeds each morning, keeps only new posts, and posts a short LLM-written digest to Slack.

· ranx.cloud

Illustration of broadcast arcs from distant towers converging into a single folded note

This is for a marketer or product marketer who is supposed to "keep an eye on competitors" and does it in bursts when someone asks. The workflow below reads a list of RSS feeds every weekday morning (competitor blogs, changelogs, release notes, a couple of industry news sources), drops anything it has already seen, and posts one Slack message with a few lines per new item and a short "worth a look" section on top. On a quiet day it posts nothing.

Schedule Trigger → Code (feed list) → RSS Read → Edit Fields (add source) → Filter (last 3 days) → Remove Duplicates
  → Aggregate → Basic LLM Chain (+ chat model) → Slack

LLM spend for this is a few cents a month. The arithmetic is near the end.

1. Find the feeds

Most blogs still publish RSS, even when they don't link to it. Try, in order:

  • /feed, /rss, /rss.xml, /feed.xml, /atom.xml, /blog/rss.xml on the blog's domain.
  • View the page source of the blog index and search for application/rss+xml.
  • Changelogs: many product changelog tools publish a feed; look for an RSS icon on the changelog page.
  • GitHub releases of an open-source competitor: https://github.com/<org>/<repo>/releases.atom.

Open each URL in a browser before adding it. If you get HTML instead of XML, it's not a feed.

2. Schedule Trigger

Trigger Interval:       Weeks
Weeks Between Triggers: 1
Trigger on Weekdays:    Monday, Tuesday, Wednesday, Thursday, Friday
Trigger at Hour:        8am
Trigger at Minute:      0

The schedule uses the workflow's timezone. Set it under the workflow's Settings → Timezone; otherwise it uses the instance default, which is America/New_York unless whoever runs the instance changed it.

Then open the trigger's Settings tab and set:

If Execution Is Missed:                   Run the Most Recent Missed Execution
Missed Execution Grace Period (Seconds):  3600

These two options exist on Schedule Trigger nodes added in n8n 2.36 or later, and only matter if n8n was down at 8:00. The default, Don't Run Missed Executions, skips the morning's digest entirely if the instance was restarting at the scheduled minute. With the setting above, a restart at 7:59 means the digest arrives a few minutes late instead of not at all. n8n's durable scheduler docs explain how missed runs are recorded and replayed (it's on by default from 2.45).

If you run this on ranx.cloud, this setting is worth having: an instance can be restarted now and then, for example for maintenance or updates, so occasionally it won't be up at the exact scheduled minute.

3. Code: the feed list

A Code node (JavaScript) that returns one item per feed keeps the list in one place:

const feeds = [
  { name: 'Acme blog',      url: 'https://acme.example/blog/rss.xml' },
  { name: 'Acme changelog', url: 'https://acme.example/changelog/feed' },
  { name: 'Globex',         url: 'https://globex.example/feed' },
  { name: 'Initech releases', url: 'https://github.com/initech/initech/releases.atom' },
];
return feeds.map(f => ({ json: f }));

4. RSS Read

URL: {{ $json.url }}

The node runs once per input item, so four feeds means four fetches. Each output item has title, link, isoDate, contentSnippet and creator where the feed provides them.

In the node's Settings, set On Error to Continue. One competitor's broken feed shouldn't cancel everyone else's digest. Also turn on Retry On Fail (2 tries, 5 seconds apart) for feeds behind flaky CDNs.

RSS Read's output doesn't say which feed a post came from. Add an Edit Fields (Set) node after it with one field, Include Other Input Fields on:

source (String): {{ $('Code').item.json.name }}

5. Filter: only recent items

The first time the workflow runs, every feed returns its whole history, often 10 to 50 posts each. A Filter node limits that:

{{ DateTime.fromISO($json.isoDate) > $now.minus({ days: 3 }) }}   is true

Three days covers a Monday run after a weekend. Items without isoDate are dropped by this filter; if one of your feeds lacks dates, the dedupe step below still protects you, so you can relax the condition to {{ !$json.isoDate || DateTime.fromISO($json.isoDate) > $now.minus({ days: 3 }) }}.

6. Remove Duplicates

Operation:          Remove Items Processed in Previous Executions
Keep Items Where:   Value Is New
Value to Dedupe On: {{ $json.link }}

This remembers every link it has passed through (the last 10,000 by default) across runs, so a post that stays in the three-day window for three mornings is reported once. Use the link rather than the title: titles get edited, links rarely do.

Don't use the Value Is a Date Later than Any Previous Date mode here. With several feeds in one node it keeps a single high-water mark, so a slow feed whose newest post is older than another feed's newest post would never get through.

7. Aggregate

If nothing new survived the filters, there are no items and the workflow stops here, so quiet days produce no message. Otherwise collect everything into one item:

Aggregate:          All Item Data (Into a Single List)
Put Output in Field: items

8. Basic LLM Chain

Add Basic LLM Chain with a chat model attached (Anthropic Chat Model with a Haiku model, or OpenAI Chat Model with a small model). Get the API key from console.anthropic.com → API Keys or platform.openai.com, and add it as an n8n credential.

Source for Prompt (User Message): Define below

Prompt (User Message):

You write a short daily competitor digest for a B2B SaaS marketing team.
Our product: <one sentence about what you sell and to whom>.

New posts since yesterday, as JSON:
{{ JSON.stringify($json.items.map(i => ({
  source: i.source,
  title: i.title,
  link: i.link,
  date: i.isoDate,
  summary: (i.contentSnippet || '').slice(0, 600)
}))) }}

Write Slack mrkdwn, no headings with #:
1. "*Worth a look*": at most 3 bullets for posts that announce pricing changes, new features that overlap with ours, funding, acquisitions, or new target markets. One sentence each on why it matters to us. Skip this section if nothing qualifies.
2. "*Everything new*": one bullet per post: <link|title> (source) – a factual one-line summary.
Do not invent details that are not in the summary. Keep the whole message under 2,500 characters.

Telling the model the 600-character snippet is all it has, and not to add details, matters. RSS summaries are often a teaser paragraph, and small models will otherwise fill the gap from their general knowledge of the company.

9. Slack

Slack node, Message → Send, to #competitors, Simple Text Message:

{{ $json.text }}

The Basic LLM Chain puts the model's answer in $json.text. Under Options, turn off Include Link to Workflow.

For the Slack credential: api.slack.com/apps → Create New App → From scratch, add the chat:write bot token scope under OAuth & Permissions, install to the workspace, paste the xoxb- token into a Slack API credential in n8n, and /invite the bot into the channel.

10. Test and publish

Click Execute workflow once. The first run is your chance to tune the prompt on real data; the Remove Duplicates node remembers what it saw in manual runs too, so if you want the next run to show the same items again, run the node once with Operation set to Clear Deduplication History, then set it back. Then Publish.

Costs

Prices as of November 2026 from Anthropic's pricing page: Claude Haiku 5.5 costs $0.10 per million input tokens and $0.50 per million output tokens (prompts up to 100,000 tokens); Claude Sonnet 5.5 costs $2 and $10.

Assume 15 feeds and a busy day with 40 new posts. Each post in the prompt is roughly 200 tokens (title, link, date, 600-character snippet), plus about 400 tokens of instructions: about 8,400 input tokens. The digest itself is about 700 tokens, and allowing for thinking tokens, budget 1,200 output tokens.

Haiku 5.5:  8,400 × $0.10/1M + 1,200 × $0.50/1M = $0.00084 + $0.00060 = $0.00144 per digest
            22 weekdays ≈ $0.03 / month
Sonnet 5.5: 8,400 × $2/1M + 1,200 × $10/1M = $0.0168 + $0.0120 = $0.0288 per digest
            22 weekdays ≈ $0.63 / month

Either is cheap enough that model choice should come down to which writes the more useful "worth a look" section. Try both for a week.

There are no API quotas on RSS itself, but be polite: once a day per feed is fine, every minute is not, and some sites rate-limit or block aggressive fetchers.

What goes wrong

A feed returns HTML or a 403. Some sites sit behind bot protection that blocks server-side fetches. With On Error: Continue, the RSS Read output for that feed carries an error field instead of posts; the Filter node drops it. Check the RSS Read output occasionally, or add an If node that sends errors to a separate Slack message.

The first digest is enormous. The date filter wasn't in place, or the feed has no dates. Clear the dedupe history, fix the filter, run again.

The same post appears twice. Some feeds put tracking parameters in links (?utm_source=rss), which change. Dedupe on {{ $json.link.split('?')[0] }} instead.

The digest invents things. It happens when the snippet is short and the instructions are loose. Keep "do not invent details" in the prompt and keep the per-post summary request to "factual one-line summary". If a source only publishes titles, the model should only restate the title.

The digest didn't arrive this morning. Look at Executions. No execution at 8:00 means the schedule was missed (instance down or restarting); see the missed-execution setting in step 2. A failed execution means one of the nodes errored; set an error workflow (Settings → Error Workflow) so you get told instead of noticing at lunch. The backups and error alerts post sets one up.

Slack shows <link|title> literally. The Slack node's message isn't being sent as mrkdwn, or the model wrapped the output in a code block. Tell it "Do not wrap the message in a code block" and check the message type.

Variations

  • Watch pages that have no feed. Use HTTP Request to fetch a pricing page daily, store a hash of the relevant section, and only call the LLM when it changes.
  • Weekly instead of daily. Change the schedule to Mondays and the filter window to 8 days, and ask for a "themes this week" paragraph on top.