Skip to content

Business · Agency Operations

AI-Automated Client Reporting: How Agencies Are Cutting 5 Hours Per Client Per Month

Manually compiling monthly reports is one of the highest-effort, lowest-value activities in a web agency. Here's how to replace most of that work with automated pipelines.

Anurag Verma

Anurag Verma

7 min read

AI-Automated Client Reporting: How Agencies Are Cutting 5 Hours Per Client Per Month

Sponsored

Share

The end of the month arrives and someone on the team spends three hours pulling numbers from Google Analytics, another hour from the ad platform, thirty minutes checking uptime logs, fifteen minutes finding last month’s report to compare against, and then another hour formatting everything into a PDF that the client will skim for two minutes before asking the same question they ask every month.

This pattern plays out across hundreds of agencies. The reporting itself has value — clients need to know what happened and whether it was good — but the labor that goes into producing it does not scale. With ten clients, it’s manageable. With thirty, it consumes a junior person’s entire last week of every month.

The reporting pipeline can be automated. Not fully, but enough to change the math significantly.

What “Automated Reporting” Actually Means

Automated reporting doesn’t mean a script that dumps raw data into a PDF and emails it. That produces unreadable reports that erode trust. What it means in practice is:

  1. Data collection is automated. Numbers are pulled from APIs on a schedule, not by a human logging into dashboards.
  2. Analysis is structured. The script identifies notable changes (traffic up 18%, bounce rate down, a page with significantly higher exits than usual) so the human reviewer focuses on things worth flagging.
  3. Narrative is AI-assisted. An LLM takes the structured data and drafts the explanation paragraphs. A human reviews, edits, and sends.
  4. Context is retained. The system knows what was reported last month, what goals exist for this client, and what changes were made to the site recently.

The human’s job shifts from data gathering and formatting to reviewing and adding judgment. For a 90-minute-per-client monthly report, this typically reduces the time to 20-30 minutes.

Building the Data Collection Layer

The first step is getting data into a structured format automatically. Every major platform has an API:

Google Analytics 4:

from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import (
    DateRange, Dimension, Metric, RunReportRequest
)

def get_monthly_traffic(property_id: str, start_date: str, end_date: str):
    client = BetaAnalyticsDataClient()
    request = RunReportRequest(
        property=f"properties/{property_id}",
        dimensions=[Dimension(name="pagePath")],
        metrics=[
            Metric(name="sessions"),
            Metric(name="bounceRate"),
            Metric(name="averageSessionDuration"),
        ],
        date_ranges=[DateRange(start_date=start_date, end_date=end_date)],
        order_bys=[{"metric": {"metric_name": "sessions"}, "desc": True}],
        limit=20,
    )
    response = client.run_report(request)
    return response

Search Console:

from googleapiclient.discovery import build

def get_search_performance(site_url: str, start_date: str, end_date: str, credentials):
    service = build('searchconsole', 'v1', credentials=credentials)
    response = service.searchanalytics().query(
        siteUrl=site_url,
        body={
            'startDate': start_date,
            'endDate': end_date,
            'dimensions': ['query'],
            'rowLimit': 25,
            'orderBy': [{'fieldName': 'clicks', 'sortOrder': 'DESCENDING'}],
        }
    ).execute()
    return response.get('rows', [])

Uptime from BetterStack/StatusPage:

import httpx

def get_uptime_summary(api_token: str, monitor_id: str):
    headers = {"Authorization": f"Bearer {api_token}"}
    response = httpx.get(
        f"https://uptime.betterstack.com/api/v2/monitors/{monitor_id}/sla",
        headers=headers,
        params={"from": "30d"},
    )
    data = response.json()
    return {
        "uptime_percentage": data["data"]["attributes"]["availability"],
        "total_downtime_minutes": data["data"]["attributes"]["total_downtime_duration"] / 60,
    }

Schedule these collection scripts to run on the last day of the month and store results in a database or JSON file per client. The key is storing not just this month’s numbers but last month’s, so the analysis step can compute changes.

The Analysis Step

Raw numbers are not a report. The analysis step transforms data into findings:

def analyze_traffic_changes(current: dict, previous: dict) -> list[dict]:
    findings = []
    
    sessions_change = (current["sessions"] - previous["sessions"]) / previous["sessions"]
    if abs(sessions_change) > 0.1:  # 10% threshold
        direction = "increased" if sessions_change > 0 else "decreased"
        findings.append({
            "type": "traffic",
            "severity": "high" if abs(sessions_change) > 0.25 else "medium",
            "message": f"Overall sessions {direction} by {abs(sessions_change):.0%} vs last month",
            "data": {
                "current": current["sessions"],
                "previous": previous["sessions"],
                "change_pct": sessions_change,
            }
        })
    
    # Check for pages with anomalous bounce rates
    for page in current["top_pages"]:
        prev_page = next((p for p in previous["top_pages"] if p["path"] == page["path"]), None)
        if prev_page:
            bounce_change = page["bounce_rate"] - prev_page["bounce_rate"]
            if bounce_change > 0.15:  # bounce rate jumped 15 points
                findings.append({
                    "type": "bounce_rate",
                    "severity": "medium",
                    "message": f"Bounce rate on {page['path']} increased significantly",
                    "data": {
                        "page": page["path"],
                        "current_bounce": page["bounce_rate"],
                        "previous_bounce": prev_page["bounce_rate"],
                    }
                })
    
    return findings

This structured output feeds directly into the next step: generating the narrative.

AI-Drafted Narrative

With structured findings in hand, generating readable paragraphs becomes straightforward:

import anthropic

def draft_report_section(client_name: str, findings: list[dict], context: dict) -> str:
    client = anthropic.Anthropic()
    
    findings_text = "\n".join([
        f"- {f['message']} (severity: {f['severity']})" 
        for f in findings
    ])
    
    prompt = f"""You are writing a monthly performance report section for {client_name}.

Client goals: {context['goals']}
Recent changes: {context['recent_changes']}

This month's key findings:
{findings_text}

Write 2-3 paragraphs summarizing performance this month. Use plain language suitable 
for a non-technical client. Focus on what changed and why it matters, not raw numbers. 
Do not invent data not listed above. Do not use jargon. Keep it under 200 words."""

    message = client.messages.create(
        model="claude-opus-4-8",
        max_tokens=400,
        messages=[{"role": "user", "content": prompt}]
    )
    
    return message.content[0].text

The output is a draft, not a final product. The account manager reads it, adjusts the tone for the specific client relationship, adds any context they know that the data doesn’t capture (a campaign that ran, a seasonal factor, a known technical issue), and sends it.

This is the key design principle: the AI drafts, the human signs off. Reports that go out unreviewed are a risk. Clients notice when the tone is off or when the report ignores something they discussed in the last call.

Storing Client Context

The system is only as useful as the context it has about each client. Build a simple context file per client:

{
  "client_id": "acme-corp",
  "name": "Acme Corp",
  "goals": [
    "Increase organic search traffic by 20% before Q3",
    "Reduce homepage bounce rate below 55%",
    "Drive 50+ monthly demo request form submissions"
  ],
  "recent_changes": [
    "2026-05-15: Launched redesigned pricing page",
    "2026-05-22: Migrated blog to new CMS",
    "2026-06-01: Started Google Ads campaign for enterprise tier"
  ],
  "kpis": {
    "primary": ["sessions", "demo_submissions"],
    "secondary": ["bounce_rate", "avg_session_duration", "search_clicks"]
  },
  "report_tone": "formal",
  "monthly_baseline": {
    "sessions": 4200,
    "demo_submissions": 31
  }
}

Update recent_changes after every deployment or campaign launch. This is a ten-second task if done immediately; it’s an unreliable reconstruction job if done a month later.

The Delivery Layer

Once the draft is reviewed, the final step is formatting and delivery. Options:

PDF via HTML template. A simple approach: populate an HTML template with the data and draft text, then convert to PDF using a headless browser. Libraries like playwright or weasyprint handle this well.

Notion or Google Doc. Use the API to populate a shared document the client has access to. They can comment, ask questions, and compare months. This often generates more engagement than a PDF attachment.

Email with inline summary. A short inline summary + “see attached for details” format. The inline summary is three sentences the AI generated from the top findings. Most clients read the email; fewer open attachments.

What to Realistically Expect

Automation reduces the data-gathering and formatting work nearly to zero. Analysis automation catches the obvious things but misses context. Narrative generation produces a readable first draft but not a final one.

The honest accounting looks like this: a 90-minute manual report becomes 20-30 minutes of reviewing, editing, and adding human context. That’s a real improvement. It is not “fully automated reporting.”

The goal is for the account manager to spend their time on judgment calls (what does this bounce rate jump mean for the client’s goals?), not on copying numbers between tabs. That’s a better use of the person, and it produces a better report.

Getting Started

Pick your highest-volume, most-formulaic reporting client. Manually document every data source you pull from and every number you include. Then automate the collection for those sources and write the analysis rules. The first time takes a day. The second time takes a few hours because you’re reusing the framework.

By the fifth client, you’ll have a system that runs the collection automatically and hands a reviewed draft to the account manager on the last business day of the month.

Sponsored

Enjoyed it? Pass it on.

Share this article.

Sponsored

The dispatch

Working notes from
the studio.

A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.

No spam, ever. Unsubscribe anytime.

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored