RSS & Atom Feed Monitor — Change Detection API avatar

RSS & Atom Feed Monitor — Change Detection API

Pricing

from $2.00 / 1,000 feed checkeds

Go to Apify Store
RSS & Atom Feed Monitor — Change Detection API

RSS & Atom Feed Monitor — Change Detection API

Monitor up to 100 RSS, Atom, or RDF feeds and emit only baseline, new, updated, or failed records. Persist state across scheduled runs and send deduplicated changes to n8n, Make, webhooks, APIs, dashboards, or AI agents—without maintaining your own polling database.

Pricing

from $2.00 / 1,000 feed checkeds

Rating

0.0

(0)

Developer

Ilya Kalesnichenka

Ilya Kalesnichenka

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

2 days ago

Last modified

Share

Monitor RSS, Atom, and RDF feeds through a stateful change detection API. Get a baseline when each feed is first seen, then send only new, updated, or failed records to n8n, Make, webhooks, dashboards, or AI agents.

Why this Actor?

  • Stop sending the same unchanged posts through every automation run.
  • Build scheduled monitoring for AI agents, n8n, Make, APIs, and webhook-driven workflows.
  • Check up to 100 feeds in one run with bounded concurrency.
  • Normalize RSS 2.0, Atom 1.0, and RSS 1.0/RDF into one Dataset contract.
  • Reuse HTTP validators and durable state instead of maintaining a separate deduplication database.

This is a stateful delta monitor, not a generic feed parser. Configure a Task and schedule in Apify, or trigger the Actor from your own automation, to turn recurring feed checks into useful changes.

Quick start

Use the same monitorId on every run that belongs to one logical monitor:

{
"feeds": [
"https://blog.apify.com/rss/",
"https://www.cisa.gov/cybersecurity-advisories/all.xml"
],
"monitorId": "operations-watch",
"bootstrapMode": "emit_current",
"maxItemsPerFeed": 20
}

The default emit_current mode writes current items as baseline rows the first time each feed is seen under a monitorId. Later checks of an already-seen feed write only new, updated, or feed_error rows. Adding a feed to an existing explicit monitor can therefore emit that feed's baseline on a later Actor run.

Ready-to-run examples

Start from a copied public Task and then replace the feeds or stable monitorId for your workflow:

Each page creates your own editable Task copy; it does not modify the publisher's Task.

How state works

The Actor stores per-monitor, per-feed state in the named Apify Key-Value Store rss-feed-delta-monitor-state. State includes conditional HTTP validators, item fingerprints, timestamps, and durable pending changes.

  • With an explicit monitorId, you can add or remove feeds while keeping state for unchanged URLs.
  • Without monitorId, the Actor derives a stable ID from the sorted normalized feed list. Changing that list creates a new baseline.
  • Do not overlap runs that use the same monitorId. Concurrent same-monitor runs are not supported in v0.1.
  • Each feed retains at most 5,000 item identities; old identities are pruned without producing deletion events.

The Actor does not create Tasks, schedules, or webhooks. Configure those separately in Apify Console or in your own automation.

Input

FieldTypeRequiredDefaultLimits and behavior
feedsstring[]Yes1–100 unique public http: or https: feed URLs; credentials in URLs are rejected
monitorIdstringNoDerived from the normalized feed list1–80 characters when provided
bootstrapModeemit_current | baseline_onlyNoemit_currentemit_current emits free baseline rows; baseline_only saves state without baseline rows
maxItemsPerFeedintegerNo201–100 entries parsed per feed per run

Unknown input fields are rejected.

Dataset output

Every row has recordType, monitorId, feedUrl, and detectedAt. Baseline and change rows also have a stable changeId.

Baseline

{
"recordType": "baseline",
"changeId": "6c01e9f0d6d6...",
"monitorId": "operations-watch",
"feedUrl": "https://example.com/feed.xml",
"feedTitle": "Example feed",
"itemId": "https://example.com/posts/123",
"title": "Existing release",
"link": "https://example.com/posts/123",
"summary": "Release summary",
"content": "Release content",
"author": "Example team",
"categories": ["releases"],
"publishedAt": "2026-07-16T08:00:00.000Z",
"updatedAt": null,
"detectedAt": "2026-07-16T10:00:00.000Z"
}

New item

{
"recordType": "new",
"changeId": "141736d6285d...",
"monitorId": "operations-watch",
"feedUrl": "https://example.com/feed.xml",
"feedTitle": "Example feed",
"itemId": "https://example.com/posts/124",
"title": "New release",
"link": "https://example.com/posts/124",
"summary": "New release summary",
"content": null,
"author": "Example team",
"categories": ["releases"],
"publishedAt": "2026-07-16T11:00:00.000Z",
"updatedAt": null,
"detectedAt": "2026-07-16T12:00:00.000Z"
}

