Daily RSS and Atom Monitor: Only Items You Have Not Seen avatar

Daily RSS and Atom Monitor: Only Items You Have Not Seen

Pricing

from $50.00 / 1,000 source checkeds

Go to Apify Store
Daily RSS and Atom Monitor: Only Items You Have Not Seen

Daily RSS and Atom Monitor: Only Items You Have Not Seen

Schedule it on your RSS and Atom feeds. The first run returns everything the feeds carry now, as a baseline. Every run after that returns only entries no earlier run returned, plus a summary row. Failed feeds and feeds robots.txt blocks are not charged.

Pricing

from $50.00 / 1,000 source checkeds

Rating

0.0

(0)

Developer

Tarcio Elyakin Agra Diniz

Tarcio Elyakin Agra Diniz

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

RSS Feed Monitor: New Items Only, From Changelogs and Vendor Feeds

You follow a dozen changelog, release, status and supplier feeds, and every week you reread the same entries to work out which ones are actually new.

Give this Actor a list of RSS or Atom feed URLs and every run returns only the new items: the entries it has never handed you before, one row per item. It remembers what it already sent, so the first run gives you the baseline and every run after that is short.

Run it on a schedule (this is the point)

What this Actor sells is the difference between two runs, so one run on its own is half of it. The first run on a feed returns the feed as it stands today: every item the feed carries at that moment, marked first_run: true in the REPORT. It is the run after that which does the job you came for, returning only what appeared since the previous run, which is usually a short list and sometimes an empty one.

So set it to run again by itself. On the Apify platform you schedule the Actor directly, with no task to create first: in Schedules, "Click on the Add dropdown and select whether you want to schedule an Actor or task", pick this Actor, and write the interval as a cron expression with six positions. There is one prerequisite: "To schedule an Actor, you need to have run it at least once before". So press Start once, let that run be the baseline, then schedule it. The platform's floor is that "The minimum interval between runs is 10 seconds"; how often you actually poll is your call and your publisher's. The steps are in the Apify documentation: https://docs.apify.com/platform/schedules

Schedules are not a paid extra. The Apify account limits page lists "Maximum number of schedules per user: 100", and the number is the same in every plan column, including Free: https://docs.apify.com/account/limits

One thing to know about the memory. The list of item ids already returned is not stored inside the Actor: it is a named key-value store record in your own account, keyed by feed URL. It survives between runs and it is yours to read or delete. If that store is deleted, or if you change the feed URL, the next run is a first run again for that feed and hands you everything it carries.

Who runs it, and when

  • Engineering and integration teams watching vendor changelogs and release feeds, to catch the version that breaks an integration;
  • operations and support, keeping a record of incidents on the status feeds of the services they depend on;
  • procurement, legal and policy people, following supplier blogs, standards bodies and regulators whose posts change how they work;
  • anyone who has to be able to say what changed last week and does not want to read every feed daily.

The usual moment is a weekly or nightly schedule on the Apify platform: the run writes the new items to a dataset, and you connect that dataset to whatever you already read.

What comes out, field by field

The dataset has two kinds of row, told apart by the rowType field: feed-item for a new item, and summary for the single run summary row that every successful run writes, new items or none. Filter on rowType to keep only the items.

One feed-item row per new item. Besides rowType, an item row has exactly these seven fields, and nothing else; the same names are declared in .actor/dataset_schema.json and checked by tests/test_schemas.py.

fieldtypewhat it holds
source_urlstringthe feed this item came from, as given in the input
source_titlestringthe title the feed publishes for itself, empty when it publishes none
item_idstringthe identity of the item: the feed's guid or id, the link when there is none, and a sha1: hash of feed URL, title and date when there is neither. This is the value the Actor remembers between runs
item_titlestringthe item title, as plain text
item_linkstringthe item URL on the publisher's site, empty when the feed gives none
item_published_atstringthe publication date in UTC, YYYY-MM-DDTHH:MM:SSZ. A date the parser cannot read is kept as the feed wrote it
item_summarystringthe short description the feed itself publishes (description in RSS, summary in Atom), as plain text, cut at 600 characters

