# RSS Feed Reader & Monitor (RSS, Atom, JSON Feed) (`adamhaleworks/rss-feed-reader-monitor`) Actor

Read many RSS, Atom and JSON feeds in one run, or paste website URLs and the feed is found for you. Clean items with title, link, date, summary, author, image and podcast audio. Only-new mode with conditional requests for cheap hourly feed alerts. Keyword and date filters.

- **URL**: https://apify.com/adamhaleworks/rss-feed-reader-monitor.md
- **Developed by:** [Adam Hale](https://apify.com/adamhaleworks) (community)
- **Categories:** News, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## RSS Feed Reader & Monitor: read RSS, Atom and JSON feeds in bulk, get only the new items

Paste **feed URLs or plain website URLs** and get one clean row per item: title, link, publish date, plain-text summary, author, categories, image and podcast audio. It reads RSS 2.0, RSS 1.0 (RDF), Atom and JSON Feed, so blogs, news sites, YouTube channels, GitHub releases, podcasts and government notices all come out in the same format.

Turn on **Only new items** and schedule it, and each run returns just the items published since the last run. You don't have to deduplicate by guid yourself. Feeds that haven't changed are checked with a conditional request.

- **Finds the feed for you.** Give it `blog.cloudflare.com` or `stripe.com/blog` and it finds `/rss/` or `/blog/feed.rss` by itself.
- **Keyword and date filters**: whole-word keywords with `launch*` wildcards, exclude words, "published in the last 24 hours".
- **Free while in launch; planned pay-per-event price: $0.0005 per feed checked + $0.20 per 1,000 items**, no start fee. Feeds that fail won't be charged.
- **Fast**: no browser and no proxy. 14 inputs (RSS 2.0, RSS 1.0, Atom, JSON Feed, three websites and three broken URLs) took 5 seconds in our test.

### What can I use it for?

- **News and blog alerts.** Watch competitors, vendors, regulators or industry blogs and get only the new posts by Slack, email, webhook or Zapier.
- **Release and changelog tracking.** GitHub release feeds (`/releases.atom`), product changelogs and status pages.
- **Content pipelines and AI.** Feed new articles into a summariser, RAG index, translation workflow or newsletter builder, with full content when the feed carries it.
- **Podcast catalogs.** Episode titles, audio URLs, MIME types, file sizes and durations for whole back catalogs (the Changelog feed has 1,014 episodes).
- **Media monitoring.** Combine many publisher feeds with keyword filters such as `"data breach"` or `"acquisition"` and get one dataset.
- **YouTube channel uploads** through the public channel feed (`youtube.com/feeds/videos.xml?channel_id=…`), including thumbnails.

### What data does it return?

One row per feed item:

| Field | Description |
|---|---|
| `title`, `url` | Item title (HTML removed) and link |
| `publishedAt`, `updatedAt` | ISO 8601 UTC. `updatedAt` is only set when the feed gives a different update time |
| `author`, `authors` | Author name with email addresses removed; `authors` when there are several |
| `summary` | Plain-text summary or description, cut at a word boundary (default 1,000 characters) |
| `categories` | Tags and categories from the feed |
| `imageUrl` | Thumbnail from `media:thumbnail`, `media:content`, an image enclosure or the first image in the item |
| `enclosureUrl`, `enclosureType`, `enclosureLength`, `durationSecs`, `enclosures` | Podcast and media files (audio/video URL, MIME type, bytes, duration) |
| `commentsUrl` | Comments page, when given |
| `itemId` | The feed's guid/id (or link) used for deduplication |
| `positionInFeed` | Position of the item in the feed as published |
| `matchedKeywords` | Which of your keywords matched |
| `feedTitle`, `feedUrl`, `feedSiteUrl`, `feedType`, `feedLanguage` | The feed the item came from. `feedType` is, for example, `RSS 2.0`, `Atom 1.0`, `RSS 1.0 (RDF)` or `JSON Feed 1.1` |
| `inputUrl`, `discoveredVia` | What you entered and, for website URLs, how the feed was found (`link-alternate`, `page-link`, `guessed`) |
| `contentHtml`, `contentText` | Full content, only with **Include full content** on |
| `fetchedAt` | Time of the run |

A per-feed report is saved as `SUMMARY` in the run's key-value store. It holds the status of each feed (`ok`, `not_modified`, `no_feed_found`, `http_error`, `parse_error`, `timeout`, `error`), the feed URL found, the item counts and a readable error message.

The Output tab exports CSV, Excel, JSON or XML. The dataset has two views: **Items** and **Podcasts and media**.

#### Example output

From a real run on 2026-10-04 with the website URL `https://blog.cloudflare.com/` as input:

```json
{
  "itemId": "01M3XKBJCTDTMBBCNNHA141F04",
  "title": "Streamline: custom video pipelines with Cloudflare Stream and Workers",
  "url": "https://blog.cloudflare.com/streamline/",
  "publishedAt": "2026-10-02T16:13:45Z",
  "updatedAt": null,
  "author": "Willi Geiger",
  "authors": ["Willi Geiger", "Taylor Smith"],
  "categories": ["Birthday Week", "Cloudflare Stream", "Containers", "Developer Platform", "Durable Objects", "Workers"],
  "summary": "Streamline demonstrates how to build long-running, continuous video processing pipelines by pairing Cloudflare Workers and Durable Objects with a containerized media engine.",
  "imageUrl": "https://blog.cloudflare.com/_emdash/api/media/file/01M3XKCAN8X5Q5KQ1Q5EW20NBG.01M3XKCB43AH4EZPQE0QQ7MNND.png",
  "enclosureUrl": null,
  "durationSecs": null,
  "positionInFeed": 1,
  "matchedKeywords": null,
  "feedTitle": "Cloudflare Blog",
  "feedUrl": "https://blog.cloudflare.com/rss/",
  "feedSiteUrl": "https://blog.cloudflare.com/",
  "feedType": "RSS 2.0",
  "feedLanguage": "en-us",
  "inputUrl": "https://blog.cloudflare.com/",
  "discoveredVia": "link-alternate",
  "fetchedAt": "2026-10-03T23:20:29Z"
}
```

A podcast episode (shortened):

```json
{
  "title": "Can we make Social Security better for blue-collar workers?",
  "publishedAt": "2026-10-02T14:13:30Z",
  "enclosureUrl": "https://prfx.byspotify.com/e/play.podtrac.com/npr-510325/npr.simplecastaudio.com/…/default.mp3?…",
  "enclosureType": "audio/mpeg",
  "enclosureLength": 8790605,
  "durationSecs": 549,
  "feedTitle": "The Indicator from Planet Money"
}
```

A feed that could not be read appears only in `SUMMARY`, not in the dataset, and is not charged:

```json
{"inputUrl": "https://this-domain-does-not-exist-rss-test.com/", "status": "error", "error": "DNS_ERROR: the domain name does not resolve"}
```

### How much does it cost?

**Free while in launch.** The actor charges no fee of its own right now; you pay only the Apify platform usage of your runs (the free Apify plan includes $5 of usage a month). The pay-per-event prices below are planned and apply only once they are switched on; Apify notifies existing users before a price change takes effect.

Planned pricing, pay per event:

| Event | Planned price |
|---|---|
| Feed checked (downloaded and read, or confirmed unchanged) | $0.0005 |
| Item in the output | $0.0002 (= **$0.20 per 1,000 items**) |

Real runs from 2026-10-04, at the planned prices:

- The default input (3 feeds, 40 items) would cost **$0.0095**.
- First monitoring run on 6 feeds: 95 items, **$0.022**. Running it again right away: all 6 feeds unchanged, 0 items, **$0.003**.
- **Example monthly bill:** 10 feeds checked every hour (7,200 checks) with about 300 new items in the month would cost **$3.66**.

**Not charged:** feeds that can't be found, fail to download or can't be parsed, items removed by your filters, and items already seen in monitoring mode. **Cost cap:** once pricing is on, set **Maximum cost per run** and the actor stops cleanly when it reaches it.

For comparison (Apify Store, checked 2026-10-03), the same hourly 10-feed schedule costs about **$190.80** a month with the most-used RSS reader ($0.035 per run plus $1.15 per 1,000 items, every item every run). It costs **$15.12** with the second ($0.001 per run plus $0.002 per feed).

### How to use it

1. Paste feed URLs or website URLs into **Feed or website URLs**, one per line (up to 5,000).
2. For a one-off export, press Start. To monitor, turn on **Only new items**. To skip the backlog, also turn off **Output current items on the first run**: the first run then only remembers what is already there.
3. Optionally add **Keywords**, **Exclude keywords**, **Published after** (`24 hours`, `7 days`, `2026-10-01`) or **Max items per feed**.
4. Schedule the actor (for example every hour) and add an integration: Slack, email, Google Sheets, Zapier, Make or a webhook. Use a different **Memory name** for each independent monitor.

#### Input example

```json
{
  "feeds": [
    "https://blog.cloudflare.com/",
    "https://github.com/apify/apify-sdk-python/releases.atom",
    "https://www.gov.uk/search/news-and-communications.atom",
    "https://feeds.npr.org/510289/podcast.xml"
  ],
  "onlyNew": true,
  "outputOnFirstRun": false,
  "keywords": ["security", "outage", "launch*"],
  "stateStoreName": "my-feed-alerts"
}
```

#### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/adamhaleworks~rss-feed-reader-monitor/run-sync-get-dataset-items?token=<TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"feeds": ["https://github.blog/feed/"], "maxItemsPerFeed": 10}'
```

### How it works

- **Feed discovery.** When a URL returns a web page, the actor reads the page's declared feed links (`<link rel="alternate">`). Main feeds come before comment feeds, and page API links are ignored. If there are none, it tries links on the page that look like RSS links. It then tries common feed paths under the page and the site root, such as `/feed`, `/rss.xml`, `/atom.xml` and `/blog/feed.rss`, skipping any that robots.txt disallows.
- **Parsing.** RSS and Atom go through the widely used `feedparser` library. It recovers items from feeds that are not well-formed and reports a warning. JSON Feed 1.0/1.1 is parsed natively. HTML in titles and summaries is converted to plain text. Scripts and styles are dropped.
- **Monitoring memory.** For each feed, the IDs of the items already seen are stored, together with the feed's ETag and Last-Modified headers, in a named key-value store in your account. The memory keeps items still in the feed plus 90 days of history (up to 5,000 per feed). Items cut by **Max items in total** or the cost cap are not marked as seen, so they come in the next run.
- **Polite fetching.** It identifies itself as `RSSFeedReaderMonitor/1.0` and sends at most 2 parallel requests per site. It retries timeouts, 429 and 5xx with backoff and honours `Retry-After` up to 30 seconds. It does not use proxies or try to get around bot protection: a site that blocks it is reported as `http_error` with the status code.

### Limitations

- Only what the feed contains. Many feeds carry the newest 10-50 items and only a summary. Older items and full article text are not fetched from the website.
- Some sites put their feeds behind bot protection (for example Cloudflare challenges) and answer 403. Those feeds are reported, not bypassed.
- Private and internal addresses (`localhost`, `10.0.0.0/8`, `169.254.169.254`, …) are refused. Responses larger than 15 MB are skipped.
- Items without a date are dropped when **Published after** is set, and they are listed after dated items.

### Is it legal? What about personal data?

Feeds are published so that machines can read them. The actor reads only the feed, the page you gave it and robots.txt, and it checks guessed feed paths against robots.txt. Author email addresses are removed from the output. Feed content stays the publisher's copyright: you are responsible for how you reuse it (for example, linking back is usually fine; republishing full articles usually needs permission).

### FAQ

**How is "only new items" different from "Published after"?** A date filter can miss items that arrive late with an older date, and it repeats items when runs overlap. The memory compares item IDs, so every item is returned exactly once, whatever the schedule.

**Why did a feed return nothing?** Open `SUMMARY` in the key-value store. `not_modified` means the feed is unchanged since the last run. `no_feed_found` means the page declares no feed and none of the common paths worked; try the feed URL directly. `http_error` with 403 means the site blocks automated readers.

**Can I get full article text?** Turn on **Include full content**. It returns whatever the feed carries (`content:encoded` or Atom content). Many WordPress and Substack feeds include the full post.

**Can the same feed be in two monitors?** Yes. Give each monitor its own **Memory name**; with the same name they share one memory per feed URL.

**Something is wrong or missing?** Open an issue on the actor's Issues tab and include the feed URL.

***

Author: Adam Hale

# Actor input Schema

## `feeds` (type: `array`):

RSS, Atom, RDF or JSON Feed URLs, one per line. You can also paste a website or blog URL: the actor reads the feed links the page declares and, if there are none, tries common feed paths such as /feed and /rss.xml. Up to 5,000 per run.

## `onlyNew` (type: `boolean`):

Remember which items were already output and return only items that are new since the last run. Schedule the actor (for example hourly) and connect a webhook, Slack, email or Zapier integration to get feed alerts. Unchanged feeds are checked with conditional requests (ETag / Last-Modified).

## `outputOnFirstRun` (type: `boolean`):

In monitoring mode: when a feed is checked for the first time, output its current items (on) or only remember them so that later runs output just what is new (off).

## `stateStoreName` (type: `string`):

Name of the key-value store that remembers seen items. Use a different name for each independent monitor; the same feed URL in two monitors with the same name is remembered once.

## `maxItemsPerFeed` (type: `integer`):

Newest items to output from each feed per run (after filters). 0 = all items in the feed. In monitoring mode, items beyond this limit are remembered as seen.

## `publishedAfter` (type: `string`):

Only items published after this date (2026-10-01 or 2026-10-01T08:00:00Z) or within a period before the run: "24 hours", "7 days", "2 weeks". Items without a date are dropped when this is set.

## `keywords` (type: `array`):

Keep only items that contain ANY of these words or phrases (whole words, case-insensitive). A trailing \* matches word endings: launch\* matches launches and launched. Empty = no keyword filter.

## `excludeKeywords` (type: `array`):

Drop items that contain any of these words or phrases.

## `keywordsIn` (type: `string`):

Title and content = title, summary, full content and categories. Title only = stricter, fewer false matches.

## `discoverFeeds` (type: `boolean`):

When a URL is a web page instead of a feed, look for its feed (declared feed links, RSS links on the page, then common feed paths allowed by robots.txt). Off = web page URLs are reported as 'no feed found'.

## `maxFeedsPerSite` (type: `integer`):

How many of the feeds a website declares to read. 1 = the main feed only (comment feeds are always tried last).

## `includeContent` (type: `boolean`):

Add the full item content as HTML and as plain text (contentHtml, contentText) when the feed provides it. Makes the dataset much larger.

## `summaryMaxLength` (type: `integer`):

The plain-text summary is cut at a word boundary after this many characters. 0 = no limit.

## `maxItems` (type: `integer`):

The run stops after this many items. You can also cap the cost with the run's 'Maximum cost per run' option.

## `maxConcurrency` (type: `integer`):

Feeds read at the same time (at most 2 at once per website).

## `requestTimeoutSecs` (type: `integer`):

How long to wait for each feed or page to respond.

## `maxRetries` (type: `integer`):

Retries on timeouts, 429 and 5xx responses (a Retry-After header up to 30 seconds is honoured).

## Actor input object example

```json
{
  "feeds": [
    "https://github.blog/feed/",
    "https://blog.cloudflare.com/",
    "https://www.nasa.gov/news-release/feed/"
  ],
  "onlyNew": false,
  "outputOnFirstRun": true,
  "stateStoreName": "rss-feed-monitor-state",
  "maxItemsPerFeed": 100,
  "publishedAfter": "7 days",
  "keywords": [
    "security",
    "outage",
    "launch*"
  ],
  "excludeKeywords": [
    "sponsored",
    "webinar"
  ],
  "keywordsIn": "titleAndContent",
  "discoverFeeds": true,
  "maxFeedsPerSite": 1,
  "includeContent": false,
  "summaryMaxLength": 1000,
  "maxItems": 5000,
  "maxConcurrency": 10,
  "requestTimeoutSecs": 30,
  "maxRetries": 2
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset items, one per feed item. Views: items, podcasts and media.

## `summary` (type: `string`):

SUMMARY record in the default key-value store: feeds requested and checked, per-feed status and errors, items output, skipped and filtered, stop reason.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "feeds": [
        "https://github.blog/feed/",
        "https://blog.cloudflare.com/",
        "https://www.nasa.gov/news-release/feed/"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("adamhaleworks/rss-feed-reader-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "feeds": [
        "https://github.blog/feed/",
        "https://blog.cloudflare.com/",
        "https://www.nasa.gov/news-release/feed/",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("adamhaleworks/rss-feed-reader-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "feeds": [
    "https://github.blog/feed/",
    "https://blog.cloudflare.com/",
    "https://www.nasa.gov/news-release/feed/"
  ]
}' |
apify call adamhaleworks/rss-feed-reader-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,adamhaleworks/rss-feed-reader-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/2hL2ekcZuRpMOjL9Z/builds/9kSIzZOKKmM0sFmpB/openapi.json
