consult-adoption-reporter · git:20260918.2429d96 · 2026-09-18 · sha256 315511a3aa5c0d74

consult-adoption-reporter git:20260918.2429d96A

Immutable. This exact content never changes and is served at /api/v1/blob/315511a3aa5c0d74.

---
name: consult-adoption-reporter
sheetId: "2.15"
description: >-
  Pull a consulting client's AI-assistant usage out of platform telemetry
  (crm_ai_usage, READ-ONLY), compute adoption metrics — WAU, actions per user
  per week, week-over-week trend, top features, at-risk users — and render a
  zh-TW "[Adoption] {{COMPANY}} — YYYY-MM" report Doc into the client's [N]
  Drive folder, backlinked to the CRM deal, plus an optional ask-only Gmail
  draft. Trigger on /consult-adoption-reporter or when Peter
  says "客戶用量報告", "採用率追蹤", "這個客戶有沒有在用", "使用狀況分析",
  "adoption report for <client>", "pull usage for this client", "how is the
  assistant being used", or otherwise asks whether a consulting client is
  actually using what we delivered — fire eagerly even without the word
  "adoption". Distinct from project-status-update (the INTERNAL tracker-Sheet
  weekly for a course/project — no client telemetry), from consult-uat-writer
  (proves the delivery WORKS once, at handoff; THIS skill measures whether the
  client keeps USING it after go-live), and from consult-status-report (the
  client-facing weekly EMAIL — it can embed this skill's usage block, but the
  telemetry analysis happens here).
category: sales-consultant
project: consult-adoption-reporter
platform: claude
status: Done
author: Peter Tu
input: "A client (deal URL or company name) + a reporting window (default: last 4 weeks vs the 4 before)"
process: "Resolve client → map to workspace/users via adoption-config.md (fallback: deal contact emails) → introspect + read-only SELECTs on crm_ai_usage → compute adoption metrics + trends → render the Doc into the [N] folder + backlink the deal → optional draft email"
output: "An adoption report Doc (metrics, weekly trend, per-user detail, risks and nudges, data-coverage caveats) linked on the CRM deal, plus an optional Gmail draft"
synergy:
  - "consult-uat-writer"
  - "consult-status-report"
  - "project-status-update"
house-style: bound

---

# Consult Adoption Reporter

```bash
npx skills add https://github.com/peter-tu-zynkr/zynkr-skill-builder --skill consult-adoption-reporter
```

After an engagement ships, the question that decides the renewal is not "did we
deliver" but "are they using it". Every platform assistant call already lands in
`crm_ai_usage` — this skill turns that table into the adoption story: WAU,
actions per user per week, the weekly trend, top features, and the users going
quiet — rendered as a zh-TW **`[Adoption] {{COMPANY}} — YYYY-MM`** Doc in the
client's `[N]` folder, backlinked to the CRM deal. It is an **analyst, not an
operator**: reads telemetry, never touches it; 無法衡量 beats an estimate.

## How this differs from its neighbours

- **project-status-update** — INTERNAL tracker-Sheet weekly; no client telemetry.
- **consult-uat-writer** — proves the delivery WORKS once, at handoff (UAT
  scripts from the PRD); THIS skill measures whether it keeps getting USED
  after go-live — the renewal signal.

## Fixed facts (don't re-derive these)

- **Supabase project_id**: `uomieoqlkazknjgmfdda` (the shared Zynkr project; CRM tables are `crm_*`)
- **Google account** for all Gmail/Drive/Docs tools: `peter_tu@zynkr.ai`
- **Drive parent folder** (`[2.2] 業務與顧問部門:專案`, where numbered project folders live): `1hkXPX7OXPFOU0BcloPbJSFp8O0zArM8t`
- **CRM deal URL** for the doc/report/backlink: `https://platform.zynkr.ai/deals/{deal_id}`
- Over the Supabase MCP, `auth.uid()` is **NULL** — moot here: this skill's SQL
  is SELECT-only (hard rule 1); the step-7 deal backlink goes through the zynkr
  MCP, never through SQL.

