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
7 min read
Sponsored
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:
- Data collection is automated. Numbers are pulled from APIs on a schedule, not by a human logging into dashboards.
- 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.
- Narrative is AI-assisted. An LLM takes the structured data and drafts the explanation paragraphs. A human reviews, edits, and sends.
- 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
More from this category
More from Business
Product-Led Growth for SaaS: How Developer Tools Get to Enterprise Without a Sales Army
Project Post-Mortems for Agencies: The Debrief Habit That Makes Teams Better
Writing Technical Specifications That Clients Will Actually Sign Off On
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.
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored