← All posts
·9 min read

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:

  1. Analyze — Claude reads the raw numbers and writes narrative: an executive summary, highlights, and areas of concern.
  2. 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.
  3. 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 &middot; 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 &middot; {report.week_ending} &middot; 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