RSS & Atom Feed Monitor — Change Detection API
Pricing
from $2.00 / 1,000 feed checkeds
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
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
2 days ago
Last modified
Categories
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:
- Monitor competitor blogs for new RSS posts
- Track new cybersecurity advisories from RSS
- Send only new RSS items to AI agents
- Monitor cloud service status feeds
- Track software release notes from RSS
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
| Field | Type | Required | Default | Limits and behavior |
|---|---|---|---|---|
feeds | string[] | Yes | — | 1–100 unique public http: or https: feed URLs; credentials in URLs are rejected |
monitorId | string | No | Derived from the normalized feed list | 1–80 characters when provided |
bootstrapMode | emit_current | baseline_only | No | emit_current | emit_current emits free baseline rows; baseline_only saves state without baseline rows |
maxItemsPerFeed | integer | No | 20 | 1–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:
| Event | Price | When it applies |
|---|---|---|
Actor start (apify-actor-start) | $0.00005 | One synthetic platform event when a run starts |
Successful feed check (feed-checked) | $0.002 | A feed returns a recognized 200 response or a valid 304 |
Emitted post-baseline change (change-detected) | $0.0005 | Each 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 jsonimport osfrom apify_client import ApifyClientclient = 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().itemsprint(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:
- Method:
POST. - URL:
https://api.apify.com/v2/actors/ikoles~rss-feed-delta-monitor/run-sync-get-dataset-items. - Header:
Authorization: Bearer $APIFY_TOKEN, stored in an n8n credential or secret. - Body content type: JSON.
- 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:
- Method:
POST. - URL:
https://api.apify.com/v2/actors/ikoles~rss-feed-delta-monitor/run-sync-get-dataset-items. - Add
Authorization: Bearer $APIFY_TOKENandContent-Type: application/jsonheaders. - Send the Quick start input as the raw JSON body.
- Iterate over the returned Dataset rows and route
recordTypevalues 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 withmonitorIdagent-news,bootstrapModeemit_current,maxItemsPerFeed20, 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
ETagandLast-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.