NextGenPRACTICE SOLUTIONSVAULT · UNLISTED

Vault · what lands

Reporting

Four scheduled pulls open a reviewed pull request weekly: search rankings, ads impression share, automation-platform backup, answer-engine citations

Campaign management and the monthly reportShipped

Schedules are read from the workflow files at build time and converted to Pacific here · every file shape is read from the source documentation · no practice's numbers appear on this page — structure only

01 · Four routines

What runs without anyone

Four scheduled pulls, each read-only by construction, each ending in a pull request a person reviews. The Pacific time on each card is converted from the workflow's cron line at build time; the quoted line beneath is the file's own reason for the schedule.

gsc-pullMondays · 06:00 PST / 07:00 PDT

Search Console pull

cron 0 14 * * 1 · Monday 14:00 UTC

Chosen so it never fires before 06:00 Pacific in either half of the year.

Pulls
Search Console's own report for the practice's site — queries, pages, clicks, impressions and average position — for every practice whose config declares a Search Console property. A 28-day snapshot, plus every month not yet sealed, re-read.
Read-only
The service account holds a read-only scope. The job can list and query; nothing in it can verify, add or remove a property, submit a sitemap or request indexing.
Lands
docs/seo/A reviewed pull request, not a deck. New rows change what the summary says about a practice's search presence, so a person reads the diff before it becomes the record.

Mondays 14:00 UTC = 06:00 Pacific Standard Time / 07:00 Pacific Daylight Time. GitHub cron has no timezone; 14:00 UTC is the value that is never EARLIER than 06:00 Pacific in either half of the year. Final data lags 2–3 days, so a Monday run reads through Thursday.

.github/workflows/gsc-pull.yml · the comment above its cron line

ads-pullMondays · 06:30 PST / 07:30 PDT

Google Ads impression-share pull

cron 30 14 * * 1 · Monday 14:30 UTC

Chosen so it never fires before 06:30 Pacific in either half of the year.

Pulls
Impression share, rank-lost share and budget-lost share — the metrics that say, in numbers and every week, whether a Search campaign is entering auctions and what it loses them to — with impressions, clicks, spend and conversions alongside. Read through a mirror of the account, one row per date and campaign.
Read-only
The mirror can only be read. Nothing in the job can pause, enable, bid, budget or edit a campaign. It cannot spend a cent.
Lands
docs/ads/impression-share/A reviewed pull request, not a deck. Half an hour after the search pull so the two open in a stable order.

Mondays 14:30 UTC — half an hour after gsc-pull (14:00) so the two measurement PRs open in a stable order. 14:30 UTC = 06:30 Pacific Standard Time / 07:30 Pacific Daylight Time; never earlier than 06:30 Pacific in either half of the year. Windsor finalizes yesterday's rows overnight, so a Monday run reads through Sunday.

.github/workflows/ads-pull.yml · the comment above its cron line

aeo-pullMondays · 07:00 PST / 08:00 PDT

AEO citation pull

cron 0 15 * * 1 · Monday 15:00 UTC

Pulls
The same 25 local-intent questions, put to a search-grounded answer engine three times each, every week. Two things recorded per answer: whether the practice's site is among the cited sources, and whether the practice is named in the text.
Read-only
The job can only ask; it changes nothing on the search side. Answers vary run to run, which is why every question is sampled and every share is reported as n of N — never a single answer.
Lands
docs/aeo/A reviewed pull request, not a deck. An hour after the search pull so the two land in order and never compete for the runner.

Mondays 15:00 UTC = 07:00 Pacific Standard Time / 08:00 Pacific Daylight Time — an hour after the Search Console pull so the two PRs land in order and never compete for the runner.

.github/workflows/aeo-pull.yml · the comment above its cron line

ghl-pullSaturdays · 22:00 PST / 23:00 PDT

GHL live pull

cron 0 6 * * 0 · Sunday 06:00 UTC

Pulls
Every live workflow in the practice's automation platform, at step level, into versioned specs — the backup. Two independent read paths, the public inventory and the step-level pull, and the run fails when they disagree: a pull that quietly under-reports is worse than one that fails, because a missing spec looks exactly like a workflow that does not exist.
Read-only
Only the read commands may appear in this job. It cannot write to a live practice's automation platform, and the rail that blocks writing has no override here.
Lands
docs/ghl-live/A pull request only when the live platform has drifted from the committed specs — the diff is a record of what someone changed since the last pull. Intentional, merge it; not, that is the finding. A weekly job that opens an empty PR every Sunday trains everyone to ignore it.

Sundays 06:00 UTC — before the week starts, after any Friday changes in the GHL UI have settled.

.github/workflows/ghl-pull.yml · the comment above its cron line

ciOn every change

CI

  1. validate
  2. lint
  3. type-check
  4. test
  5. build
  6. launch-QA
  7. determinism gate
  8. performance budget

A build that fails any of these does not ship. A check is broken on purpose before it is trusted; the gates whose record this page can read are the agency-site gate and the automation-platform write gate, and the build refuses if a record goes.

scripts/agency-site-gate.ts · docs/audits/2026-09-02-ghl-smoke-test.md

02 · What a month looks like

The shapes, not the numbers

Four sources, each with a fixed file shape. The figures live in the pull request the routine opened and in the report built from it; this page carries the structure only.

