mcpbeat Sign in

Reporting Agent Skill

Use when a recurring report must ship itself on a cadence — weekly digest, monthly exec pack, board pack — via a fetch→narrate→render→deliver pipeline with a schedule and a freshness gate. NOT a live view people slice (that is `dashboard`), NOT picking which KPIs to track (that is `kpi-framework`), NOT a one-off investigation (that is `analytics`).

8k tokens
context cost
the whole folder, loaded on every use
5
files
ships runnable scripts
0
copies elsewhere
how many repositories repackaged it
105
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/ericrisco/rsc-harness --skill reporting

What comes with it

19 658 bytes besides the instruction
evals/README.md
evals/cases.yaml
references/pipeline.md
scripts/verify.sh

The instruction itself

9 sections, as written by the author

reporting — the standing artifact that ships itself

A report is a push artifact: a fixed snapshot that lands in someone's inbox or shared drive on a

cadence, the same shape every period, with no human assembling it. That is the whole job. If a human

opens a live view to slice numbers themselves, that is pull — a

dashboard, not a report. Hold that line; almost every failed "reporting"

project is a dashboard wearing a report's name, or a report nobody can trust because the numbers went

stale and no one noticed.

Your deliverable is a runnable pipeline, not advice: a template, a generation script, a schedule,

and a delivery step, with a freshness gate so it fails loud instead of shipping yesterday's numbers.

Step 0 — Write the report contract (no contract, no build)

Before any code, pin six fields. An undefined audience is a report no one reads; an undefined source

is a number no one can defend.

| Field | What it fixes | Example |

| --- | --- | --- |

| Audience | Who reads it → sets depth and tone | "5-person leadership team" |

| Cadence | When it ships → sets the scheduler | "every Monday 08:00 Europe/Madrid" |

| Sections | The fixed skeleton, same every period | Exec summary · revenue · pipeline · risks |

| Metric source | Where numbers come from + who owns them | metrics.csv from the data team |

| Channel | The artifact + transport | PDF over email |

| Owner | Who gets paged when it breaks | a named person, not "the team" |

Rule: you consume an agreed metric set — you do not adjudicate which KPIs matter. If the ask is

"which metrics should we even track?", that is kpi-framework, not this

skill. The contract names the source and its owner so a wrong number has an address.

Decide: push report vs live dashboard (this branches — settle it first)

Audiences want a citable snapshot for the period more often than a live feed — that is why push wins

for exec summaries, weekly reviews, and finance close packs.

| Signal in the ask | Route |

| --- | --- |

| "Same summary, every week/month/quarter" | reporting (here) |

| "Consistent snapshot the board can cite in a meeting" | reporting (here) |

| "It must arrive even if no one logs in" | reporting (here) |

| "A screen people open to filter/drill live" | dashboard |

| "Self-serve, slice by region/date on demand" | dashboard |

When both are wanted, build the report and let it link to the dashboard as a "go deeper" footer.

Pick the artifact format

| Format | Wins when | Notes |

| --- | --- | --- |

| Email-body HTML | Short digest, ≤1 screen, read on a phone | Inline the key numbers; no attachment to open |

| PDF | Exec / client / board, fixed layout that must look identical for everyone | The default for anything formal; pinned render stack below |

| Spreadsheet | Analysts who will re-filter and pivot the data themselves | Hand off the data, not a picture → spreadsheet-ops |

| Notion / living doc | Internal team, the doc evolves and gets commented | Page over file → notion-connector |

A board pack is a PDF; an analyst hand-off is a spreadsheet. Picking PDF for analysts who wanted to

pivot is the most common format miss.

The four-layer pipeline: fetch → narrate → render → deliver

Keep the four layers separate so each is testable and the template never knows where data came from.

Pinned stack (Python, the most-documented 2025 PDF path — verified on PyPI 2026-06-02):

pandas (data) → Jinja2 (HTML template) → WeasyPrint 68.1 (HTML+CSS → PDF), Matplotlib

for embedded charts. Pin WeasyPrint — major versions change CSS support, so an unpinned bump can

silently reflow a board pack. Re-check PyPI before you freeze, then freeze what you resolve.

# render.py — minimal fetch → narrate → render. Delivery + charts live in references/pipeline.md.
import pandas as pd
from jinja2 import Environment, FileSystemLoader
from weasyprint import HTML  # WeasyPrint==68.1

def build_report(metrics_csv: str, params: dict) -> bytes:
    df = pd.read_csv(metrics_csv)                      # fetch
    df = df[df["region"] == params["region"]]          # per-recipient scope, NOT a new template
    cur, prev = df["revenue"].iloc[-1], df["revenue"].iloc[-2]
    ctx = {
        "title": f'{params["region"]} weekly revenue',
        "period": params["period"],
        "summary": narrate(cur, prev),                 # narrate (see below)
        "rows": df.tail(8).to_dict("records"),
    }
    env = Environment(loader=FileSystemLoader("templates"))
    html = env.get_template("report.html.j2").render(**ctx)   # render
    return HTML(string=html).write_pdf()