## Hard rules

1. **SELECT only.** Never INSERT / UPDATE / DELETE on platform tables — no SQL
   write of any kind, anywhere in a run. The only writes are the report Doc
   (Google Drive) and the step-7 deal-notes backlink, which goes through the
   zynkr MCP (`mcp__zynkr__update_deal`); MCP unavailable → skip the backlink
   and hand Peter the line to paste (step 7) — never drop to SQL.
2. **Never fabricate numbers.** A metric the data can't support is written as
   無法衡量 with the reason, in the report's 資料覆蓋範圍 section.
3. **Introspect before querying.** `information_schema.columns` on
   `crm_ai_usage` every run; if the schema drifted, adapt and note it.
4. **Client-facing email is ALWAYS a Gmail draft**
   (`mcp__google-workspace__draft_gmail_message`), only on Peter's yes in
   step 8 — never send.

## Configuration — the local `adoption-config.md`

Client → telemetry mapping lives in a **local** `adoption-config.md` outside
this repo (workspace ids, user emails and go-live dates are client
PII/commercials — never commit the file to this or any repo). Read it every run. Expected per
client: `company` (as named on the CRM deal) · `workspace_id` (platform.zynkr.ai
workspace uuid) · `user_emails` · `features` (as in `crm_ai_usage.feature`) ·
`go_live` (YYYY-MM-DD).

Degradation ladder (never a hard stop): **row missing for this client** → fall
back to the deal's CRM contact emails as the user set, resolve the workspace
from them (step 1), and SAY SO in the report's 資料覆蓋範圍. **File missing
entirely** → same fallback mode, and tell Peter in the final report how to
create it (the five fields above, one row per client).

---

## Workflow

### 1 · Resolve the client, the config row, and the `[N]` folder

- **Deal** — from a `…/deals/{id}` URL, or by company name. Prefer
  `mcp__zynkr__get_deal` / `mcp__zynkr__list_deals` when the zynkr MCP is
  connected; fallback SQL via `mcp__supabase__execute_sql`:
  `SELECT id, name, notes, contact_id FROM crm_deals WHERE name ILIKE '%<company>%' ORDER BY created_at DESC;`
- **Config row** — look the company up in `adoption-config.md` (ladder above):
  take `workspace_id`, `user_emails`, `features`, `go_live`. Without a row: use
  the deal's contact emails from `crm_contacts` as the user set and resolve
  `workspace_id` via the platform's workspace-membership tables (introspect for
  names — read-only). No workspace resolves → coverage-caveat case; continue.
- **Folder** — the deal's `notes` carry a `專案資料夾:<url>` backlink (written
  by consult-intake / consult-project-specialist); extract the folder id, or
  list the parent folder and match `[N] Company(…)`. No `[N]` folder at all →
  STOP and route to /consult-intake or /consult-project-specialist; never
  create a competing folder.

### 2 · Introspect `crm_ai_usage`

```sql
SELECT column_name, data_type FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'crm_ai_usage'
ORDER BY ordinal_position;
```

Columns as verified 2026-08: `id` · `workspace_id` · `user_id` · `feature` ·
`model` · `input_tokens` · `output_tokens` · `request_count` ·
`cache_read_tokens` · `cache_creation_tokens` · `created_at` — rule 3 governs drift.

### 3 · Read-only SELECTs

All via `mcp__supabase__execute_sql(project_id="uomieoqlkazknjgmfdda", ...)`.
Default window: last 4 complete ISO weeks vs the 4 before — pull 8 weeks in one
pass. Config `features` present → add `AND feature = ANY('{...}')`; else report
the whole workspace.

**a) Per-user weekly activity** (feeds the trend table + user detail):

