# Feed to Markdown for AI Agents (`comradecoding/feed-to-markdown`) Actor

Returns one row per feed item: title, url, published_at, author, and markdown, the article as one Markdown document. Reads RSS, Atom and JSON Feed. Opens the linked page when the feed only has a teaser. Deduplicated, optionally across runs.

- **URL**: https://apify.com/comradecoding/feed-to-markdown.md
- **Developed by:** [Comrade Coding](https://apify.com/comradecoding) (community)
- **Categories:** AI, Developer tools, News
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 feed item as markdowns

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## 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

## Feed to Markdown for AI Agents

Returns one row per feed item: `title`, `url`, `published_at`, `author`, and `markdown`, the whole article as one Markdown document that starts with the title, date and link. Give it RSS, Atom or JSON Feed URLs. When a feed only carries a teaser, the actor opens the linked page and converts the main text instead. Items are deduplicated within a run, and across runs if you name a store.

It is built for pipelines that hand news, blog posts or release notes to a language model: one field holds everything the model needs, with no HTML, scripts or site navigation.

### What it does with each feed URL

1. Checks the site's `robots.txt`. A disallowed URL is skipped. A `Crawl-delay` is waited out between requests to that site.
2. Downloads the feed (up to 20 MB) and works out its text encoding, including servers that send the wrong charset header.
3. If the URL is a web page rather than a feed, it follows the first feed the page declares in a `<link rel="alternate">` tag, so `https://github.blog/` works as well as `https://github.blog/feed/`.
4. Sorts items newest first when every item has a date, otherwise keeps the feed's order. Drops items older than `publishedAfter`, items already returned in this run (same id, or same link once `utm_*` parameters are removed), and items returned by earlier runs when `seenItemsStore` is set. Then keeps up to `maxItemsPerFeed`.
5. For each item, picks the text (see `contentMode`), converts it to Markdown, and saves one row.

### Where the text comes from

`contentMode` decides:

- `auto` (default): if the feed's own text for the item has 150 words or more, that is used and no page is fetched. Otherwise the linked page is opened and its main text is extracted the way a reader view does it. If the page gives fewer words than the feed, the feed text is kept.
- `page`: always open the linked page.
- `feed`: never open pages. Fastest, and the only mode that makes one request per feed instead of one per item.

If the page cannot be used (HTTP error, disallowed by `robots.txt`, not HTML, timed out), the row still comes back with the feed text and `page_error` says why. An item with no text from either source is not saved and not charged; it is listed in `FAILED_URLS`.

### Input

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `feedUrls` | array | required, no default | Feed URLs, or web pages that link to a feed. Plain strings or `{ "url": "..." }` objects. A bare host like `example.com/feed` gets `https://`. Duplicates are dropped. |
| `maxItems` | integer | `100` | Stop after this many items in total. Caps what a run costs. |
| `maxItemsPerFeed` | integer | `20` | At most this many items from each feed. |
| `contentMode` | string | `auto` | `auto`, `page`, or `feed`, as described above. |
| `publishedAfter` | string | empty | Skip items older than a date (`2026-10-01`) or an age (`24 hours`, `7 days`, `2 weeks`, `3 months`). Items with no date are kept. |
| `includeImages` | boolean | `true` | Keep images as Markdown image links. Turn off for shorter, text-only output. |
| `seenItemsStore` | string | empty | Name of a key-value store in your account. Items returned by earlier runs with the same name are skipped, so a scheduled run returns only new items. |

Example input:

```json
{
    "feedUrls": [
        { "url": "https://blog.rust-lang.org/feed.xml" },
        { "url": "https://github.blog/" }
    ],
    "maxItems": 50,
    "maxItemsPerFeed": 10,
    "contentMode": "auto",
    "publishedAfter": "7 days",
    "includeImages": false,
    "seenItemsStore": "my-news-feeds"
}
```

### Output

One dataset item per feed item. This is a real row from a test run. The feed only had a short summary, so the actor read the page (`content_source` is `page`). The `markdown` value is cut short here.

```json
{
    "source_url": "https://www.jsonfeed.org/feed.json",
    "fetched_at": "2026-10-29T13:18:30.338Z",
    "feed_url": "https://www.jsonfeed.org/feed.json",
    "feed_title": "JSON Feed",
    "item_key": "cfdf7e9e1353c0b50d3e77ee",
    "url": "https://www.jsonfeed.org/2020/08/07/json-feed-version.html",
    "title": "JSON Feed version 1.1",
    "author": null,
    "published_at": "2020-08-07T16:44:36.000Z",
    "updated_at": null,
    "categories": [],
    "content_source": "page",
    "page_url": "https://www.jsonfeed.org/2020/08/07/json-feed-version.html",
    "page_error": null,
    "word_count": 89,
    "truncated": false,
    "markdown": "# JSON Feed version 1.1\n\n- Published: 2020-08-07 16:44 UTC\n- Link: https://www.jsonfeed.org/2020/08/07/json-feed-version.html\n- Feed: JSON Feed\n\n---\n\nWe’ve updated the spec to [version 1.1](https://jsonfeed.org/version/1.1). ..."
}
```

Fields:

- `source_url`: the URL you gave. `feed_url`: the feed actually read, after redirects or after following a page's feed link.
- `fetched_at`: ISO 8601 time the row was made.
- `feed_title`: the feed's own title, or `null`.
- `item_key`: a stable 24-character key, the same on every run. Use it to deduplicate in your own database.
- `url`: the item's link. `title`, `author`: plain text, `null` when the feed has none.
- `published_at`, `updated_at`: ISO 8601 UTC, or `null` when the feed gives no readable date.
- `categories`: RSS categories, Atom terms, or JSON Feed tags.
- `content_source`: `feed` or `page`, where the body text came from.
- `page_url`: the page that was read, after redirects. `null` when the feed text was used.
- `page_error`: why the page was not used, for example `HTTP 404` or `Disallowed by robots.txt`. `null` when no page was tried or it worked.
- `word_count`: words in the body, not counting the header.
- `truncated`: `true` when the body was cut at 1,000,000 characters.
- `markdown`: the document. A `#` title line, a short list with Published (or Updated), Author, Link and Feed (lines with no value are left out), a `---` rule, then the body. Links and images are absolute. Lists, tables and code blocks are kept. A heading at the top of the body that repeats the title is removed.

Feeds that fail and items with no text are not added to the dataset and are not charged. They are listed in the `FAILED_URLS` record in the run's key-value store with a reason. A `SUMMARY` record gives the counts for the run: items pushed, how many came from pages and how many from feed text, page errors, items skipped as too old, duplicates, seen before, or over the per-feed cap, and why the run stopped (`done`, `maxItems`, or `charge limit`).

### Limits

- Public feeds and pages only. It does not log in or send cookies.
- It reads what the feed holds at run time. Most feeds hold the last 10 to 50 items; older posts are not fetched from the site's archive.
- Feeds over 20 MB and pages over 5 MB are skipped.
- Page extraction works well on articles and blog posts. Pages built entirely with JavaScript return little text, and in `auto` mode the feed text is kept for those. Image posts such as web comics are a weak spot: the feed holds only the image, so `auto` opens the page, and on some sites the page's main text is the site's own boilerplate. Use `contentMode: "feed"` with images on for those feeds.
- Pages are not fetched from sites whose `robots.txt` asks for more than 10 seconds between requests. Those rows use the feed text and get a `page_error`.
- Requests go out one at a time, with a 300 to 800 ms pause between requests to the same site, or the site's `Crawl-delay` if longer. Sites asking for more than 120 seconds are skipped. A run that opens pages takes about one second per item.
- Redirects are followed up to 5 times, and `robots.txt` is checked for each target before it is requested. A site whose `robots.txt` cannot be reached (server error or timeout) is skipped.
- `seenItemsStore` keeps the most recent 100,000 item keys per store name, in your Apify account until you delete it. Items cut off by `maxItems`, and items that failed, are not marked as seen, so a later run picks them up.
- Link titles are written on one line and cut at 300 characters.
- Output keeps whatever the feed and pages contain, including author names. Check that you may use the content you collect.

### Pricing

Pay per event: **$0.002 per item saved** ($2 per 1,000). Silver plan users pay $0.0018 and Gold plan users $0.0016. Failed feeds, items with no text, and skipped items cost nothing.

| Run | Cost at the Free plan price |
| --- | --- |
| 100 items (the default `maxItems`) | about $0.20 |
| 1,000 items | about $2 |
| 10,000 items | about $20 |

A schedule that checks 20 feeds every hour with `seenItemsStore` set and finds about 5 new items an hour saves about 3,600 items a month: about $7.20.

Set `maxItems` to cap a run, or set a maximum cost per run in Apify; the actor stops when either is reached.

This actor is not affiliated with any of the sites used in the examples.

### Use it from an AI agent

Add it as a tool through Apify's MCP server: `https://mcp.apify.com?tools=comradecoding/feed-to-markdown`. The tool takes a list of feed URLs and returns one row per item with the article as a Markdown document in `markdown`, plus `title`, `url` and `published_at`.

### Integrations

Anything that can call the Apify API can run this actor: the Apify API and client libraries, and Apify's integrations for Make, n8n, Zapier and LangChain. Results come back as a dataset you can download as JSON, CSV or Excel.

### FAQ

**How is this different from RSS and Atom to JSON?**

That actor returns many structured fields per item with a plain-text summary, and full text as an add-on. This one returns a single Markdown document per item, reading the page by default when the feed is short.

**How do I get only new items on a schedule?**

Set `seenItemsStore` to a name and keep it the same on every run.

**Am I charged when a page fails?**

The row is still saved with the feed text and `page_error`, and that row is charged once. If there is no text at all, nothing is saved or charged.

**Can it read paywalled or members-only posts?**

No. It reads public pages only, so a paywalled post gives whatever the feed itself contains.

### Related actors by ComradeCoding

- [RSS and Atom to JSON](https://apify.com/comradecoding/rss-to-json): structured feed rows, one per item.
- [URL to Markdown Converter](https://apify.com/comradecoding/url-to-markdown): any list of pages to Markdown.
- [Sitemap URL Extractor](https://apify.com/comradecoding/sitemap-url-extractor): every page URL of a site, to feed into URL to Markdown.
- [PDF to Text and JSON](https://apify.com/comradecoding/pdf-to-text): text from PDF links that feeds point to.

# Actor input Schema

## `feedUrls` (type: `array`):

Required. RSS, Atom, or JSON Feed URLs, as plain strings or objects with a url field. A web page URL works too if the page declares its feed in a link tag. Duplicates are dropped. Feeds that fail are listed in the FAILED_URLS record and are not charged.

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

Stop after this many items have been saved, across all feeds. You pay per item, so this caps the cost of a run.

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

Take at most this many items from each feed, newest first when every item has a date, otherwise in feed order.

## `contentMode` (type: `string`):

auto: use the feed's own text when it has 150 words or more, otherwise open the linked page. page: always open the linked page. feed: never open pages, use only the feed text. If a page fails, the feed text is used and page_error says why.

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

Skip items older than this. A date such as 2026-10-01, or an age such as 24 hours, 7 days, 2 weeks, 3 months. Items with no date are kept. Leave empty to keep everything.

## `includeImages` (type: `boolean`):

Keep images as Markdown image links. Turn off for text only, which is shorter for language models.

## `seenItemsStore` (type: `string`):

Name of a key-value store in your account, such as my-news-feeds. Items saved by earlier runs that used the same name are skipped, so scheduled runs return only new items. Leave empty to return every item each run.

## Actor input object example

```json
{
  "feedUrls": [
    {
      "url": "https://blog.rust-lang.org/feed.xml"
    },
    {
      "url": "https://github.blog/"
    }
  ],
  "maxItems": 50,
  "maxItemsPerFeed": 10,
  "contentMode": "auto",
  "publishedAfter": "7 days",
  "includeImages": false,
  "seenItemsStore": "my-news-feeds"
}
```

# Actor output Schema

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

No description

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

No description

## `failures` (type: `string`):

No description

# 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 = {
    "feedUrls": [
        {
            "url": "https://blog.rust-lang.org/feed.xml"
        },
        {
            "url": "https://www.nasa.gov/feed/"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("comradecoding/feed-to-markdown").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 = { "feedUrls": [
        { "url": "https://blog.rust-lang.org/feed.xml" },
        { "url": "https://www.nasa.gov/feed/" },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("comradecoding/feed-to-markdown").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 '{
  "feedUrls": [
    {
      "url": "https://blog.rust-lang.org/feed.xml"
    },
    {
      "url": "https://www.nasa.gov/feed/"
    }
  ]
}' |
apify call comradecoding/feed-to-markdown --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,comradecoding/feed-to-markdown"
        }
    }
}
```

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/dHGtwRScIaLG0dibR/builds/Uad4KJFUDoHiGtTqq/openapi.json