Rule: one template, per-recipient params — regional managers each get their region from the same

report.html.j2, scoped by params, never a forked template per recipient. A template-per-recipient

codebase rots the first time the layout changes. The full worked template, the Matplotlib base64 chart

embed, the per-recipient loop, the GitHub Actions workflow, and the freshness gate are in

references/pipeline.md.

The narrative layer (the part that gets read)

Executives read the executive summary and stop. Lead every report with: the report's purpose in

one line, 3 key takeaways, and what changed versus last period. Then per section, one

period-over-period delta sentence and one "so what". An annotated chart callout is retained; a bare

chart is not.

def narrate(cur: float, prev: float) -> str:
    delta = (cur - prev) / prev * 100
    arrow = "up" if delta >= 0 else "down"
    flag = " — investigate" if abs(delta) >= 20 else ""    # anomaly callout
    return (f"Revenue {arrow} {abs(delta):.1f}% vs last week "
            f"(€{cur:,.0f} from €{prev:,.0f}){flag}.")

Bad → Good, the difference that decides whether the report is useful:

  • Bad: a table row Revenue | 248,300 | 201,400. The reader must do the math and guess if it matters.
  • Good: "Revenue up 23.3% WoW (€248,300 from €201,400) — investigate: driven by the enterprise renewal that won't recur next week."

The numbers are the evidence; the sentence is the report.

Schedule + reliability gate (where reports actually die)

Reports die two ways: stale data ships silently, and a silent break means no one notices for

weeks. Treat the schedule as managed infrastructure.

Where to run it:

| Situation | Scheduler |

| --- | --- |

| You already run a server / cron host | cron (0 8 * * 1 = Mondays 08:00) |

| No server, want zero infra | GitHub Actions on: schedule: - cron: — the recommended cloud option |

| Multi-app event wiring beyond a timer | route the plumbing to automation-flows |

This skill owns the report-shaped concern (contract, narrative, artifact) and *uses* a scheduler — it

does not teach scheduling in general. The actual transport (SMTP/Gmail send) belongs to

email-connector; for a Drive/Workspace destination see

google-workspace. The GH Actions schedule: YAML is in

references/pipeline.md.

Reliability checklist — every recurring report:

  • [ ] Freshness gate before send — assert the source is newer than the last period; if stale, do

NOT send, raise instead. A report that consumes dirty data is not this skill's job to clean —

that is data-cleaning; this gate only refuses to ship

known-stale numbers.

  • [ ] Fail loud — on any error, alert the owner (the report not arriving is itself a silent failure).
  • [ ] Idempotent run — re-running for the same period produces the same artifact, no duplicate send.
  • [ ] Dead-report cleanup — if no one opened the last N editions, kill the schedule. Unread reports

are pure cost and noise.

Anti-patterns

| Anti-pattern | Why it bites | Do instead |

| --- | --- | --- |

| Email a live dashboard link, call it "the report" | Recipient must log in and slice; most won't, the snapshot is lost | Ship the fixed artifact; link the dashboard as a footer |

| No freshness gate | Stale numbers ship silently; trust dies on the first wrong figure | Assert source newer than last period; refuse to send if stale |

| One mega-report for every audience | Execs drown in analyst detail; analysts can't refilter a PDF | Contract per audience; one template, per-recipient params |

| Raw metric dump, no narrative | Reader does the math, misses what changed, stops opening it | Exec summary + 3 takeaways + a "so what" per section |

| Schedule with no failure alert | A broken job is invisible for weeks; the report just stops | Fail loud to the named owner on any error |

| A template forked per recipient | Layout change must be made N times; they drift | Single template scoped by params |

| Redefining KPIs inside the report | The report quietly becomes the metric authority, numbers diverge | Consume an agreed set; defer definition to kpi-framework |

| Unpinned WeasyPrint / render lib | A minor bump reflows the board pack with no warning | Pin (WeasyPrint 68.1) and re-check PyPI before changing |

| Hand-assembling the digest weekly | It breaks the week the owner is on leave | Automate the pipeline end to end; a human only reads it |

Verify

Run scripts/verify.sh (read-only; pass a path to a generated pipeline directory, or run in it). It

confirms a Jinja2 template renders against a sample context without error, the generation script

produces a non-empty PDF/HTML artifact, a schedule definition exists (a crontab line or a

.github/workflows/*.yml with a schedule: block), and a delivery step is wired. It soft-warns if no

freshness/failure gate is detected, and exits 0 on an empty/clean target — nothing to check is not a

failure.

How to use it

Copy the folder

Take ericrisco/reporting from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.