```sql
SELECT date_trunc('week', created_at)::date AS week_start, user_id,
       SUM(request_count) AS requests, SUM(input_tokens + output_tokens) AS tokens
FROM crm_ai_usage
WHERE workspace_id = '<workspace_id>'
  AND created_at >= date_trunc('week', now()) - interval '8 weeks'
GROUP BY 1, 2 ORDER BY 1, 2;
```

**b) Feature breakdown** (current window only):

```sql
SELECT feature, SUM(request_count) AS requests
FROM crm_ai_usage WHERE workspace_id = '<workspace_id>'
  AND created_at >= date_trunc('week', now()) - interval '4 weeks'
GROUP BY feature ORDER BY requests DESC;
```

**c) Days since last use, per user** (all-time, not window-bound):

```sql
SELECT user_id, MAX(created_at) AS last_used,
       (now()::date - MAX(created_at)::date) AS days_silent
FROM crm_ai_usage WHERE workspace_id = '<workspace_id>' GROUP BY user_id;
```

**d) Label the user ids** — resolve `user_id` → email/name with a read-only
lookup against the platform's user tables (introspect for the exact table). An
id mapping to no known email shows as 未識別使用者 #n — never guess who it is.

### 4 · Compute the metrics

- **WAU** — distinct active users per ISO week; series + window averages.
- **Actions / user / week** — `Σ request_count ÷ active users ÷ weeks`, per window.
- **WoW trend** — direction of the weekly series + current-vs-prior totals (a
  flat average can hide a falling last fortnight).
- **Top 3 features** — from query (b), with share of total requests.
- **At-risk users** — `days_silent >= 14` AND go-live ≥ 14 days ago. If
  `go_live` is unknown, use the user's earliest telemetry row as the proxy
  start and say so. Round visibly — false precision is rule-2 fabrication too.

### 5 · Coverage check

Diff **expected vs observed** before writing narrative: expected users (config
`user_emails` or fallback contact emails) vs `user_id`s seen; expected
`features` vs features seen; **zero rows for the workspace** (or none resolved)
→ the classic case: the assistant was delivered as **Claude skills**, not
platform features, so `crm_ai_usage` cannot see it. Everything found goes into
資料覆蓋範圍 — what IS measurable, what ISN'T, why, and the concrete step that
closes each gap (onboard the client onto platform.zynkr.ai; add their row to
`adoption-config.md`). A zero-coverage client still gets a report — one that
says exactly that, not empty tables presented as "no adoption".

### 6 · Render the report Doc into the `[N]` folder

Read `./references/adoption-report-template.md`, fill every placeholder, delete
the comment blocks, and create the Doc via the reliable two-step (creating a
Doc directly in a folder via `create_drive_file` returns HTTP 400).
`{{YYYY_MM}}` = the month the reporting window ends in:

```
mcp__google-workspace__create_doc(          # 1. create — lands in My Drive root
  user_google_email = "peter_tu@zynkr.ai",
  title   = "[Adoption] {{COMPANY}} — {{YYYY_MM}}",  # e.g. "[Adoption] 宏宇精密 — 2026-08"
  content = "<filled-in template>"
)
mcp__google-workspace__update_drive_file(   # 2. move into the client's [N] folder
  user_google_email = "peter_tu@zynkr.ai",
  file_id     = "<doc id from step 1>",
  add_parents = "<the [N] folder id from workflow step 1>"
)
```

### 7 · Backlink the Doc to the CRM deal (MCP write — SQL stays read-only)

Hard rule 1 has no exceptions, so the backlink never touches SQL. Append, never
overwrite:

1. `mcp__zynkr__get_deal(deal_id)` — take the current `notes` verbatim.
2. `mcp__zynkr__update_deal` with `notes` = the existing notes plus:

   ```
   採用率報告:[Adoption] {{COMPANY}} — {{YYYY_MM}}
   <doc url>
   ```

zynkr MCP not connected → do **not** fall back to a SQL UPDATE. Skip the
backlink and surface the two lines above in the step-9 report under 待 Peter
for a manual paste into the deal's notes.

