OKR Planning skill for Claude Code
Key results that measure change, not activity. Claude turns launch-and-build task lists into outcome key results written as baseline to target with a named data source, adds guardrails so a number cannot be gamed, and labels each objective committed or aspirational. A bundled script flags activity phrasing and missing baselines, then scores KRs linearly for check-ins and the end-of-quarter review.
- name
okr-planning - size185 lines · ≈2,050 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: Writes, reviews, and scores OKRs — qualitative objectives tied to strategy, two to five measurable key results each with a baseline, target, and data source, guardrail counter-metrics, committed versus aspirational labels, cross-team alignment, and mid-quarter and end-of-quarter scoring with a bundled lint and scoring script. Use when drafting quarterly or annual OKRs, rewriting key results that are really task lists, aligning team OKRs with company goals, running an OKR check-in, or grading OKRs at quarter end. Try asking
- Rewrite our Q3 OKRs, they read like a project plan
- Draft OKRs for the onboarding team that ladder up to the company retention goal
- Score our OKRs from this spreadsheet and tell me which objectives need escalation
Use it when
- Drafting quarterly or annual OKRs
- Key results are really launches or tasks
- Aligning team OKRs with company goals
- Running a mid-quarter check-in or end-of-quarter grading
Skip it when
- You need to choose which metric is the north star in the first place, use product-metrics instead
- You are sequencing initiatives across quarters, use roadmap-planning instead
How to check the result
The lint script reports no activity-phrased KRs or missing baselines, every KR names a data source someone can query today, and each objective carries a guardrail.
The common mistake it prevents
Writing key results as things the team will launch or build, which can all be completed while the customer behavior the objective cares about never moves.
OKRs are done well when a stranger can read them and know what the team is trying to change, how they will know it changed, and what the team will say no to. Key results measure outcomes (customer or business behavior), never the team’s own activity. If a team can hit every KR while the objective fails, the KRs are wrong.
Ground rules
- 2–4 objectives per team per quarter; 2–5 KRs per objective. More is a task list.
- Every KR has a baseline, a target, a deadline (end of period unless stated), and a named data source. No baseline = the first KR task is measuring it, and the target is provisional until week 3.
- KRs measure outcomes; initiatives (projects, launches) sit under KRs as bets.
- Every objective that pushes one number carries a guardrail so it cannot be gamed (activation up but support tickets flat or better).
- Label each objective committed (expected 1.0; missing it is an escalation) or aspirational (0.6–0.7 is a good result). Never mix the two silently.
- OKRs are not the whole job. Keep-the-lights-on work and health metrics live in a separate list, not as padding KRs.
Workflow
- [ ] 1. Collect inputs: company/parent OKRs, strategy doc, last quarter's scores, metric baselines
- [ ] 2. Draft objectives: the change the team wants, in plain words
- [ ] 3. For each objective, write 2–5 outcome KRs with baseline → target
- [ ] 4. Add guardrails; label committed vs. aspirational
- [ ] 5. List initiatives under each KR as bets, with owners
- [ ] 6. Run the lint script and the review checklist
- [ ] 7. Align with dependent teams; record shared KRs and asks
- [ ] 8. Set the check-in rhythm and scoring method
Writing objectives
An objective is qualitative, motivating, time-bound by the period, and within the team’s influence.
| Weak | Why | Better |
|---|---|---|
| “Increase revenue” | Not inspiring; a metric pretending to be an objective | “Make annual plans the obvious choice for growing teams” |
| “Ship v2 of the editor” | Output, not change | “Writers finish long documents without fighting the editor” |
| “Be the best in the market” | Unmeasurable, not team-scoped | “Win head-to-head evaluations against our top competitor in mid-market” |
| “Improve stuff for users” | Vague | “New teams reach value in their first week” |
Writing key results
Pattern: <metric> from <baseline> to <target> [by <date>] (source: <where measured>)
| Type | Use | Example |
|---|---|---|
| Increase/decrease | Most KRs | “Week-1 activation from 22% to 35%” |
| Threshold (guardrail) | Must not get worse | “Day-30 team retention stays ≥ 61%” |
| Milestone (binary) | Only when no outcome can be measured yet, max 1 per objective | “Pricing experiment read out with a decision by Aug 15” |
Bad to good rewrite
Before (a project plan dressed as OKRs):
O: Improve onboarding
KR1: Launch new onboarding flow
KR2: Build 5 template projects
KR3: Send onboarding email sequence
KR4: Improve activation
Problems: KR1–3 are activities the team controls fully and can finish while nothing changes; KR4 has no number; nothing protects against gaming.
After:
O: New teams reach value in their first week (aspirational)
KR1: Week-1 activation (team shares 3+ projects) from 22% to 35% source: Amplitude "activated_w1"
KR2: Median time from signup to first shared project from 3.1 days to 1 day source: warehouse
KR3: Guardrail — day-30 team retention stays ≥ 61% source: retention dashboard
Initiatives (bets): new onboarding flow (Ana), template gallery (Raj), lifecycle emails (Mei)
The flow, templates, and emails still happen, but now they are bets judged by whether they move KR1 and KR2. If templates ship and activation does not move, the team learns something and changes bets mid-quarter.
Setting targets
| Signal | Target rule |
|---|---|
| Committed KR | Target you would bet on at ~90% odds: historical trend plus planned work |
| Aspirational KR | ~50% odds of hitting; a 0.7 result should still feel like a win |
| Last quarter scored ≥ 0.9 everywhere | Targets were sandbagged; raise them |
| Last quarter scored ≤ 0.3 everywhere | Targets were fantasy or the team was overloaded; cut scope |
| No baseline | Week 1–2: measure; set the target by week 3 and log it |
Use history: if activation grew 1–2 points per quarter for a year, 22% → 35% in one quarter is aspirational unless the team names the mechanism that changes the slope.
Alignment
- Each team objective cites the parent OKR it supports, or states why it stands alone (e.g. platform health).
- Shared KRs list every contributing team and one accountable owner.
- Dependencies become explicit asks: “Needs data eng to ship
activated_w1event by week 2.” Unconfirmed asks are risks, not plans. - Alignment is not cascading every number. Teams choose their own KRs that move the parent; copying the parent KR to five teams hides who owns it.
Check-ins and scoring
Weekly or biweekly check-in per KR: current value, confidence to hit (1–10 or on track / at risk / off track), and what changes next. Five minutes per objective.
Score at mid-quarter and end of quarter with the bundled script:
python3 scripts/okr_score.py okrs.csv
CSV columns: objective,kr,baseline,target,current. Linear score per KR =
(current - baseline) / (target - baseline), clipped to 0.0–1.0, so lower-is-better
metrics work too. Prefix a target with >= or <= for a guardrail: it reports
met/BREACHED and is excluded from the objective mean. The script also lints activity
phrasing (“Launch…”, “Build…”), missing baselines, target equal to baseline, and
objective and KR counts. Exit code 2 means lint problems.
| Score | Committed objective | Aspirational objective |
|---|---|---|
| 0.7–1.0 green | On track / delivered | Strong result; 1.0 every time means targets were too low |
| 0.4–0.6 yellow | Escalate with a recovery plan | Normal |
| 0.0–0.3 red | Escalate now | Reassess bets, or the target was unreasonable |
End-of-quarter retro per objective: what moved the metric, what did not, what we learned, and whether the objective carries over (carry over at most one per team).
Gotchas
- Task-list KRs (“launch”, “build”, “migrate”). Ask “why does that matter?” until you reach a number a customer or the business would notice.
- Vanity metrics. Page views, signups without activation, features shipped. Prefer metrics tied to retained value (see product-metrics).
- Uncontrollable KRs. “Company revenue +20%” for a single feature team; pick the input metric the team actually moves.
- No baseline. “Increase NPS to 45” from an unknown start cannot be judged.
- Sandbagging and grading on effort. Score the number, not how hard people worked; discuss effort in the retro.
- Set and forget. OKRs not reviewed until week 12 are trivia. Book the check-ins when the OKRs are approved.
- Tying OKR scores to bonuses turns aspirational targets into sandbagged ones. Flag this to the user if their process does it.
Verification
scripts/okr_score.pyruns with no lint errors, or each remaining error has a written justification (e.g. one milestone KR).- Every KR names its data source and someone has confirmed the number can be pulled today.
- Each objective has at least one guardrail or states why none is needed.
- Committed/aspirational labels are present, and committed KRs have capacity behind them.
- A reader outside the team can explain each objective’s intended change in one sentence.
Output contract
# OKRs: <team> — <period>
Parent goals: <company OKRs supported> Check-ins: <cadence, owner>
## O1: <objective> (committed | aspirational) — supports <parent>
| KR | Baseline | Target | Source | Owner | Current | Score |
|---|---|---|---|---|---|---|
Guardrails: ...
Initiatives (bets): <initiative — owner — KR it should move>
## Dependencies and asks
| Ask | From team | Needed by | Confirmed? |
## Not doing this quarter
- <tempting work explicitly deferred, and why> Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
scripts/okr_score.py
#!/usr/bin/env python3
"""Lint and score OKRs from a CSV.
Usage:
python3 okr_score.py okrs.csv
Columns (header required): objective,kr,baseline,target,current
baseline : value at the start of the period (blank = missing, flagged)
target : value that counts as 1.0, or ">=61" / "<=2.0" for a threshold
(guardrail) KR: reported as met/BREACHED, excluded from the
objective mean, and a breach is a lint error
current : latest measured value (blank = not yet measured, scored as baseline)
Direction is inferred: target below baseline means "lower is better".
Score per KR = (current - baseline) / (target - baseline), clipped to 0.0-1.0,
rounded to 0.1. Objective score = mean of its KR scores.
Lint checks: KRs phrased as activities (launch/ship/build...), missing baseline,
target equal to baseline, more than 5 KRs per objective, more than 4 objectives.
Exits 1 on unreadable input, 2 if any lint error was found, else 0.
"""
from __future__ import annotations
import csv
import re
import sys
from collections import OrderedDict
ACTIVITY = re.compile(
r"^\s*(launch|ship|build|implement|release|complete|create|deliver|develop|"
r"migrate|roll out|rollout|design|write|hire|run|start|finish|deploy|introduce)\b",
re.I,
)
def band(s: float) -> str:
return "green" if s >= 0.7 else "yellow" if s >= 0.4 else "red"
def num(v: str) -> float | None:
v = (v or "").strip().replace(",", "").rstrip("%")
return float(v) if v else None
def main() -> None:
if len(sys.argv) != 2:
print(__doc__, file=sys.stderr)
sys.exit(1)
try:
with open(sys.argv[1], newline="", encoding="utf-8-sig") as fh:
rows = list(csv.DictReader(fh))
except OSError as exc:
print(f"error: {exc}", file=sys.stderr)
sys.exit(1)
need = {"objective", "kr", "baseline", "target", "current"}
if not rows or not need <= {k.strip().lower() for k in rows[0]}:
print(f"error: header must include {sorted(need)}", file=sys.stderr)
sys.exit(1)
objectives: "OrderedDict[str, list]" = OrderedDict()
problems: list[str] = []
for i, raw in enumerate(rows, start=2):
r = {k.strip().lower(): (v or "").strip() for k, v in raw.items() if k}
obj, kr = r["objective"], r["kr"]
if not kr:
continue
raw_t = r["target"].replace(" ", "")
threshold = raw_t[:2] if raw_t[:2] in (">=", "<=") else None
try:
base, cur = num(r["baseline"]), num(r["current"])
tgt = num(raw_t[2:] if threshold else raw_t)
except ValueError:
print(f"error: line {i}: baseline/target/current must be numbers", file=sys.stderr)
sys.exit(1)
if ACTIVITY.match(kr):
problems.append(f"line {i}: '{kr}' reads as an activity; state the measurable outcome it should move")
if threshold and tgt is not None:
if cur is None:
objectives.setdefault(obj, []).append((kr, "unmeasured"))
else:
met = cur >= tgt if threshold == ">=" else cur <= tgt
objectives.setdefault(obj, []).append((kr, "met" if met else "BREACHED"))
if not met:
problems.append(f"line {i}: guardrail '{kr}' breached ({cur:g} vs {threshold}{tgt:g})")
continue
if tgt is None:
problems.append(f"line {i}: '{kr}' has no target")
objectives.setdefault(obj, []).append((kr, None))
continue
if base is None:
problems.append(f"line {i}: '{kr}' has no baseline; the target cannot be judged as ambitious or trivial")
objectives.setdefault(obj, []).append((kr, None))
continue
if tgt == base:
problems.append(f"line {i}: '{kr}' target equals baseline (that is a health metric, not a KR)")
objectives.setdefault(obj, []).append((kr, None))
continue
value = base if cur is None else cur
s = max(0.0, min(1.0, (value - base) / (tgt - base)))
objectives.setdefault(obj, []).append((kr, round(s, 1)))
if len(objectives) > 4:
problems.append(f"{len(objectives)} objectives; 2-4 per team keeps focus")
for obj, krs in objectives.items():
if len(krs) > 5:
problems.append(f"objective '{obj}' has {len(krs)} KRs; keep 2-5")
if len(krs) < 2:
problems.append(f"objective '{obj}' has 1 KR; add a counter-metric or quality KR")
for obj, krs in objectives.items():
scored = [s for _, s in krs if isinstance(s, float)]
mean = round(sum(scored) / len(scored), 1) if scored else None
head = f"{mean:.1f} {band(mean)}" if mean is not None else "unscored"
print(f"O: {obj} [{head}]")
for kr, s in krs:
if isinstance(s, str):
tag = f"guardrail {s}"
else:
tag = f"{s:.1f} {band(s)}" if s is not None else "n/a"
print(f" - {kr} [{tag}]")
if problems:
print("\nLint:")
for p in problems:
print(f" ! {p}")
sys.exit(2)
print("\nLint: no problems found.")
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.