docs/seo/<practice>/
Snapshot
gsc-<date>.json — the 28-day snapshot of one run. Final data: the window ends three days before the run.
History
history/<month>.json — one file per month, about sixteen months back. A month is re-pulled every run until it was read at least five days after it ended; then its file is sealed.
Summary
summary-<month>.md — top 50 queries by impressions with position and clicks, top 30 pages, totals, month-over-month deltas once two months exist. Regenerated from the history file every run; never hand-edited.
Columns
date · query · page · clicks · impressions · ctr · position
Read as
Position is an average of averages — direction, not rank. Query and page tables sum to less than the totals because rare queries are withheld; the totals line is the true site total. A month short of its calendar end is partial, not missing.

Ads impression share

docs/ads/impression-share/<practice>/
Snapshot
is-<date>.json — the 28-day snapshot, one row per date × campaign. The window ends one day before the run.
History
history/<month>.json — six months back. Sealed once read three days after month end with the range reaching the month's last day.
Summary
summary-<month>.md — per campaign: impressions, clicks, CTR, spend, conversions, cost per conversion. For Search campaigns, the six share metrics as impressions-weighted monthly figures and the one-line answer — is it entering auctions? Month-over-month deltas, including campaigns that vanished.
Columns
date · campaign · status · channel · impressions · clicks · spend · conversions · the six share columns
Read as
Share is reported only from 10% upward; the floor and ceiling sentinels print as bounds — under 10%, over 90% — never as figures. A literal 0 in a share cell is “not computed”, not 0%. An empty response is a failure, not a result. A dark month exists only when a person records it, with a reason and a date.

Answer-engine citations

docs/aeo/<practice>/
Run file
aeo-<date>.json — every sample of one run: question, sample index, model, status, grounded, cited, named, the brand, competitor and aggregator domains, the searches the engine ran, every cited source, the answer text. 25 questions × 3 samples = 75 rows; the question set is embedded so the file stands alone.
Summary
summary-<month>.md — per-question cited and named shares as n of N usable samples; excluded samples listed by status; cited-domain leaderboards, practices and aggregators separately; the search queries the engine used; month-over-month once two months exist.
Recorded
Cited — the practice's site is among the answer's grounding sources. Named — the answer text names the practice. Independent: an answer can name a practice from a directory listing without citing its site, and the reverse.
Read as
Only usable samples sit in the denominators, so an empty or blocked answer can never read as “not cited”; a run with fewer than half its samples usable is refused rather than written. One engine is measured, with search grounding — a citation here is downstream of ranking, not independent of it. The question set stays fixed: the value is the week-over-week trend on the same questions.

Automation-platform backup

docs/ghl-live/
Specs
Every live workflow, at step level, as versioned files. What a drift PR changes is the content, never the coverage: the reconcile gate passes only when every live workflow is accounted for.
The gate
Two independent read paths — the public inventory and the step-level pull — and the run fails when they disagree. The folder-scoped list was once found to omit anything filed in a folder, a published sequence included; the second path is what makes that gap visible.
Never committed
Locks carrying signed URLs and live version numbers. Every lock on disk is checked against the ignore list, and a staged diff carrying anything credential-shaped is refused.
Lands
A pull request only when the live platform moved. Read the diff as a record of what someone changed in the platform's UI since the last pull.

03 · The console

Three views, three readers

The Digital Practice Twin console: three views over one practice's live data, each answering one question for one reader. One console per practice, named for that practice.

The console's three views: route, audience, pillar and the question each answers
RouteAudiencePillarQuestion it answers
/Todayfront deskPractice GrowthWho do I call next, and what do I say?
/roiPerformancethe doctorPractice ManagementWhat did the spend produce?
/playbookPlaybookNextGen, internalPractice DevelopmentWhat is installed, and what is actually running?

Not tracked, never zero

A metric this system cannot measure renders as "not tracked", never as zero.

Measure in lib/types.ts forces that distinction at every call site. Collections are the worked example: GoHighLevel holds no transaction or invoice rows for this location, so revenue is genuinely unknown. A console showing a zero-collections figure would assert a fact nobody verified — which is the exact failure this repo keeps producing (every opportunity reading zero, a portrait social card, a compliance sweep scanning a field that did not exist).

The empty cells are the argument for closing them. Do not fill them with zeroes.

packages/twin-console/README.md · “The rule that shapes everything here”

No patient identifiers

Rows carry a contact id, tags and the patient's own form answers — never a name, email or phone number. Each row links into GoHighLevel, which already holds the contact details and already has the practice's access control. Keep it that way: if a name ever needs to appear here, that is a decision with consent and access implications, not a UI tweak.

packages/twin-console/README.md · “No patient identifiers”

04 · The split ledger · Prove

What we measured, what you reported

The three stages of the Prove phase, as canon states them. The monthly report is the middle one.

  1. Run — the monthly rhythm

  2. The monthly report — split ledger: what we measured versus what you reported, never mixed

  3. The re-scan — the same checks re-run monthly, so the before-and-after is the scorecard itself

Why the two columns never mix. What NextGen measured comes from pulls that can be re-run — rankings, impression share, citations, the automation platform's own records — and every figure has a file, a date and a diff a person reviewed. What the practice reported comes from its practice-management system, its bank and its front desk: figures this system cannot pull, verify or re-run. Put them in one column and a number nobody verified borrows the credibility of one that was. So the report keeps them apart, labels each, and the join between them is a conversation on the scorecard call, not a cell. Where the measured side has nothing, it says not tracked — never zero.