### 8 · OPTIONAL — ask-only email draft

Ask Peter — never assume: "Draft an email with this? (a) internal digest to
peter_tu@zynkr.ai, (b) client-facing summary, (c) skip." On a yes, build it with
`mcp__google-workspace__draft_gmail_message` — **draft only, never send** (hard
rule 4). A client-facing draft carries the headline metrics + doc link in the
client's language — and inherits every 無法衡量 caveat.

### 9 · Report

A compact artifact table, then the headline in prose:

```
採用率報告已產出:宏宇精密 — 2026-08

| 產出 | 內容 |
|------|------|
| 文件 | [Adoption] 宏宇精密 — 2026-08(<doc url>)|
| CRM backlink | <deal url> — notes 已附報告連結 |
| 本期指標 | WAU 5 · 每人每週 12.3 次 · WoW ↑ |
| 風險 | 王小明(wang@example.com)21 天未使用 — 建議已列 |
| 待 Peter | email draft:已建立(內部 digest)/未建立 · backlink:已寫入/待手動貼上 |
```

---

## Why it's built this way

- **Read-only by contract.** `crm_ai_usage` is the platform's live metering
  table (quota warnings and per-user caps read from it); SQL stays SELECT-only
  and the entire write surface is a Google Doc + one MCP notes-append.
- **無法衡量 beats a guess.** An invented number, found out later, poisons every
  honest report after it; the coverage section makes the gap a deliverable.
- **Config out-of-repo.** Workspace ids, client emails and go-live dates never
  sit in a public repo — same pattern as guest-lecturer-program's config.
- **Introspect-first.** The platform ships weekly; the reference SQL is a
  starting point, not a contract.
- **Table-first, no charts.** Docs render tables losslessly; charts go stale
  and can't be diffed. The trend IS the table.

## Inference defaults (Peter overrides by just saying so)

- **Window** → last 4 complete ISO weeks vs the 4 before ("last quarter" → 13 vs 13).
- **Feature scope** → config `features` when present; whole workspace otherwise.
- **At-risk threshold** → 14 days silent post go-live.
- **Report language** → zh-TW; user labels = email (or CRM contact name).
- **Email step** → OFF; asked in step 8, executed only on a yes, draft only.

## Reference files

- `./references/adoption-report-template.md` — the zh-TW report skeleton (fill
  all placeholders, delete its comment blocks); its 資料覆蓋範圍 comment lists
  the canonical caveat cases. Keep that section even when coverage is perfect.

## Limitations

- Sees only platform telemetry: Claude-skill deliveries leave no `crm_ai_usage`
  rows — the fix is onboarding onto platform.zynkr.ai, not better SQL.
- Counts and tokens only — no conversation content. It can say WHO used WHAT
  feature HOW OFTEN, never what they asked or whether the answer helped.
- `request_count` measures invocations, not business value; pair the numbers
  with a qualitative client check-in before renewal conversations.
- Requires an existing deal + `[N]` folder (bootstrapping is consult-intake /
  consult-project-specialist's job). One client per run — a portfolio sweep is
  one run per client.

## House style

Writing style is **not owned by this file**. The house voice lives in two Google Docs under
`[@] 寫作指南` (`12DBdFz3SK22ie9im_ThFMI7IBRXsTZsV`), read at runtime:

- 《[2.0] Zynkr 通用風格指南 House Voice》 `10bOIQwRm9Pxwgct4hlwCwK_B4Pipai1HqBPZKzyRHSE` —
  the universal core, plus the addendum for this surface
- 《[3.2] 禁用詞清單 Forbidden Words》 `1N5sHLP4qzmmhpCGsi6KElxi1z0MFe4QZ0Q_35T10Uyg`

Read both before producing client- or reader-facing text, and scan the draft against 《[3.2]》
before handing it over. If Drive is unreachable, say so in the output rather than proceeding
unchecked. Never re-implement either list inside this file.