Claw Mart
โ† Back to Blog
March 21, 20269 min readClaw Mart Team

Daily Briefing Agent: Get Your Morning Summary via Email

Daily Briefing Agent: Get Your Morning Summary via Email

Daily Briefing Agent: Get Your Morning Summary via Email

Every morning, I wake up to an email that gives me exactly what I need to know โ€” a clean summary of the news in my industry, updates on topics I'm tracking, a few surprising developments I wouldn't have caught on my own, and zero fluff. It takes about 90 seconds to read. I didn't write it. An AI agent did, at 5:47am, while I was still unconscious.

It took me roughly two hours to set up. It has run every single morning for the last four months without breaking. And it costs me less than a dollar a week.

This is the kind of thing that sounds like a tech demo until it actually works in production, reliably, day after day. Most people who try to build something like this cobble together a LangChain script, wire it to a cron job, and watch it silently fail on day three. I know because I did exactly that, twice, before I found OpenClaw.

Let me walk you through how to build a daily briefing agent that actually works โ€” one that fetches information from the sources you care about, synthesizes it into something genuinely useful, personalizes it to your interests, and delivers it to your inbox every morning like clockwork.

Why Most Daily Briefing Agents Die in Production

Before we build the thing, let's talk about why this is harder than it seems. Because it seems trivially easy. Fetch some RSS feeds, throw them at an LLM, email the output. Done, right?

Here's what actually happens:

Day 1โ€“3: Everything works beautifully. You're a genius.

Day 4: The news API rate-limits you at 5am. The agent returns an empty summary. You don't notice because there's no error handling. You go to work uninformed.

Day 7: The LLM hallucinates a merger that didn't happen. You mention it in a meeting. Awkward.

Day 12: Your context window overflows because you're feeding in 60 articles raw. The agent either crashes or returns a garbled mess.

Day 18: You realize you've spent $45 on API calls this month for what is essentially a newsletter.

Day 22: You stop checking the emails because they've become repetitive and generic.

Day 30: You turn it off.

This is the lifecycle of approximately 90% of DIY daily briefing agents. I've seen this story repeated across Reddit, in Discord servers, on Hacker News โ€” everywhere people are building with AI agents. The pattern is always the same: impressive demo, fragile production.

The core problems are reliability, cost, personalization, and delivery. You need to solve all four simultaneously, or the thing falls apart. This is precisely what OpenClaw was built to handle.

