Systems Library / Marketing Automation / How to Automate Newsletter Content Curation
Marketing Automation content marketing

How to Automate Newsletter Content Curation

Curate relevant industry content for newsletters using AI.

Jay Banlasan

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

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

Related Reading

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

Related Systems