How to Automate Newsletter Content Curation
Curate relevant industry content for newsletters using AI.
Jay Banlasan
The AI Systems Guy
A newsletter that takes four hours to curate every week is a newsletter that gets skipped or killed. This automate newsletter content curation ai system monitors your RSS feeds, scores articles for relevance, writes the summaries, and assembles the draft by Friday morning. You review, add your commentary, and send. The whole review takes twenty minutes instead of four hours.
Newsletters are one of the highest-ROI channels in content marketing because they reach people who already opted in. The constraint is never the sending. It is always the finding and writing. This system eliminates that constraint.
What You Need Before Starting
- Python 3.10 or higher
- Anthropic API key
- A list of RSS feeds from sources your audience respects
pip install anthropic feedparser requests python-dotenv
Step 1: Configure Your Feed Sources
Group your sources by category so you can weight them differently:
# newsletter_config.py
NEWSLETTER_CONFIG = {
"name": "The AI Operations Brief",
"audience": "Marketing directors and agency owners who use AI tools",
"frequency": "weekly",
"max_stories": 6,
"editorial_stance": "Practical over theoretical. Real implementations over concept pieces. Tools that save time.",
"banned_topics": ["cryptocurrency speculation", "NFTs", "general tech news without marketing angle"]
}
RSS_SOURCES = [
{"url": "https://feeds.feedburner.com/MarketingProfs", "category": "marketing", "weight": 1.2},
{"url": "https://contentmarketinginstitute.com/feed/", "category": "content", "weight": 1.1},
{"url": "https://blog.hubspot.com/marketing/rss.xml", "category": "marketing", "weight": 1.0},
{"url": "https://www.socialmediaexaminer.com/feed/", "category": "social", "weight": 0.9},
{"url": "https://searchengineland.com/feed", "category": "seo", "weight": 1.0}
]
Step 2: Fetch and Parse Feeds
import feedparser
import requests
from datetime import datetime, timedelta
from newsletter_config import RSS_SOURCES, NEWSLETTER_CONFIG
def fetch_articles(days_back: int = 7) -> list:
cutoff = datetime.now() - timedelta(days=days_back)
articles = []
for source in RSS_SOURCES:
try:
feed = feedparser.parse(source["url"])
source_name = feed.feed.get("title", source["url"])
for entry in feed.entries:
pub_date = None
if hasattr(entry, "published_parsed") and entry.published_parsed:
pub_date = datetime(*entry.published_parsed[:6])
if pub_date and pub_date < cutoff:
continue
articles.append({
"title": entry.get("title", ""),
"url": entry.get("link", ""),
"summary": entry.get("summary", "")[:500],
"source": source_name,
"category": source["category"],
"weight": source["weight"],
"published": pub_date.isoformat() if pub_date else ""
})
except Exception as e:
print(f"Feed error {source['url']}: {e}")
print(f"Fetched {len(articles)} articles from {len(RSS_SOURCES)} sources")
return articles
Step 3: Score Articles for Relevance
import anthropic
import json
import os
from dotenv import load_dotenv
load_dotenv()
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
def score_articles(articles: list) -> list:
config = NEWSLETTER_CONFIG
articles_for_scoring = [
{"index": i, "title": a["title"], "summary": a["summary"][:200], "source": a["source"]}
for i, a in enumerate(articles)
]
batch_size = 20
all_scores = {}
for i in range(0, len(articles_for_scoring), batch_size):
batch = articles_for_scoring[i:i+batch_size]
batch_str = json.dumps(batch, indent=2)
prompt = f"""Score these articles for newsletter inclusion.
NEWSLETTER: {config['name']}
AUDIENCE: {config['audience']}
EDITORIAL STANCE: {config['editorial_stance']}
AVOID TOPICS: {', '.join(config['banned_topics'])}
ARTICLES:
{batch_str}
For each article, return a JSON object with:
- index: the article index
- relevance_score: 1-10 (how relevant to the audience)
- freshness_score: 1-10 (is this timely or evergreen in a good way?)
- value_score: 1-10 (would this audience learn or act on something from this?)
- total: sum of the three scores
- include: true/false
- reason: one sentence why you scored it this way
Return as a JSON array."""
message = client.messages.create(
model="claude-opus-4-5",
max_tokens=2000,
messages=[{"role": "user", "content": prompt}]
)
raw = message.content[0].text.strip()
if raw.startswith("```"):
raw = raw.split("```")[1]
if raw.startswith("json"):
raw = raw[4:]
batch_scores = json.loads(raw)
for score in batch_scores:
all_scores[score["index"]] = score
for i, article in enumerate(articles):
score_data = all_scores.get(i, {"total": 0, "include": False, "reason": ""})
article["relevance_score"] = score_data.get("relevance_score", 0)
article["total_score"] = score_data.get("total", 0) * article["weight"]
article["include"] = score_data.get("include", False)
article["score_reason"] = score_data.get("reason", "")
return sorted(articles, key=lambda x: x["total_score"], reverse=True)
Step 4: Write the Newsletter Summaries
def write_newsletter_section(article: dict) -> str:
config = NEWSLETTER_CONFIG
prompt = f"""Write a newsletter summary for this article.
NEWSLETTER AUDIENCE: {config['audience']}
EDITORIAL STANCE: {config['editorial_stance']}
ARTICLE TITLE: {article['title']}
ARTICLE SUMMARY: {article['summary']}
SOURCE: {article['source']}
URL: {article['url']}
Write a 3-4 sentence newsletter entry. Structure:
1. One sentence on what the article covers and why it matters NOW.
2. One sentence on the key insight or takeaway.
3. One sentence on what the reader should do with this information.
No em dashes. Direct language. No phrases like "in conclusion" or "it is important to note."
End with: Read more: [title]({url})"""
message = client.messages.create(
model="claude-opus-4-5",
max_tokens=300,
messages=[{"role": "user", "content": prompt}]
)
return message.content[0].text
def assemble_newsletter(scored_articles: list) -> str:
config = NEWSLETTER_CONFIG
top_articles = [a for a in scored_articles if a["include"]][:config["max_stories"]]
today = datetime.now().strftime("%B %d, %Y")
newsletter = f"# {config['name']}\n"
newsletter += f"**{today}**\n\n"
newsletter += "---\n\n"
newsletter += "[EDITOR'S NOTE: Add your personal commentary here before sending.]\n\n"
newsletter += "---\n\n"
for i, article in enumerate(top_articles, 1):
newsletter += f"## {i}. {article['title']}\n"
newsletter += f"*{article['source']} | {article['category'].upper()}*\n\n"
section_text = write_newsletter_section(article)
newsletter += section_text + "\n\n"
newsletter += "---\n\n"
return newsletter
if __name__ == "__main__":
articles = fetch_articles(days_back=7)
scored = score_articles(articles)
newsletter_draft = assemble_newsletter(scored)
with open("newsletter-draft.md", "w") as f:
f.write(newsletter_draft)
print(f"Draft saved to newsletter-draft.md")
print(newsletter_draft[:500])
What to Build Next
- Schedule this to run every Thursday evening so the draft is waiting for you Friday morning
- Add a subscriber engagement tracker that shows which story links get clicked most, so you can tune your source weights over time
- Build a sponsor slot generator that writes contextually relevant ad copy based on the newsletter content that week
Related Reading
- How to Build an AI Blog Post Generator - Expand top newsletter stories into full articles
- How to Create Automated Content Performance Reports - Track newsletter open rates and click rates alongside your content metrics
- How to Create an AI-Powered FAQ Generator - Build an FAQ section for your newsletter archive
Want this system built for your business?
Get a free assessment. We will map every system your business needs and show you the ROI.
Get Your Free Assessment