Automatically sync RSS feeds to a Notion database with smart pruning and deduplication.
- π° Reads Feedly OPML files to discover RSS feeds
- π Fetches new items across all feeds with concurrency control
- π€ AI-powered triage with OpenAI to auto-classify articles (keep/deprioritize/ignore)
- π Link validation to filter out dead links before import
- π Creates one Notion page per item in your database
- π·οΈ Smart status assignment based on AI decisions
- ποΈ Automatically prunes old items based on age and status
- π’ Enforces per-feed item caps
- πΎ Maintains local cache to avoid duplicates
- β‘ Batched API calls with retry logic for rate limits
- Node.js 18+ (for native fetch and top-level await)
- A Notion integration token
- A Notion database with specific properties (see below)
npm install- Go to https://www.notion.so/my-integrations
- Click "New integration"
- Give it a name and select your workspace
- Copy the "Internal Integration Token"
Create a Notion database with these properties:
| Property Name | Type | Options/Values |
|---|---|---|
| Title | Title | - |
| URL | URL | - |
| Published | Date | - |
| Source | Select | (auto-populated from feeds) |
| Summary | Rich text | - |
| Status | Select | Unread, Read, Archived |
- Open your Notion database
- Click "..." (top right) β "Connections"
- Add your integration
Copy env.example to .env:
cp env.example .envEdit .env with your values:
NOTION_TOKEN=secret_xxx
PRUNE_MAX_AGE_DAYS=30
PER_FEED_HARD_CAP=500
STATE_FILE=.rss_seen.json
BATCH_SIZE=20
CONCURRENCY=4Export your RSS feeds from Feedly, NewsBlur, or any RSS reader as an OPML file.
Example feeds.opml:
<?xml version="1.0" encoding="UTF-8"?>
<opml version="1.0">
<head>
<title>My Feeds</title>
</head>
<body>
<outline text="Tech News" title="Tech News">
<outline type="rss" text="Hacker News" title="Hacker News" xmlUrl="https://hnrss.org/frontpage" />
<outline type="rss" text="TechCrunch" title="TechCrunch" xmlUrl="https://techcrunch.com/feed/" />
</outline>
</body>
</opml>npm run dev -- --opml ./feeds.opml --db YOUR_NOTION_DB_IDBuild and run:
npm run build
npm start -- --opml ./feeds.opml --db YOUR_NOTION_DB_IDThe database ID is the part of the URL after your workspace name and before the "?":
https://www.notion.so/myworkspace/DATABASE_ID?v=...
^^^^^^^^^^^
Add to your crontab to run every 30 minutes:
# Edit crontab
crontab -e
# Add this line (adjust paths)
*/30 * * * * cd /path/to/rss-to-notion && npm start -- --opml /path/to/feeds.opml --db YOUR_DB_ID >> /var/log/rss_to_notion.log 2>&1- Parse OPML: Discovers all RSS feeds from your OPML file
- Concurrent Fetch: Fetches feeds in parallel (controlled by
CONCURRENCY) - Deduplication: Checks local cache to skip already-seen items
- Batching: Creates Notion pages in batches (controlled by
BATCH_SIZE)
The tool performs two types of pruning:
- Age-based: Items with
Status=Readolder thanPRUNE_MAX_AGE_DAYSare archived - Cap-based: Each feed keeps only the latest
PER_FEED_HARD_CAPitems (older ones archived)
- Automatically retries on 429 (rate limit) responses
- Respects
retry_afterheader from Notion API - Batches requests to avoid overwhelming the API
rss-to-notion/
βββ src/
β βββ index.ts # Main entry point
β βββ config.ts # Configuration loading
β βββ types.ts # TypeScript type definitions
β βββ state.ts # State persistence
β βββ opml.ts # OPML parsing
β βββ rss.ts # RSS fetching
β βββ notion.ts # Notion API operations
βββ dist/ # Compiled JavaScript (generated)
βββ package.json # Dependencies and scripts
βββ tsconfig.json # TypeScript configuration
βββ .env # Your configuration (create from env.example)
βββ README.md # This file
| Variable | Default | Description |
|---|---|---|
| NOTION_TOKEN | (required) | Your Notion integration token |
| NOTION_DB_ID | - | Notion database ID (or use --db flag) |
| OPENAI_API_KEY | - | OpenAI API key for AI triage (optional) |
| AI_TRIAGE | true | Enable AI-powered article classification |
| AI_MODEL | gpt-4o-mini | OpenAI model to use for triage |
| AI_MAX_TOKENS | 400 | Max tokens for AI response |
| LINK_VALIDATE | true | Validate links before importing |
| LINK_TIMEOUT_MS | 8000 | Link validation timeout (milliseconds) |
| MAX_ARTICLE_AGE_DAYS | 0 | Only import articles from last N days (0=all) |
| PRUNE_MAX_AGE_DAYS | 30 | Archive Read items older than this (days) |
| PER_FEED_HARD_CAP | 500 | Max items per feed (archive older) |
| STATE_FILE | .rss_seen.json | Local cache file for seen items |
| BATCH_SIZE | 20 | Number of pages to create per batch |
| CONCURRENCY | 4 | Number of parallel feed fetches |
# Install dependencies
npm install
# Run in development mode
npm run dev -- --opml ./feeds.opml --db YOUR_DB_ID
# Build for production
npm run build
# Run tests
npm test
# Lint code
npm run lint
# Clean build artifacts
npm run cleanMake sure you've created a .env file with your Notion integration token.
Check that your OPML file has <outline> elements with xmlUrl attributes.
Reduce BATCH_SIZE and CONCURRENCY in your .env file.
- Verify your database is shared with the integration
- Check that property names match exactly (case-sensitive)
- Ensure Status has "Unread", "Read", and "Archived" as select options
MIT