---
name: ghl-reports
description: Answer GoHighLevel data questions — counts, lists, and weekly reports — fast, cheap, and verified. Uses get_contact_count for any "how many contacts" question (one call, GHL's own index, exact window echoed), clean pagination recipes for lists with a CSV deliverable, and get_account_health_summary for composed account reports. Never guesses a number, never paginates to count, always states the exact window and source next to every figure. Triggers on how many contacts, new contacts this week, new leads last 7 days, count my contacts, list my new contacts, contacts added since, weekly report, account report, GHL report, export contacts to CSV.
compatibility: Claude Code, Claude Cowork, Claude Desktop
---

# GHL Reports

Data questions have exactly three shapes. Pick the shape first — mixing them is what burns
50,000 tokens and produces a wrong number.

**The three iron rules (they override improvisation every time):**
1. **Never guess.** Every number you state comes from a tool response you actually received.
   If a count can't be verified, say so and show the error — an honest "unavailable" beats a
   plausible figure.
2. **Count and list are different jobs.** A count is ONE `get_contact_count` call. A list is
   paginated `search_contacts` with a deliverable. Never paginate to produce a count; never
   answer a list request with only a total.
3. **Every figure ships with its window and source.** "163 contacts (dateAdded 2026-07-30T07:00Z
   → 2026-08-06T06:59Z, resolved in US/Arizona, via get_contact_count)" — the echo comes back
   from the tool; repeat it.

---

## Shape 1 — "How many …?" → one call

Use `get_contact_count`:
- `from`: "YYYY-MM-DD" (resolves to the START of that day in the location's timezone)
- `to` (optional): "YYYY-MM-DD" (END of that day, inclusive) — omit for "through now"
- "last 7 days" = from 7 days ago, `to` omitted. Say the resolved window back to the user,
  because "last 7 days" and "this week" are different windows and the echo settles which one ran.

The response is `status: "ok"` with `total` + `window`, or `status: "unavailable"` with a reason.
Report exactly what it says. Do not retry with pagination; do not estimate.

## Shape 2 — "List them" → paginate with a destination

1. Ask (or infer) the destination first: table in chat (small), CSV file (anything over ~30 rows).
2. `search_contacts` sorted by `date_added` desc; walk pages with `startAfter` + `startAfterId`
   (both, together — one alone repeats the page).
3. Stop when a page's oldest `dateAdded` is before the window start; drop out-of-window rows
   from the last page.
4. For CSV: write the file, state the row count, and reconcile: run `get_contact_count` for the
   same window and confirm the row count matches the total. If they differ, say so and show both
   numbers — do not silently pick one.

## Shape 3 — "How is the account doing?" → composed report

`get_account_health_summary` (optionally `windowDays`). Every metric arrives labeled all_time or
window, with unavailable sections marked. Render it as a short table and KEEP the labels — never
merge an all-time number and a windowed number into one row without saying which is which.

For a recurring weekly report: Shape 3 + a Shape-1 count for the exact 7-day window + anything
the user asked to track, each figure with its window and source line.

---

## When something fails

- `unavailable` with a timezone reason → re-ask with full ISO timestamps (the tool tells you this).
- `unavailable` with an API error → report the error text as-is. The user would rather see
  "GHL returned 429" than a made-up count.
- The one thing you never do is fill the gap with an estimate.
