Auto-Generate Weekly Analytics Reports as PDFs with LangChain and AgentGen
Every week someone on your team exports a spreadsheet, copies numbers into a slide deck, and sends it around as a PDF. It takes two hours and looks slightly different every time. In this tutorial we'll automate the whole pipeline: a LangChain agent reads raw metrics, writes an executive summary, builds a styled HTML report, and renders it to a pixel-perfect PDF via AgentGen — all in under 15 seconds.
Architecture
The pipeline has three stages:
- Analyze — Claude reads the raw numbers and writes narrative: an executive summary, highlights, and areas of concern.
- Render — A Python template function turns the structured report object into styled HTML with metric cards, a color-coded summary section, and a highlights/concerns grid.
- Convert — AgentGen renders the HTML to a PDF via headless Chrome and returns a CDN URL.
The LLM handles the hard part — interpreting numbers and writing prose. AgentGen handles the part that demands consistency — turning finished HTML into an identical PDF every run.
Setup
pip install langchain langchain-anthropic requests python-dotenv
Data schema
Define typed dataclasses for your weekly metrics. These flow through the whole pipeline — into the LLM prompt as JSON, into the HTML template as values, and finally into the PDF:
from dataclasses import dataclass, field
from typing import List
@dataclass
class WeeklyMetric:
label: str
value: str # pre-formatted, e.g. "$48,200" or "12,847"
change_pct: float # positive = up vs last week, negative = down
note: str = "" # optional context shown under the value
@dataclass
class WeeklyReport:
week_ending: str
product_name: str
metrics: List[WeeklyMetric]
executive_summary: str = ""
highlights: List[str] = field(default_factory=list)
concerns: List[str] = field(default_factory=list)
Step 1 — LLM analysis tool
Pass the raw metrics to Claude and get back structured analysis. Using a @tool decorator makes it easy to slot this into a LangChain pipeline or agent:
import json
from langchain_core.tools import tool
from langchain_anthropic import ChatAnthropic
_llm = ChatAnthropic(model="claude-opus-4-6", temperature=0.2)
@tool
def analyze_metrics(metrics_json: str) -> str:
"""Analyze a JSON array of weekly metrics. Returns JSON with keys:
summary (string), highlights (list of strings), concerns (list of strings)."""
response = _llm.invoke(
f"""You are a sharp business analyst. Analyze these weekly metrics and return a JSON object with:
- "summary": a 2-3 sentence executive overview (use specific numbers)
- "highlights": 2-3 positive trends worth celebrating
- "concerns": 1-2 areas that need attention
Metrics:
{metrics_json}
Return only valid JSON, no markdown fences."""
)
return response.content
Step 2 — HTML report template
A Python function that turns a populated WeeklyReport into a styled HTML dashboard. Metric cards are color-coded by direction; highlights and concerns get distinct green/red panels:
def build_report_html(report: WeeklyReport) -> str:
def metric_card(m: WeeklyMetric) -> str:
positive = m.change_pct >= 0
color = "#16a34a" if positive else "#dc2626"
arrow = "↑" if positive else "↓"
sign = "+" if positive else ""
note_html = (
f'<p style="margin:4px 0 0;font-size:11px;color:#9ca3af">{m.note}</p>'
if m.note else ""
)
return f"""<div style="background:white;border:1px solid #e5e7eb;border-radius:10px;padding:20px;flex:1;min-width:140px">
<p style="margin:0 0 8px;font-size:11px;text-transform:uppercase;color:#9ca3af;letter-spacing:.05em">{m.label}</p>
<p style="margin:0 0 4px;font-size:28px;font-weight:800;color:#111">{m.value}</p>
<p style="margin:0;font-size:13px;font-weight:600;color:{color}">{arrow} {sign}{m.change_pct:.1f}% vs last week</p>
{note_html}
</div>"""
cards_html = "".join(metric_card(m) for m in report.metrics)
highlights_html = "".join(
f'<li style="margin-bottom:6px;color:#166534">{h}</li>'
for h in report.highlights
)
concerns_html = "".join(
f'<li style="margin-bottom:6px;color:#991b1b">{c}</li>'
for c in report.concerns
)
return f"""<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head>
<body style="margin:0;padding:36px;background:#f9fafb;font-family:'Helvetica Neue',Arial,sans-serif;color:#111">
<!-- Header -->
<div style="background:linear-gradient(135deg,#1e1b4b,#3730a3);border-radius:12px;padding:28px 32px;color:white;margin-bottom:28px">
<h1 style="margin:0 0 6px;font-size:24px;font-weight:800">{report.product_name}</h1>
<p style="margin:0;opacity:.75;font-size:13px">Weekly Performance Report · Week ending {report.week_ending}</p>
</div>
<!-- Executive Summary -->
<div style="background:white;border:1px solid #e5e7eb;border-radius:10px;padding:22px;margin-bottom:22px">
<p style="margin:0 0 10px;font-size:11px;text-transform:uppercase;color:#9ca3af;letter-spacing:.05em">Executive Summary</p>
<p style="margin:0;line-height:1.75;color:#374151;font-size:15px">{report.executive_summary}</p>
</div>
<!-- Metric cards -->
<div style="display:flex;gap:14px;flex-wrap:wrap;margin-bottom:22px">
{cards_html}
</div>
<!-- Highlights / Concerns -->
<div style="display:flex;gap:14px;margin-bottom:28px">
<div style="flex:1;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:10px;padding:18px">
<p style="margin:0 0 10px;color:#15803d;font-size:11px;text-transform:uppercase;letter-spacing:.05em;font-weight:600">Highlights</p>
<ul style="margin:0;padding-left:18px">{highlights_html}</ul>
</div>
<div style="flex:1;background:#fef2f2;border:1px solid #fecaca;border-radius:10px;padding:18px">
<p style="margin:0 0 10px;color:#dc2626;font-size:11px;text-transform:uppercase;letter-spacing:.05em;font-weight:600">Watch</p>
<ul style="margin:0;padding-left:18px">{concerns_html}</ul>
</div>
</div>
<p style="text-align:center;color:#d1d5db;font-size:11px">
Auto-generated · {report.week_ending} · Powered by AgentGen
</p>
</body></html>"""
Step 3 — PDF generation tool
import os
import requests
from pydantic import BaseModel, Field
from langchain_core.tools import StructuredTool
class RenderReportInput(BaseModel):
html: str = Field(description="Complete HTML report to convert to PDF")
def _render_to_pdf(html: str) -> dict:
response = requests.post(
"https://www.agent-gen.com/api/v1/generate/pdf",
headers={"X-API-Key": os.environ["AGENTGEN_API_KEY"], "Content-Type": "application/json"},
json={
"html": html,
"format": "A4",
"margin": {"top": "10mm", "bottom": "10mm", "left": "10mm", "right": "10mm"},
},
timeout=30,
)
response.raise_for_status()
return response.json() # {"url": "...", "pages": 1, "tokens_used": 2, "request_id": "..."}
render_pdf_tool = StructuredTool.from_function(
func=_render_to_pdf,
name="render_report_pdf",
description="Render a complete HTML report to PDF. Returns a CDN download URL. Cost: 2 tokens/page.",
args_schema=RenderReportInput,
)
The full pipeline
Wire the three steps together. The LLM does the analysis; Python does the templating and PDF call:
def generate_weekly_report(
raw_metrics: list[dict],
product_name: str,
week_ending: str,
) -> str:
"""Full pipeline: raw data → LLM analysis → HTML → PDF. Returns a CDN URL."""
# 1. LLM analysis
analysis = json.loads(
analyze_metrics.invoke({"metrics_json": json.dumps(raw_metrics, indent=2)})
)
# 2. Assemble typed report object
report = WeeklyReport(
week_ending=week_ending,
product_name=product_name,
metrics=[
WeeklyMetric(
label=m["label"],
value=m["value"],
change_pct=m.get("change_pct", 0),
note=m.get("note", ""),
)
for m in raw_metrics
],
executive_summary=analysis.get("summary", ""),
highlights=analysis.get("highlights", []),
concerns=analysis.get("concerns", []),
)
# 3. Render HTML → PDF
result = _render_to_pdf(build_report_html(report))
return result["url"]
# Example run
raw_data = [
{"label": "Active Users", "value": "12,847", "change_pct": 8.3, "note": "All-time high"},
{"label": "Revenue", "value": "$48,200", "change_pct": 12.1},
{"label": "Churn Rate", "value": "2.1%", "change_pct": -0.4, "note": "Improving"},
{"label": "Avg Session", "value": "4m 32s", "change_pct": 5.7},
]
url = generate_weekly_report(
raw_metrics=raw_data,
product_name="AgentGen",
week_ending="March 2, 2026",
)
print(f"Report ready: {url}")
Scheduling with GitHub Actions
Drop this workflow file into your repo to auto-generate the report every Monday at 8 AM UTC and post the link to Slack:
# .github/workflows/weekly-report.yml
name: Weekly Report
on:
schedule:
- cron: "0 8 * * MON"
workflow_dispatch: # allow manual trigger
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install -r requirements.txt
- name: Generate report
run: python scripts/weekly_report.py
env:
AGENTGEN_API_KEY: ${{ secrets.AGENTGEN_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
In weekly_report.py, fetch last week's data from your database or analytics API, call generate_weekly_report(), and POST the resulting URL to Slack:
import requests, os, json
url = generate_weekly_report(fetch_last_week_metrics(), "My Product", last_monday())
requests.post(os.environ["SLACK_WEBHOOK_URL"], json={
"text": f":bar_chart: *Weekly report is ready* — <{url}|Download PDF>"
})
Cost breakdown
A typical two-page weekly report costs 4 tokens (~$0.06 at Growth tier). For a 10-person team receiving 52 reports a year that's $3.12 per year in PDF generation costs. The LLM analysis adds a few cents of Claude API cost per run.
Start free → New accounts get 50 tokens — enough for 12 full two-page reports to test the whole pipeline before spending anything.
Ready to start generating?
Create a free account and generate your first PDF or image in minutes.
Get started free