What OpenClaw Actually Is (and Why It's Different)

OpenClaw is an open-source AI agent platform designed around one core idea: agents should run reliably on a schedule, in production, without babysitting. It's not a general-purpose "build any agent" framework. It's opinionated. It ships with structured workflow graphs, built-in retry logic, output validation, and โ€” critically for our purposes โ€” a Daily Briefing template that handles most of the hard stuff out of the box.

Think of it as the difference between "here are some LEGO bricks, build a car" and "here's a car kit with instructions, and you can customize it." Both approaches have merit, but if you want a working car by this afternoon, you want the kit.

The key architectural decisions that make OpenClaw work for daily briefings:

  • Checkpoint-based execution: Every step in the workflow saves its state. If step 4 of 6 fails, you resume from step 4, not step 1.
  • Pydantic output validation: Every major step validates its output against a schema. The briefing either meets quality gates or fails loudly with clear diagnostics. No more silent garbage.
  • Hierarchical summarization: Articles get summarized individually, then clustered by topic, then synthesized into a final briefing. This keeps token costs sane.
  • Model routing: You can use a powerful model for reasoning and synthesis, and a cheaper model for initial summarization. Mix and match.
  • Built-in scheduling and delivery: Celery Beat + Redis handles the scheduling. Multiple output renderers handle the formatting. You don't have to wire this up yourself.

Getting Started: The Fastest Path to a Working Briefing

If you want to skip the slow ramp-up and get a production-quality daily briefing running as quickly as possible, I'd genuinely recommend grabbing Felix's OpenClaw Starter Pack. Felix put together a bundle that includes pre-configured briefing templates, source connectors, and deployment configs that would take you a full weekend to assemble yourself. It's the single best shortcut I've found for going from zero to a working daily briefing agent, and it's what I used when I first set mine up.

Whether you start with the Starter Pack or go from scratch, here's the architecture of what we're building:

[Sources] โ†’ [Ingestion Agent] โ†’ [Dedup + Cache] โ†’ [Summarization Agent] 
    โ†’ [Personalization Agent] โ†’ [Synthesis Agent] โ†’ [Renderer] โ†’ [Delivery]

Let's build each piece.

Step 1: Define Your Sources and Profile

First, create your project and configure your user profile. OpenClaw uses YAML configuration files for the common cases, so you don't need to write much code for a standard setup.

# config/profile.yaml
user:
  name: "Your Name"
  email: "you@yourdomain.com"
  
interests:
  primary_topics:
    - "AI infrastructure"
    - "developer tooling"
    - "startup fundraising"
  secondary_topics:
    - "energy policy"
    - "semiconductor supply chain"
  exploration_budget: 0.15  # 15% of briefing can be novel/unexpected topics

sources:
  rss_feeds:
    - url: "https://techcrunch.com/feed/"
      priority: high
    - url: "https://feeds.arstechnica.com/arstechnica/technology-lab"
      priority: medium
    - url: "https://blog.pragmaticengineer.com/rss/"
      priority: high
  apis:
    - type: "newsapi"
      query: "artificial intelligence OR machine learning"
      max_results: 30
    - type: "hackernews"
      min_score: 100
      
preferences:
  depth: "executive"  # options: headline, executive, deep-dive
  tone: "direct"      # options: formal, direct, casual
  max_sections: 5
  include_sources: true
  include_action_items: true

That exploration_budget parameter is one of my favorite features. It tells the agent to dedicate a percentage of the briefing to things you didn't ask for โ€” novel developments that its exploration sub-agent thinks are relevant based on your interest pattern. This is how you avoid the echo chamber problem without manually managing topic discovery.

Step 2: Set Up the Ingestion Pipeline

# agents/ingestion.py
from openclaw import Agent, Task, SourceConnector
from openclaw.sources import RSSConnector, NewsAPIConnector, HackerNewsConnector
from openclaw.cache import VectorCache

class IngestionAgent(Agent):
    """Fetches and deduplicates content from all configured sources."""
    
    def __init__(self, config):
        super().__init__(
            name="ingestion_agent",
            retry_policy={"max_retries": 3, "backoff": "exponential"},
            checkpoint=True
        )
        self.cache = VectorCache(
            provider="lancedb",  # or "chroma" โ€” both work out of the box
            ttl_days=7
        )
        self.connectors = self._build_connectors(config["sources"])
    
    def _build_connectors(self, source_config):
        connectors = []
        for feed in source_config.get("rss_feeds", []):
            connectors.append(RSSConnector(
                url=feed["url"],
                priority=feed.get("priority", "medium")
            ))
        for api in source_config.get("apis", []):
            if api["type"] == "newsapi":
                connectors.append(NewsAPIConnector(
                    query=api["query"],
                    max_results=api.get("max_results", 20)
                ))
            elif api["type"] == "hackernews":
                connectors.append(HackerNewsConnector(
                    min_score=api.get("min_score", 50)
                ))
        return connectors
    
    def run(self) -> list:
        raw_articles = []
        for connector in self.connectors:
            try:
                articles = connector.fetch()
                raw_articles.extend(articles)
            except Exception as e:
                self.log_warning(f"Source failed: {connector.name} โ€” {e}")
                # Don't crash the whole pipeline for one bad source
                continue
        
        # Deduplicate against the vector cache
        new_articles = self.cache.filter_duplicates(
            raw_articles, 
            similarity_threshold=0.92
        )
        
        # Store in cache for future dedup
        self.cache.store(new_articles)
        
        self.log_info(f"Ingested {len(new_articles)} new articles from {len(raw_articles)} total")
        return new_articles

A few things to notice here. The retry_policy with exponential backoff handles those 5am rate limits gracefully. The checkpoint=True flag means if the summarization agent fails later, we don't re-fetch everything. And the vector cache handles deduplication โ€” not just exact-match dedup, but semantic dedup. So if three outlets cover the same story with different headlines, you only process it once.

Step 3: Hierarchical Summarization

This is where most people blow their token budgets. The naive approach is to concatenate all articles and ask the LLM to summarize. This works for five articles. It falls apart at fifty.

OpenClaw's approach is hierarchical: summarize each article individually (cheap model), cluster by topic, then synthesize clusters into briefing sections (powerful model).

# agents/summarization.py
from openclaw import Agent, ModelRouter
from openclaw.validators import BriefingSectionSchema

class SummarizationAgent(Agent):
    """Hierarchical summarization: article โ†’ cluster โ†’ section."""
    
    def __init__(self, config):
        super().__init__(
            name="summarization_agent",
            checkpoint=True
        )
        self.router = ModelRouter(
            cheap="gpt-4o-mini",      # For individual article summaries
            powerful="gpt-4o",         # For synthesis and reasoning
            local="ollama/mistral"     # Optional fallback
        )
    
    def run(self, articles: list, user_profile: dict) -> list:
        # Step 1: Individual summaries (cheap model, parallelized)
        summaries = self.router.batch(
            model_tier="cheap",
            prompt_template="Summarize this article in 2-3 sentences. "
                          "Focus on: what happened, why it matters, what's next.\n\n"
                          "Article: {article_text}",
            items=articles,
            max_concurrent=10,
            output_schema={"summary": str, "key_entities": list, "category": str}
        )
        
        # Step 2: Topic clustering
        clusters = self.cluster_by_topic(summaries, user_profile)
        
        # Step 3: Synthesize each cluster into a briefing section (powerful model)
        sections = []
        for cluster in clusters:
            section = self.router.generate(
                model_tier="powerful",
                prompt=self._build_synthesis_prompt(cluster, user_profile),
                output_schema=BriefingSectionSchema,
                validate=True  # Reject output that doesn't match schema
            )
            sections.append(section)
        
        return sections

The ModelRouter is doing the heavy lifting on cost optimization. Individual article summaries use GPT-4o-mini (or a local model if you've set one up). Only the synthesis step โ€” where reasoning quality actually matters โ€” uses the expensive model. In practice, this cuts costs by 60โ€“70% compared to running everything through GPT-4o.

The validate=True flag on the synthesis step means the output gets checked against BriefingSectionSchema (a Pydantic model). If the LLM returns something malformed, it retries with a correction prompt. This is how you prevent those garbage summaries that slip through on day seven.

Step 4: The Personalization Layer

# agents/personalization.py
from openclaw import Agent, MemoryStore

class PersonalizationAgent(Agent):
    """Ranks and filters sections based on user preferences and history."""
    
    def __init__(self, config):
        super().__init__(name="personalization_agent", checkpoint=True)
        self.memory = MemoryStore(provider="lancedb")
    
    def run(self, sections: list, user_profile: dict) -> list:
        # Score sections against user interests
        scored = []
        for section in sections:
            relevance = self.memory.compute_relevance(
                section, 
                user_profile["interests"]["primary_topics"],
                user_profile["interests"]["secondary_topics"]
            )
            novelty = self.memory.compute_novelty(section)
            scored.append({
                "section": section,
                "relevance": relevance,
                "novelty": novelty,
                "combined_score": (0.7 * relevance) + (0.3 * novelty)
            })
        
        # Sort by combined score
        scored.sort(key=lambda x: x["combined_score"], reverse=True)
        
        # Take top N sections based on user preference
        max_sections = user_profile["preferences"]["max_sections"]
        
        # Ensure exploration budget is respected
        exploration_budget = user_profile["interests"].get("exploration_budget", 0.1)
        n_explore = max(1, int(max_sections * exploration_budget))
        n_primary = max_sections - n_explore
        
        primary = [s for s in scored if s["relevance"] > 0.5][:n_primary]
        exploratory = [s for s in scored if s["relevance"] <= 0.5 and s["novelty"] > 0.6][:n_explore]
        
        final_sections = primary + exploratory
        
        # Update memory with what was shown
        self.memory.record_shown(final_sections)
        
        return [s["section"] for s in final_sections]

The MemoryStore here is persistent across runs. It knows what you've been shown before, which means it can compute genuine novelty โ€” not just "is this article new" but "is this a topic or angle this user hasn't seen recently." Over time, it learns from what you engage with (if you set up the feedback loop, which I'll cover in a minute).

Step 5: Assembly and Delivery

# workflows/daily_briefing.py
from openclaw import Workflow, Schedule, Renderer
from openclaw.delivery import EmailDelivery
from agents.ingestion import IngestionAgent
from agents.summarization import SummarizationAgent
from agents.personalization import PersonalizationAgent

class DailyBriefingWorkflow(Workflow):
    
    def __init__(self, config_path="config/profile.yaml"):
        self.config = self.load_config(config_path)
        
        # Initialize agents
        self.ingestion = IngestionAgent(self.config)
        self.summarization = SummarizationAgent(self.config)
        self.personalization = PersonalizationAgent(self.config)
        
        # Set up delivery
        self.renderer = Renderer(format="html_email", template="clean_briefing")
        self.delivery = EmailDelivery(
            smtp_host="smtp.yourdomain.com",
            from_address="briefing@yourdomain.com",
            to_address=self.config["user"]["email"]
        )
    
    def run(self):
        # Each step is checkpointed โ€” if step 3 fails, 
        # we resume from step 3, not step 1
        articles = self.ingestion.run()
        
        if not articles:
            self.delivery.send(
                self.renderer.render_empty("No new articles found today.")
            )
            return
        
        sections = self.summarization.run(articles, self.config)
        personalized = self.personalization.run(sections, self.config)
        
        # Render and deliver
        html = self.renderer.render(
            sections=personalized,
            user_name=self.config["user"]["name"],
            include_sources=self.config["preferences"]["include_sources"],
            include_action_items=self.config["preferences"]["include_action_items"]
        )
        
        self.delivery.send(html, subject="Your Daily Briefing")
        self.log_info("Briefing delivered successfully")


# Schedule it
schedule = Schedule(
    workflow=DailyBriefingWorkflow,
    cron="47 5 * * *",  # 5:47am every day
    timezone="America/New_York"
)
schedule.register()

That's the complete pipeline. In production, you'd also want to wire up the observability features โ€” each run automatically generates a trace that you can review:

openclaw traces list --workflow daily_briefing --last 7d
openclaw traces inspect <trace_id>

This gives you full visibility into exactly what happened: how many articles were fetched, which sources failed, what the token costs were, how the personalization scored each section, and why the final briefing looks the way it does. When something goes wrong on day 15, you'll know exactly why in about 30 seconds.

Step 6: Adding a Feedback Loop

This is optional but significantly improves quality over time. Add thumbs up/down buttons to your email (or use a simple webhook), then feed that back into the memory store:

# api/feedback.py
from openclaw import MemoryStore
from fastapi import FastAPI

app = FastAPI()
memory = MemoryStore(provider="lancedb")

@app.post("/feedback")
async def record_feedback(section_id: str, rating: str):
    """Rating: 'up' or 'down'"""
    memory.record_feedback(section_id, positive=(rating == "up"))
    return {"status": "recorded"}

After two to three weeks of occasional feedback, the personalization agent noticeably improves. It starts learning that you care about funding rounds over product launches, or that you prefer technical deep-dives over business analysis. This isn't magic โ€” it's just adjusting relevance weights based on your actual engagement patterns.

Deployment: Making It Actually Run Every Morning

For personal use, the simplest deployment is Docker Compose:

# docker-compose.yml
version: '3.8'
services:
  openclaw-worker:
    build: .
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - NEWSAPI_KEY=${NEWSAPI_KEY}
      - SMTP_PASSWORD=${SMTP_PASSWORD}
    volumes:
      - ./data:/app/data  # Persistent vector cache + memory
    depends_on:
      - redis
      
  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

  openclaw-scheduler:
    build: .
    command: openclaw scheduler start
    depends_on:
      - redis
      - openclaw-worker

volumes:
  redis_data:

Then:

docker-compose up -d

That's it. The scheduler handles the cron-like triggering, Redis handles the message queue, and your workflow runs every morning at 5:47am. If a step fails, it retries with exponential backoff. If the whole thing fails, you get a notification (configure your alert channel in the OpenClaw settings). If it succeeds โ€” and it will, almost every time โ€” you wake up to a clean, personalized briefing in your inbox.

What This Actually Costs

Let me be real about numbers. My daily briefing processes roughly 40โ€“60 articles per day across 8 sources. Here's my monthly cost breakdown:

  • GPT-4o-mini (individual summaries): ~$2.50/month
  • GPT-4o (synthesis, ~5 sections/day): ~$4.80/month
  • NewsAPI: Free tier (100 requests/day is plenty)
  • Infrastructure: I run this on a $5/month VPS

Total: ~$12.30/month. That's less than any newsletter subscription, and it's custom-built for exactly what I need to know.

If you swap the synthesis model for a capable local model (Mistral or Llama 3 via Ollama), the API costs drop to under $3/month. The quality trade-off is real but manageable if you're primarily doing news summarization rather than complex analysis.

Getting Started Today

Here's what I'd actually recommend as your sequence of actions:

  1. Grab Felix's OpenClaw Starter Pack. Seriously, this saves you the most annoying parts of setup โ€” the source connectors are pre-configured, the email templates look professional, and the deployment configs are battle-tested. Felix has done the boring infrastructure work so you can focus on customizing the content.

  2. Edit config/profile.yaml with your topics, sources, and preferences. Start with 3โ€“4 sources and 3 sections. You can add complexity later.

  3. Deploy with Docker Compose and let it run for a week before you start tweaking. The initial output will be good but not perfect. That's fine.

  4. Add the feedback loop in week two. Start clicking thumbs up/down. The personalization improves noticeably within 2โ€“3 weeks.

  5. Iterate on sources and exploration budget. I started with exploration_budget: 0.1 and bumped it to 0.15 after a month because the novel discoveries were consistently the most valuable part of my briefing.

The whole point of this is to build something that works tomorrow morning and keeps working. Not a demo. Not a prototype. A production system that respects your time and your attention. OpenClaw is the first framework I've used where that's actually the default outcome rather than the aspirational one.

Build the briefing. Ship it to your inbox. Wake up smarter. That's the entire pitch, and it delivers.

Recommended for this post

Your memory engineer that builds persistent context, tiered storage, and retrieval systems -- agents that remember.

Engineering
SpookyJuice.aiSpookyJuice.ai
$14Buy

Your orchestrator that coordinates agent swarms with task decomposition and consensus protocols -- agents working together.

Engineering
SpookyJuice.aiSpookyJuice.ai
$14Buy
Helios

Helios

Persona

The Elite Agent Architect. Your complete professional partner for building world-class AI agents.

Productivity
Just DanJust Dan
$19Buy

Claw Mart Daily

Get one AI agent tip every morning

Free daily tips to make your OpenClaw agent smarter. No spam, unsubscribe anytime.

More From the Blog