Updated item

{
"recordType": "updated",
"changeId": "990642cd0e87...",
"monitorId": "operations-watch",
"feedUrl": "https://example.com/feed.xml",
"feedTitle": "Example feed",
"itemId": "https://example.com/posts/123",
"title": "Existing release — corrected",
"link": "https://example.com/posts/123",
"summary": "Corrected release summary",
"content": "Corrected release content",
"author": "Example team",
"categories": ["releases"],
"publishedAt": "2026-07-16T08:00:00.000Z",
"updatedAt": "2026-07-16T11:30:00.000Z",
"detectedAt": "2026-07-16T12:00:00.000Z"
}

Feed error

{
"recordType": "feed_error",
"monitorId": "operations-watch",
"feedUrl": "https://example.com/broken.xml",
"detectedAt": "2026-07-16T12:00:00.000Z",
"errorCode": "INVALID_XML",
"errorMessage": "https://example.com/broken.xml: Feed XML is malformed.",
"retryable": false,
"httpStatus": 200
}

A feed error is isolated: other feeds continue. changeId is intentionally absent from feed_error rows.

The OUTPUT Key-Value Store record summarizes the run:

{
"monitorId": "operations-watch",
"requestedFeeds": 2,
"checkedFeeds": 2,
"unchangedFeeds": 1,
"failedFeeds": 0,
"skippedFeeds": 0,
"baselineItems": 0,
"newItems": 1,
"updatedItems": 0,
"pendingItems": 0,
"spendingLimitReached": false,
"chargedEvents": {
"feedChecked": 2,
"changeDetected": 1
},
"startedAt": "2026-07-16T12:00:00.000Z",
"finishedAt": "2026-07-16T12:00:01.000Z"
}

Pricing

The Actor uses Pay Per Event with three transparent prices:

EventPriceWhen it applies
Actor start (apify-actor-start)$0.00005One synthetic platform event when a run starts
Successful feed check (feed-checked)$0.002A feed returns a recognized 200 response or a valid 304
Emitted post-baseline change (change-detected)$0.0005Each new or updated row emitted after baseline

Failed feeds do not receive a successful-feed-check charge. baseline and feed_error rows do not receive a change charge, and the apify-default-dataset-item event is not priced. A successful first-run fetch can still receive its feed-check charge.

Spending limits and pending changes

Set an Apify maximum total charge for a run when you need a hard spend ceiling. The Actor checks paid-event capacity before each operation.

If the limit is reached, the Actor preserves confirmed work and keeps undispatched changes as pending state. The next run drains those pending changes before making another HTTP request for that feed. Check spendingLimitReached and pendingItems in OUTPUT to detect a partial run.

Delivery guarantee

Delivery is at least once, not exactly once.

The Dataset write, PPE charge, and Key-Value Store commit are not one atomic transaction. If a Dataset write and its change-detected charge succeed but the run crashes before the KVS commit, the next run can emit the same row and charge again. The repeated row has the same stable changeId. Deduplicate downstream on changeId when duplicate side effects matter.

This durable pending-outbox design avoids silently losing detected changes, but it does not claim exactly-once delivery or exactly-once billing.

API and automation examples

The Store identifier is ikoles/rss-feed-delta-monitor. In REST paths, Apify represents the owner/name separator as ~.

cURL

Run synchronously and receive Dataset rows:

curl --fail-with-body \
--request POST \
--url 'https://api.apify.com/v2/actors/ikoles~rss-feed-delta-monitor/run-sync-get-dataset-items' \
--header "Authorization: Bearer $APIFY_TOKEN" \
--header 'Content-Type: application/json' \
--data '{"feeds":["https://blog.apify.com/rss/"],"monitorId":"apify-blog-monitor-v01","bootstrapMode":"emit_current","maxItemsPerFeed":20}'

JavaScript

import { ApifyClient } from 'apify-client';
const token = process.env.APIFY_TOKEN;
if (!token) throw new Error('APIFY_TOKEN is required');
const client = new ApifyClient({ token });
const input = {
feeds: ['https://blog.apify.com/rss/'],
monitorId: 'apify-blog-monitor-v01',
bootstrapMode: 'emit_current',
maxItemsPerFeed: 20,
};
const run = await client.actor('ikoles/rss-feed-delta-monitor').call(input);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(JSON.stringify(items, null, 2));

Python

import json
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run_input = {
'feeds': ['https://blog.apify.com/rss/'],
'monitorId': 'apify-blog-monitor-v01',
'bootstrapMode': 'emit_current',
'maxItemsPerFeed': 20,
}
run = client.actor('ikoles/rss-feed-delta-monitor').call(run_input=run_input)
items = client.dataset(run['defaultDatasetId']).list_items().items
print(json.dumps(items, indent=2))