The summary row carries the counts of the run: runStartedAt, finishedAt, sourcesGiven, sourcesRead, sourcesFailed, sourcesSkipped, sourcesSkippedByRobots, newItems, chargedEvents, chargeLimitReached, chargeFailures and a one-sentence message. It exists so a quiet run does not look like a crash: an empty dataset and a broken Actor read the same, a dataset with one summary row saying "no new items" does not.

Every run also writes a REPORT record in the key-value store with the same counts plus robotsFetches and a perSource list (source_url, source_title, status, error, items_in_feed, new_items, first_run).

items_in_feed next to new_items is the part that matters when the answer is "nothing new": it shows the feed was really read and was really quiet, instead of leaving you to guess whether the run failed.

When a feed is not read

status in perSource is one of four values, and the last three mean this run says nothing about whether that feed changed:

statuswhat happenedcharged
okfetched, parsed and comparedyes, one source-checked
failedthe reason is in errorno
skipped_by_robotsthe host's robots.txt disallows this Actor's user agent, so nothing was fetchedno
skippedthe run had already hit its pay-per-event charge limitno

A feed is failed when any of these happens: the host answers an HTTP error status (4xx or 5xx), the request times out or the connection fails, the body is empty, the body is not valid XML, or the XML root element is not a feed. The failure is written to the log and to the report, the other feeds in the list keep going, and nothing is charged for it.

One limit worth knowing: the Actor does not check the Content-Type header. It tries to parse whatever the URL returns. A page that is valid XML but is not a feed is rejected by the root-element check, and an HTML page is normally rejected as invalid XML, but the decision is made by the parser, not by the content type the server declares.

Real output

The row below is line 21 of logs/corrida-local-2026-09-20-run2.log in this repository: an end-to-end run of src/main.py, the second run of a pair, with the feeds served from tests/fixtures/run2. Between the two runs one post was added to the blog feed and nothing changed in the status feed. The blog feed carried four items at that moment, and this is the only row the run returned:

{"item_id": "post-release-2-2", "item_link": "http://127.0.0.1:8099/posts/release-2-2", "item_published_at": "2026-09-20T09:15:00Z", "item_summary": "Exported timestamps are now written in UTC instead of the browser timezone.", "item_title": "Release 2.2 fixes the timezone of the export", "source_title": "Example Machines Blog", "source_url": "http://127.0.0.1:8099/blog.xml"}

The REPORT record of that same run, lines 23 to 53 of the same log:

{
"runStartedAt": "2026-09-20T06:00:00Z",
"sourcesGiven": 2,
"sourcesRead": 2,
"sourcesFailed": 0,
"newItems": 1,
"perSource": [
{"source_url": "http://127.0.0.1:8099/blog.xml", "source_title": "Example Machines Blog", "status": "ok", "error": "", "items_in_feed": 4, "new_items": 1, "first_run": false},
{"source_url": "http://127.0.0.1:8099/status.atom", "source_title": "Example Machines Status", "status": "ok", "error": "", "items_in_feed": 2, "new_items": 0, "first_run": false}
],
"chargedEvents": 0,
"chargeLimitReached": false,
"chargeFailures": 0
}

The first run of the same pair, in logs/corrida-local-2026-09-20-run1.log, returned five rows: everything the two feeds carried. That is the baseline run.

Both logs were recorded before the rowType field and the summary row were added. The same pair of runs today returns the same item rows, each with "rowType": "feed-item", plus one summary row per run.

Input

The example below is the input this Actor is prefilled with, so you can press Start and read a real result before pointing it at your own feeds.

{
"feedUrls": ["https://news.ycombinator.com/rss"],
"maxSources": 20,
"requestDelaySeconds": 2,
"requestTimeoutSeconds": 20
}
fieldtyperequireddefaultrange
feedUrlsarray of feed URLsyes[]RSS 2.0, RSS 1.0 or Atom
maxSourcesintegerno201 to 500 feeds read in this run
requestDelaySecondsintegerno20 to 60 seconds between two feed requests
requestTimeoutSecondsintegerno203 to 120 seconds before a feed is reported as failed

