How to Automatically Generate and Send Weekly Reports via Email Using Python and Claude (2026 Guide)
How to Automatically Generate and Send Weekly Reports via Email Using Python and Claude
01. The Friday Afternoon Problem
Every Friday at 4:30 PM, I became a human spreadsheet. I opened twelve browser tabs, copy-pasted numbers into a Google Doc, wrote three sentences of commentary, and hit Send. Every single week.
It started as a quick ten-minute task. Then the team grew, the metrics expanded from five KPIs to twenty-three, and suddenly I was spending an hour every Friday afternoon doing the same mechanical work: pull sales data from our dashboard, pull support ticket counts from Zendesk, pull website sessions from Google Analytics, paste them all into an email template, and write a summary paragraph that said roughly the same thing every week with slightly different numbers.
The worst part wasn't the time. It was the context switching. That Friday afternoon reporting slot was prime problem-solving time — the kind of focused, uninterrupted hour where you could actually think. Instead it disappeared into a mindless data assembly job that any script could handle.
I finally automated it on a Tuesday evening when the Friday report was particularly late because I'd been stuck in meetings all day. The script took about two hours to build the first version. It has now been running automatically for over six months. It sends a polished, accurate HTML report every Friday morning at 9 AM — before I've even opened my laptop. This guide shows you exactly how to build the same thing.
Return to Operational Guide Outline02. What This Automation Actually Does
Not magic — just four steps that your computer executes while you're doing something more important.
The automation pipeline has four stages, each simple on its own but powerful in combination. First, it reads your weekly operational data from a source you define — this could be a CSV export, a Google Sheet, a database query, or a JSON file from your analytics tool. Second, it aggregates the key metrics: total revenue, units sold, resolution rates, and whatever else matters to your team. Third, it sends those aggregated numbers to Claude and asks for a concise executive summary in plain English. Fourth, it assembles a clean HTML email — complete with a formatted data table and the AI-written summary — and sends it via SMTP to your team distribution list.
The result is a report that looks like it was written by a diligent analyst who reviewed all the data and distilled the key insights. Because Claude did exactly that — reviewed all the data and distilled the key insights. The script takes about 8 to 12 seconds to run from start to finish, including the API call to Claude.
Return to Operational Guide Outline03. What You Need to Get Started
Three Python libraries and an email account. That's genuinely the full list.
Official Anthropic SDK. Sends your aggregated metrics to Claude and returns a plain-English executive summary paragraph.
Both built into Python's standard library. Construct and send HTML email messages via any SMTP server — Gmail, Outlook, SendGrid, etc.
Also built-in. Read your weekly data from CSV exports or JSON files produced by your analytics dashboard or database.
Pure Python job scheduler. Lets you define "run every Friday at 09:00" in plain code without touching cron syntax on Linux or Task Scheduler on Windows.
pip install anthropic schedule
You'll also need a Claude API key from console.anthropic.com and a Gmail app password (or any SMTP credentials). Store both as environment variables:
$env:ANTHROPIC_API_KEY = "sk-ant-your-key-here"
$env:SMTP_PASSWORD = "your-gmail-app-password"
04. Step 1 — Load and Aggregate Your Data
Before Claude can summarize anything, you need to know what the numbers actually are. This step turns raw data into clean metrics.
import json
from datetime import date
def load_weekly_data(filepath: str) -> dict:
"""Load your weekly operational data from a JSON file."""
with open(filepath, encoding="utf-8") as f:
return json.load(f)
def aggregate_metrics(data: dict) -> dict:
"""Compute summary metrics from raw weekly data."""
total_revenue = sum(item["revenue"] for item in data["sales"])
total_units = sum(item["units"] for item in data["sales"])
top_product = max(data["sales"], key=lambda x: x["revenue"])
tickets = data["support_tickets"]
resolution_rate = round(
tickets["resolved"] / tickets["opened"] * 100, 1
) if tickets["opened"] > 0 else 0.0
return {
"week_ending": data.get("week_ending", str(date.today())),
"total_revenue": total_revenue,
"total_units": total_units,
"top_product": top_product["product"],
"resolution_rate": resolution_rate,
"sessions": data["website_traffic"]["sessions"],
"bounce_rate": data["website_traffic"]["bounce_rate"],
}
# Usage
data = load_weekly_data("weekly_data.json")
metrics = aggregate_metrics(data)
print(f"Total Revenue: ${metrics['total_revenue']:.2f}")
print(f"Resolution Rate: {metrics['resolution_rate']}%")
💡 Pro tip: If your data lives in a CSV file, replace json.load() with the csv.DictReader pattern. If it's in a Google Sheet, the gspread library gives you a one-line sheet-to-dict conversion. The rest of the pipeline stays identical.
05. Step 2 — Let Claude Write the Summary
The most time-consuming part of any weekly report is the commentary. Claude handles it in under three seconds.
The key to a useful AI summary is giving Claude the right context in the prompt. Don't just dump raw numbers — give it the business context so the summary reads like it came from someone who understands what the numbers mean. Compare these two prompts:
Weak prompt: "Summarize this data: revenue 4528, units 123, rate 78.6%."
Strong prompt: "You are an operations analyst. Review this week's performance data and write a 2-sentence executive summary highlighting what went well and any metric that warrants attention."
import anthropic
import os
def generate_summary_with_claude(metrics: dict, raw_data: dict) -> str:
"""Send weekly metrics to Claude. Returns a plain-English summary."""
client = anthropic.Anthropic(
api_key=os.environ.get("ANTHROPIC_API_KEY")
)
prompt = f"""You are an operations analyst writing a weekly executive summary.
Review this week's business performance data and write exactly 3 sentences:
1. Overall revenue and top product performance.
2. Support team efficiency and outstanding issues.
3. One forward-looking observation or recommendation.
Write in confident, professional business English. No bullet points. Plain prose only.
WEEKLY DATA:
- Total Revenue: ${metrics['total_revenue']:.2f}
- Total Units Sold: {metrics['total_units']}
- Top Revenue Product: {metrics['top_product']}
- Support Resolution Rate: {metrics['resolution_rate']}%
- Website Sessions: {metrics['sessions']:,}
- Bounce Rate: {metrics['bounce_rate']*100:.0f}%
- Week Ending: {metrics['week_ending']}
"""
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=512,
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
# Usage
summary = generate_summary_with_claude(metrics, data)
print(f"Summary ({len(summary)} chars):\n{summary}")
06. Step 3 — Build the HTML Email
A plain-text report gets ignored. A clean HTML email with a table and color gets read.
The email module in Python's standard library handles HTML emails natively. You construct a MIMEMultipart("alternative") message, attach your HTML string as the "html" part, and the recipient's email client renders it as a formatted report — complete with your data table, the AI summary highlighted in a colored block, and your company branding.
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
def build_html_email(metrics: dict, summary: str, sales_data: list) -> str:
"""Build a clean, styled HTML email body."""
rows = "".join(
f"<tr><td>{item['product']}</td>"
f"<td>{item['units']}</td>"
f"<td>${item['revenue']:.2f}</td></tr>"
for item in sales_data
)
return f"""
<html><body style="font-family:Arial,sans-serif;max-width:650px;margin:auto;">
<div style="background:#065f46;color:#fff;padding:30px;border-radius:12px 12px 0 0;">
<h1 style="margin:0;">Weekly Operations Report</h1>
<p style="margin:8px 0 0;opacity:0.8;">Week Ending: {metrics['week_ending']}</p>
</div>
<div style="background:#f0fdf4;padding:25px;border-left:4px solid #10b981;">
<strong>AI Executive Summary</strong><br><br>{summary}
</div>
<div style="padding:25px;">
<h2 style="color:#065f46;">Sales Performance</h2>
<table width="100%" cellpadding="10" style="border-collapse:collapse;">
<tr style="background:#d1fae5;">
<th align="left">Product</th>
<th>Units</th><th>Revenue</th>
</tr>
{rows}
<tr style="border-top:2px solid #10b981;font-weight:bold;">
<td>TOTAL</td>
<td>{metrics['total_units']}</td>
<td>${metrics['total_revenue']:.2f}</td>
</tr>
</table>
<p style="margin-top:20px;">
Support Rate: <strong>{metrics['resolution_rate']}%</strong> |
Sessions: <strong>{metrics['sessions']:,}</strong>
</p>
</div>
</body></html>
"""
def create_email_message(to: str, subject: str, html_body: str) -> MIMEMultipart:
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = "reports@yourcompany.com"
msg["To"] = to
msg.attach(MIMEText(html_body, "html", "utf-8"))
return msg
07. The Complete Script (Copy-Paste Ready)
Everything from Steps 1 through 6, assembled into one clean script you can deploy immediately.
import json, os, smtplib
import anthropic
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from datetime import date
SMTP_HOST = "smtp.gmail.com"
SMTP_PORT = 587
FROM_EMAIL = os.environ.get("REPORT_FROM_EMAIL", "reports@yourcompany.com")
TO_EMAIL = os.environ.get("REPORT_TO_EMAIL", "team@yourcompany.com")
SMTP_PASS = os.environ.get("SMTP_PASSWORD")
API_KEY = os.environ.get("ANTHROPIC_API_KEY")
def load_and_aggregate(filepath: str) -> tuple:
with open(filepath, encoding="utf-8") as f:
data = json.load(f)
total_rev = sum(i["revenue"] for i in data["sales"])
total_unit = sum(i["units"] for i in data["sales"])
top = max(data["sales"], key=lambda x: x["revenue"])
t = data["support_tickets"]
metrics = {
"week_ending": data.get("week_ending", str(date.today())),
"total_revenue": total_rev,
"total_units": total_unit,
"top_product": top["product"],
"resolution_rate": round(t["resolved"] / t["opened"] * 100, 1),
"sessions": data["website_traffic"]["sessions"],
"bounce_rate": data["website_traffic"]["bounce_rate"],
}
return metrics, data["sales"]
def get_claude_summary(metrics: dict) -> str:
client = anthropic.Anthropic(api_key=API_KEY)
prompt = (
f"Write a 3-sentence weekly business summary.\n"
f"Revenue: ${metrics['total_revenue']:.2f} | "
f"Top Product: {metrics['top_product']} | "
f"Support Rate: {metrics['resolution_rate']}% | "
f"Sessions: {metrics['sessions']:,}.\n"
"Be concise, professional, and insightful."
)
r = client.messages.create(
model="claude-sonnet-4-5", max_tokens=400,
messages=[{"role": "user", "content": prompt}]
)
return r.content[0].text
def send_report(metrics: dict, sales: list, summary: str):
rows = "".join(
f"{i['product']} {i['units']} "
f"${i['revenue']:.2f} " for i in sales
)
html = (
f""
f""
f"Weekly Report — {metrics['week_ending']}
"
f""
f"AI Summary
{summary}"
f""
f""
f"Product "
f"Units Revenue {rows}"
f""
f"TOTAL {metrics['total_units']} "
f"${metrics['total_revenue']:.2f}
"
f""
)
msg = MIMEMultipart("alternative")
msg["Subject"] = f"[Weekly Report] {metrics['week_ending']}"
msg["From"] = FROM_EMAIL
msg["To"] = TO_EMAIL
msg.attach(MIMEText(html, "html", "utf-8"))
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
server.starttls()
server.login(FROM_EMAIL, SMTP_PASS)
server.sendmail(FROM_EMAIL, TO_EMAIL, msg.as_bytes())
print(f"[SENT] Report dispatched to {TO_EMAIL}")
if __name__ == "__main__":
metrics, sales = load_and_aggregate("weekly_data.json")
summary = get_claude_summary(metrics)
send_report(metrics, sales, summary)
08. Scheduling It to Run Automatically
A script you have to remember to run is not automation. A script that runs itself — that's the goal.
Add 4 lines to your script. No system-level configuration needed. Requires the Python process to stay running.
import schedule, time
def weekly_report_job():
metrics, sales = load_and_aggregate("weekly_data.json")
summary = get_claude_summary(metrics)
send_report(metrics, sales, summary)
# Run every Friday at 09:00
schedule.every().friday.at("09:00").do(weekly_report_job)
while True:
schedule.run_pending()
time.sleep(60)
Right-click Task Scheduler > Create Basic Task > Weekly, Friday, 09:00 > Start a Program: python C:\path\to\weekly_report.py. Survives reboots. Runs even if you're not logged in.
Add to crontab with crontab -e: 0 9 * * 5 /usr/bin/python3 /home/user/weekly_report.py — runs at 9 AM every Friday automatically.
09. Real-World Variations
The pattern adapts to whatever data your team actually cares about.
The pipeline described in this guide uses sales revenue, support tickets, and website sessions as the example dataset. But the architecture is entirely data-agnostic. Your weekly data could come from anywhere and track anything — as long as you can load it into a Python dictionary, the aggregation and AI summary steps work identically.
Load orders CSV, aggregate by product category and fulfillment status. Claude summarizes inventory risk and top-selling SKUs automatically.
Pull MRR delta, churn count, and DAU from your analytics API. Claude generates a growth narrative with retention flags.
Load campaign spend and conversion data. Claude produces a budget efficiency summary with ROAS comparison across ad groups.
Load sprint velocity, bugs opened, and deployment frequency. Claude writes a delivery cadence assessment with bottleneck identification.
10. What's Next
Once the weekly report runs on autopilot, the next question is obvious: what else can be automated the same way?
The script you've built today follows a pattern that generalizes to almost any routine business communication: load data, compute metrics, generate natural-language commentary with Claude, format as a professional document, and dispatch automatically on a schedule. That same pattern handles daily standup summaries, monthly board packs, quarterly business reviews, customer-facing usage reports, and supplier performance scorecards.
The practical next upgrade for most teams is connecting the data loader directly to a live source rather than a manually exported JSON file. The gspread library lets you pull directly from Google Sheets. The requests library lets you query any REST API with a token. The psycopg2 library connects to PostgreSQL. Once you replace the static JSON load with a live data pull, the entire workflow becomes truly zero-touch — the script fetches, aggregates, summarizes, and sends without any human input at any stage.
If your team receives the reports but someone still needs to act on them — following up on pending support tickets, adjusting ad spend based on ROAS — the next evolution is adding a decision layer. After Claude writes the summary, you can pass the same metrics through a second Claude prompt that generates a prioritized action list based on your team's predefined business rules. The report stops being a passive information document and becomes an active directive that tells people exactly what to do next. That's the transition from automation to agentic operations.
Return to Operational Guide OutlineAutomated reports are a great start. The next level is software that manages its own data pipelines, payments, and infrastructure without human supervision. The Master Class series covers the full architectural blueprint for building truly autonomous systems.
Read: Decentralized AI Agent Marketplaces →