For a non-blocking run, POST the same body to:

https://api.apify.com/v2/actors/ikoles~rss-feed-delta-monitor/runs

The response contains the run and storage IDs you can use to fetch Dataset items and the OUTPUT record.

n8n

Create a workflow with a Schedule Trigger followed by an HTTP Request node:

  1. Method: POST.
  2. URL: https://api.apify.com/v2/actors/ikoles~rss-feed-delta-monitor/run-sync-get-dataset-items.
  3. Header: Authorization: Bearer $APIFY_TOKEN, stored in an n8n credential or secret.
  4. Body content type: JSON.
  5. Body: the Quick start input above.

Process the returned array in later nodes. Keep monitorId stable between scheduled executions. The n8n workflow owns its schedule; this Actor does not create it.

Make

Use HTTP > Make a request in a scheduled scenario:

  1. Method: POST.
  2. URL: https://api.apify.com/v2/actors/ikoles~rss-feed-delta-monitor/run-sync-get-dataset-items.
  3. Add Authorization: Bearer $APIFY_TOKEN and Content-Type: application/json headers.
  4. Send the Quick start input as the raw JSON body.
  5. Iterate over the returned Dataset rows and route recordType values to downstream modules.

The Make scenario owns the recurrence and downstream actions.

Webhook after a run

Create an Apify Task with a stable input, then configure a webhook separately from the Task or Actor Integrations tab:

  • Event: ACTOR.RUN.SUCCEEDED.
  • Request URL: your HTTPS automation endpoint.
  • Suggested payload template:
{
"runId": "{{resource.id}}",
"datasetId": "{{resource.defaultDatasetId}}",
"keyValueStoreId": "{{resource.defaultKeyValueStoreId}}"
}

Your receiver can fetch the Dataset and OUTPUT record using those IDs. Apify sends the webhook; the RSS & Atom Delta Monitor does not create or manage it.

MCP

Expose Apify Actor tools to an MCP-compatible client:

{
"mcpServers": {
"apify": {
"url": "https://mcp.apify.com?tools=actors",
"headers": {
"Authorization": "Bearer $APIFY_TOKEN"
}
}
}
}

Then ask the client:

Find ikoles/rss-feed-delta-monitor, add or expose it if the client requires that step, then run it with monitorId agent-news, bootstrapMode emit_current, maxItemsPerFeed 20, and my feed URLs. Return the Dataset rows and run summary.

The available MCP tool flow depends on the client: it may discover and add the Actor first or expose a direct Actor-run tool.

Keep the token in your MCP client's secret storage; never commit it.

Supported and unsupported inputs

Supported:

  • RSS 2.0;
  • Atom 1.0;
  • RSS 1.0/RDF;
  • direct public HTTP and HTTPS feed URLs;
  • conditional requests using ETag and Last-Modified.

Not supported in v0.1:

  • JSON Feed;
  • authenticated feeds, cookies, custom headers, or URL credentials;
  • proxies;
  • HTML feed auto-discovery;
  • full-text scraping of linked pages;
  • LLM summaries or classification;
  • deletion detection;
  • creating schedules, Tasks, or webhooks for the user.

An item disappearing from a rolling feed is not treated as a deletion.

Security, resource limits, and data handling

  • The end-to-end HTTP deadline is 15 seconds per feed.
  • Responses are limited to 5 MiB of decompressed content.
  • Redirects are limited to five.
  • At most 10 feeds perform fetch and parse work concurrently.
  • Loopback, private, and link-local destinations are blocked. Every redirect and connected socket peer is checked again.
  • Feed URLs with user information are rejected.
  • URL query strings are omitted from errors and status messages.

The Actor accepts no API tokens, cookies, feed credentials, or payment data in its input. It writes output rows to the run Dataset and stores feed URLs, validators, item fingerprints/timestamps, and pending change rows in rss-feed-delta-monitor-state. Manage storage retention and deletion from your Apify account.

FAQ and support

Why can a successful repeated run have an empty Dataset? An empty Dataset means the monitored feeds were unchanged. The run summary remains available in the OUTPUT record.

Can I use private or authenticated feeds? No. This release accepts direct public HTTP(S) RSS, Atom, or RDF URLs only.

How should downstream automation handle retries? Delivery is at least once. Deduplicate consequential side effects with the stable changeId.

How do I report a problem? Open an Issue with the affected run ID, expected and actual behavior, the feed format, and the smallest sanitized input that reproduces the problem. Remove URL credentials, secret query parameters, tokens, and private feed URLs before posting.

This is an independent Actor and is not affiliated with or endorsed by the publishers of monitored feeds.