Product Metrics skill for Claude Code
A north star and a metric tree teams can move. Claude picks a north star that measures delivered value, decomposes it into a metric tree whose children reconcile to the parent, and assigns each leaf to a team. Activation is chosen by comparing retention gaps across candidate behaviors, active means a value-carrying action rather than a login, and a bundled script builds N-period cohort retention tables from raw events.
- name
product-metrics - size196 lines · ≈2,100 tokens
- files2
- licenseMIT
- updated
What Claude sees before loading it
This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.
description: Defines the metrics a product team runs on — a north star tied to delivered customer value, a metric tree of input metrics teams can move, activation, engagement, and retention definitions with exact event logic, guardrails, and a dashboard layout — and builds cohort retention tables from raw activity with a bundled script. Use when choosing a north star metric, defining activation or an active user, building a metric tree or KPI dashboard, reading retention curves, or when a metric moved and nobody agrees what it means. Try asking
- Help us pick a north star metric for our team task app and build the metric tree under it
- Figure out what our activation event should be from this signup and usage export
- Build a weekly cohort retention table from activity.csv and tell me if the curve flattens
Use it when
- Choosing or replacing a north star metric
- Defining activation, active users or retention precisely
- Designing a KPI dashboard or metric tree
- A metric moved and the team disagrees about why
Skip it when
- You need to install PostHog, Mixpanel or GA4 and send events, use product-analytics instead
- You need to judge whether an A/B test result is significant, use experiment-analysis instead
How to check the result
The metric tree's children reconcile to the parent within about five percent, and two people computing any definition independently get the same number.
The common mistake it prevents
Counting a login or page view as an active user, which inflates engagement with people who opened the app and got no value.
Metrics are done well when each one has a precise written definition, an owner, a known baseline, and a clear place in a tree that connects a team’s daily work to the value customers get and the business captures. A metric nobody can compute the same way twice is an opinion.
Ground rules
- One north star per product: a measure of value delivered to customers that leads revenue. Not revenue itself, not signups.
- Every metric has a written definition: entity, event(s), time window, filters, exclusions, and the query or dashboard link.
- Prefer rates and per-entity counts over raw totals; totals rise with any growth.
- Define “active” by a value-carrying action, never by login or page view.
- Pair every target metric with a guardrail that would catch gaming or damage.
- B2B products measure at the account/team level as well as the user level.
Workflow
- [ ] 1. Write the value proposition in one sentence: who gets what value, how often
- [ ] 2. Choose the north star (tests below)
- [ ] 3. Decompose into a metric tree down to inputs a team can move in weeks
- [ ] 4. Define activation, active use, and retention precisely
- [ ] 5. Add guardrails and counter-metrics
- [ ] 6. Pull baselines; build cohort retention from raw data
- [ ] 7. Lay out the dashboard; assign an owner per metric
- [ ] 8. Write the metric definitions doc
North star tests
| Test | Pass example | Fail example |
|---|---|---|
| Reflects value the customer gets | Weekly teams with 3+ members completing tasks | Registered accounts |
| Leads revenue (moves before it) | Weekly active paying teams | Monthly revenue |
| Teams can influence it | Projects with activity this week | Market share |
| Frequency matches the natural use cycle | Weekly for a work tool; monthly for invoicing; per trip for travel | Daily for a tax app |
| Hard to game alone | Paired with retention guardrail | Raw “actions taken” |
Examples by model:
| Product | North star |
|---|---|
| Team task tool | Weekly active teams (≥ 3 members each completing ≥ 1 task) |
| Marketplace | Successful transactions per week (both sides satisfied, no dispute) |
| Analytics SaaS | Weekly accounts viewing a saved dashboard |
| Consumer subscription | Weekly subscribers completing the core action ≥ 2 times |
Metric tree
Decompose the north star multiplicatively or additively into inputs. Stop when a node is something one team can move within a quarter.
Weekly active teams (WAT) = 8,400
├── New activated teams this week = 610
│ ├── New team signups = 2,300
│ └── Week-1 activation rate = 26.5%
│ ├── % teams creating first project = 71%
│ ├── % inviting ≥ 2 teammates = 44%
│ └── median hours to first task done = 30 h
├── Retained active teams (from last week) = 7,520
│ └── Week-over-week team retention = 92.1%
│ ├── % teams using ≥ 2 core features = 58%
│ └── % teams with integration linked = 33%
└── Resurrected teams = 270
└── Re-engagement email → return rate = 4.2%
Check the arithmetic: WAT ≈ new activated + retained + resurrected. If the parts do not sum to the whole within a few percent, a definition is inconsistent; fix that before setting targets. Assign each leaf an owning team; that leaf becomes their OKR candidate (see okr-planning).
Core definitions
Activation
Activation is the earliest behavior that predicts long-term retention. Find it with data:
- List 5–10 candidate early behaviors (first week): created X, invited N, connected Y.
- For each, compare day-30 (or week-8) retention of users who did it vs. did not.
- Pick the behavior with the largest retention gap that still covers a meaningful share of users (≥ 20%), then tune the threshold (1 vs. 3 vs. 5 invites) and window.
- Correlation is not causation: confirm with an experiment that pushes the behavior (see experiment-analysis) before betting the roadmap on it.
Write it as: Activated = team that within 7 days of signup creates ≥ 1 project AND has ≥ 3 members who each complete ≥ 1 task.
Engagement
| Metric | Definition | Use |
|---|---|---|
| DAU/WAU/MAU | Distinct entities doing the core action in the window | Size of active base |
| Stickiness | DAU/MAU (daily products) or WAU/MAU (weekly products) | Habit strength; ~20% DAU/MAU is solid for many SaaS tools, 50%+ for messaging |
| L7 / L28 histogram | Days active out of last 7 / 28 per user | Shows power users vs. casual; better than an average |
| Feature adoption | % of active entities using feature in window | Feature health |
Retention
- N-period retention: % of a cohort active in period N exactly. Default view.
- Unbounded (rolling) retention: % active in period N or later. Flatters numbers; label it if used.
- Revenue retention (B2B): GRR = (start MRR − churn − contraction) / start MRR; NRR adds expansion. Typical healthy SaaS: GRR ≥ 90%, NRR ≥ 100% (mid-market).
Build the cohort table from raw activity:
python3 scripts/cohort_retention.py activity.csv --period week --periods 8
Input columns: user_id,date (one row per value-carrying action), optional
signup_date; rename with --user-col, --date-col, --signup-col. The script
prints cohort sizes, N-period retention by cohort, a size-weighted average curve, and
whether the curve flattens. The latest period is treated as incomplete and left blank.
Reading the curve:
| Shape | Meaning | Action |
|---|---|---|
| Flattens (last 3 periods within 1 point) | Product-market fit for a core segment | Grow acquisition into that segment |
| Keeps declining to ~0 | No retained core | Fix value before spending on growth |
| Newer cohorts flatten higher | Product improvements are working | Identify which changes and double down |
| Steep P0 → P1 drop | Activation problem | Work the activation leaves of the tree |
Guardrails
| Target metric | Guardrail |
|---|---|
| Activation rate | Week-4 retention of activated teams; support tickets per new team |
| Notifications engagement | Unsubscribe and mute rate |
| Conversion to paid | Refund rate, 60-day paid churn |
| Feature adoption | Core action rate (cannibalization), p95 latency |
Dashboard layout
- Top row: north star (value, WoW and YoY change, target line).
- Second row: tree level 1 (new activated, retained, resurrected).
- Third row: activation funnel and cohort retention heatmap.
- Fourth row: guardrails with threshold coloring.
- Every chart: definition tooltip, owner, data freshness timestamp.
Keep it to 8–12 charts. Use weekly granularity for weekly products; daily lines add noise (weekday seasonality) without information.
Gotchas
- Vanity metrics: total signups, page views, cumulative anything. They only go up.
- Login as “active”. Counts people who opened the app and left. Use the core action.
- Changing definitions silently. Version definitions; annotate dashboards on change.
- Averages hiding distributions. Mean session length is dragged by a few outliers; report median and the L7/L28 histogram.
- Comparing young cohorts on late periods. They have not had time; blank the cells.
- Mix shift. Overall rate falls because a low-converting channel grew, not because the product got worse. Segment by channel and plan before concluding.
- Bots, internal users, and test accounts inflate small products. Exclude by domain and flag at ingestion.
Verification
- The tree’s children reconcile to the parent within ~5%.
- Each metric’s definition yields the same number when two people compute it.
- Activation was chosen by comparing retention gaps across candidates, with the gap and coverage written down.
- Cohort table built from raw events; incomplete periods excluded from comparisons.
- Every target metric has at least one guardrail and an owner.
Output contract
# Product metrics: <product>
Value proposition: <one sentence>
## North star
<name> — <definition> — baseline <x> (<date>) — owner <name>
## Metric tree
<tree with current values and owning teams>
## Definitions
| Metric | Entity | Event logic | Window | Filters/exclusions | Source/query | Owner |
## Activation analysis
| Candidate behavior | % who did it | D30 retention did / did not | Gap |
## Retention
<cohort table summary, curve shape, interpretation>
## Guardrails
## Dashboard spec Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
scripts/cohort_retention.py
#!/usr/bin/env python3
"""Build an N-period cohort retention table from a raw activity CSV.
Usage:
python3 cohort_retention.py activity.csv [--period week|month] [--periods 8]
[--user-col user_id] [--date-col date]
[--signup-col signup_date]
Input: one row per meaningful activity (the event that defines "active", not
page views). Dates in ISO format (YYYY-MM-DD or a full ISO timestamp).
Cohort = period of the user's signup date if --signup-col is given and filled,
otherwise the period of their first activity.
Output: cohort size and the % of the cohort active in period 0..N (classic
N-period retention: active in that exact period), a cohort-size-weighted
average curve, and whether the curve flattens (change < 1 point across the last
3 observed periods). Cells for periods that have not fully elapsed are blank.
Standard library only. Exits 1 on bad input.
"""
from __future__ import annotations
import argparse
import csv
import sys
from collections import defaultdict
from datetime import date, datetime, timedelta
def parse_day(s: str) -> date:
s = s.strip()
try:
return datetime.fromisoformat(s.replace("Z", "+00:00")).date()
except ValueError:
return date.fromisoformat(s[:10])
def bucket(d: date, period: str) -> date:
if period == "week":
return d - timedelta(days=d.weekday())
return d.replace(day=1)
def diff(a: date, b: date, period: str) -> int:
if period == "week":
return (b - a).days // 7
return (b.year - a.year) * 12 + (b.month - a.month)
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("csv")
ap.add_argument("--period", choices=["week", "month"], default="week")
ap.add_argument("--periods", type=int, default=8)
ap.add_argument("--user-col", default="user_id")
ap.add_argument("--date-col", default="date")
ap.add_argument("--signup-col", default="signup_date")
a = ap.parse_args()
signup: dict[str, date] = {}
first: dict[str, date] = {}
active: dict[str, set[date]] = defaultdict(set)
last_seen = date.min
try:
with open(a.csv, newline="", encoding="utf-8-sig") as fh:
reader = csv.DictReader(fh)
cols = reader.fieldnames or []
if a.user_col not in cols or a.date_col not in cols:
print(f"error: need columns '{a.user_col}' and '{a.date_col}'; got {cols}", file=sys.stderr)
sys.exit(1)
for line, row in enumerate(reader, start=2):
uid = (row.get(a.user_col) or "").strip()
raw = (row.get(a.date_col) or "").strip()
if not uid or not raw:
continue
try:
d = parse_day(raw)
s = (row.get(a.signup_col) or "").strip()
if s:
signup.setdefault(uid, parse_day(s))
except ValueError:
print(f"error: line {line}: unparseable date", file=sys.stderr)
sys.exit(1)
active[uid].add(bucket(d, a.period))
first[uid] = min(first.get(uid, d), d)
last_seen = max(last_seen, d)
except OSError as exc:
print(f"error: {exc}", file=sys.stderr)
sys.exit(1)
if not active:
print("error: no activity rows", file=sys.stderr)
sys.exit(1)
last_bucket = bucket(last_seen, a.period)
cohorts: dict[date, list[str]] = defaultdict(list)
for uid in active:
cohorts[bucket(signup.get(uid, first[uid]), a.period)].append(uid)
n = a.periods
header = f"{'cohort':<10} {'users':>6} " + " ".join(f"{('P' + str(i)):>5}" for i in range(n + 1))
print(header)
num = [0] * (n + 1)
den = [0] * (n + 1)
for c in sorted(cohorts):
users = cohorts[c]
cells = []
for i in range(n + 1):
if diff(c, last_bucket, a.period) < i or (diff(c, last_bucket, a.period) == i and i > 0):
cells.append(f"{'':>5}") # period not complete yet
continue
k = sum(1 for u in users if any(diff(c, b, a.period) == i for b in active[u]))
num[i] += k
den[i] += len(users)
cells.append(f"{k / len(users):>5.0%}")
print(f"{c.isoformat():<10} {len(users):>6} " + " ".join(cells))
curve = [num[i] / den[i] if den[i] else None for i in range(n + 1)]
print(f"{'weighted':<10} {sum(len(v) for v in cohorts.values()):>6} "
+ " ".join(f"{v:>5.0%}" if v is not None else f"{'':>5}" for v in curve))
observed = [v for v in curve[1:] if v is not None]
if len(observed) >= 3:
tail = observed[-3:]
if max(tail) - min(tail) < 0.01:
print(f"\nCurve flattens near {tail[-1]:.0%}: a retained core exists.")
else:
print(f"\nCurve still moving over the last 3 periods ({tail[0]:.0%} -> {tail[-1]:.0%}); not flat yet.")
else:
print("\nToo few complete periods to judge flattening.")
print("Note: the newest cohorts have few complete periods; do not compare them on later columns.")
if __name__ == "__main__":
main()
About this skill
Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.