Notes on the input, as the code handles it:

  • a URL without http:// or https:// gets https:// added, and the same URL given twice is read once;
  • entries may also be objects with a url key, which is what the Apify "Link list" style input produces;
  • maxSources cuts the list for this run: feeds beyond that number are simply not read, and the run says so in the log;
  • the first request does not wait; the delay applies between requests.

The list of item ids already returned is kept per feed URL in a named key-value store that belongs to your account and survives between runs. Up to 5000 ids are kept per feed, oldest dropped first. Change a feed URL and the next run treats it as a first run and returns everything again.

What this Actor does not do

  • It does not open the item link, so it never returns the full article text. You get the summary the feed publishes, and a feed that publishes only a title gives you only a title.
  • It does not watch pages that have no feed, and it does not build a feed out of an HTML page.
  • It does not detect an edit to an item it already sent. A changed item is not a new item here.
  • It does not send e-mail, Slack or any other notification. It writes a dataset, and you connect that to whatever you already use.
  • It does not filter by keyword, tag or author, and it does not rank, translate or summarise anything.
  • It does not run on a schedule by itself. You set the schedule on the Apify platform.
  • It does not deduplicate across feeds: the same post published in two feeds comes back once per feed.
  • It does not get past a login, a paywall or a captcha, and it does not run JavaScript.
  • It does not return who wrote an item. The parser has an allow list of seven fields (ALLOWED_FIELDS in src/feed_parser.py) and nothing else reaches the dataset: author elements, dc:creator, managingEditor, webMaster, contributor, name, uri and email are never read, and any value that still contains an "@" after cleaning is dropped.

Manners, robots.txt and your responsibility

  • A feed is fetched only when you list it. This Actor makes one HTTP GET per feed URL in your input, follows redirects, reads at most 10 MB, and never crawls, follows links or discovers feeds on its own.
  • It identifies itself on every request as FeedChangeWatcher/0.1 (Apify Actor; +https://apify.com/lotebo-lab/feed-change-watcher), and keeps requestDelaySeconds between requests so the servers you follow are not hit hard.
  • It reads robots.txt before it reads a feed. One robots.txt per host, fetched once per run and reused, asked with the same user agent the feed request sends. An explicit Disallow that matches that user agent skips the feed: nothing is fetched, nothing is charged, and the feed appears as skipped_by_robots in the report. A robots.txt that cannot be read, because of a 404, a server error or a timeout, states no restriction, so the feed is fetched and the log says so.
  • robots.txt is not the whole answer. You are still responsible for the terms of each site you list: a feed can be open in robots.txt and closed to automated clients by the site's terms or by your agreement with it.
  • You are responsible for having the right to read every feed you list. Check the terms of the sites that publish them, and check whether your own agreement with them allows automated access.

Price

Pay per event, two events, exactly as declared in .actor/actor.json:

eventpricewhen it is charged
source-checkedUS$ 0.05once per feed that was fetched, parsed and compared. A feed that failed to load, that robots.txt disallowed, or that was skipped after the charge limit, is not charged
change-reportUS$ 0.50once per run, and only when the run found at least one new item

A run that finds nothing new costs only the feeds it read. Watching a changelog means most runs have nothing to report, and you are not charged for the report on those runs: ten feeds with no new item cost ten source-checked events and nothing else. Ten feeds with at least one new item cost ten source-checked events plus one change-report. The summary row is written to the dataset either way, free. The first run of a feed is charged like any other read, and it returns everything the feed carries. Apify charges its own Actor start event and the platform usage of the run on top of this; those are not set by this Actor.

About this Actor

The code, the tests and the run logs quoted here are in this repository. Parsing uses the Python standard library, not feedparser, so there is no extra dependency to break, and a failing feed never stops the run: it is reported and the other feeds keep going. The Actor is written in Python and was built with the help of AI.

Example tasks

Each page below is a published example task of this Actor. It shows the input used and the fields the run returns. The same page is served as Markdown by adding .md to the URL.