Feature Prioritization skill for Claude Code
Rankings anyone can challenge, cut lines that hold. Claude scores the backlog against a single stated objective, attaches a source to every reach, impact and effort number, and calibrates against a reference feature. A bundled script jitters the inputs to show which items near the cut line could flip, deadlines are handled with cost of delay instead of inflated impact, and every item below the line gets a reason and a revisit trigger.
- name
feature-prioritization - size186 lines · ≈2,150 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: Prioritizes a backlog or feature list with an honest, explainable method — RICE or ICE scores with calibrated inputs, cost of delay and WSJF for time-sensitive work, Kano for delighters versus basics, and MoSCoW for fixed-date scope — then checks rank stability with a bundled script and writes the trade-off memo. Use when asked what to build next, to rank or score features, to run RICE or ICE, to cut scope for a deadline, or to explain to stakeholders why their request is not on the plan. Try asking
- Score these 15 feature requests with RICE and tell me where the cut line falls
- We have six weeks and too much scope; help me run MoSCoW on this list
- Write a memo explaining to sales why their top request is below the line this quarter
Use it when
- Deciding what to build next from a long backlog
- Stakeholders disagree about which feature matters most
- A fixed deadline forces scope cuts
- Explaining why a request did not make the plan
Skip it when
- You need to sequence themes over several quarters, use roadmap-planning instead
- The items have unproven value and need testing, start with product-discovery
How to check the result
Every input has a source or is marked as a 50% confidence assumption, the stability check has run, and committed effort above the cut fits the capacity left after fixed commitments.
The common mistake it prevents
Inflating a deadline-driven item's impact score to force it up the list, instead of treating it as a labeled cost-of-delay override with an owner.
Prioritization is done well when the ranking follows from written-down inputs anyone can challenge, the inputs are calibrated against each other, the items near the cut line have been pressure-tested, and every “not now” comes with a reason the requester can accept. A score is a conversation aid, not an oracle.
Ground rules
- Agree the goal first. Items are scored against one objective (this quarter’s OKR or north star input), not against “value” in general.
- One method per decision. Mixing RICE for some items and gut feel for others is politics with a spreadsheet.
- Inputs come with sources: analytics for reach, tickets or interviews for impact, engineering for effort. Write the source next to each number.
- Confidence is a penalty for missing evidence, not a mood. Default to 50% unless there is data.
- Commitments (contracts, compliance, security fixes) are not scored; they come off the top as fixed capacity, stated explicitly.
- Cap the scored list at ~30 items. Below that cut, items are “not now” without a score.
Workflow
- [ ] 1. State the objective and the planning horizon (e.g. Q3, next 6 weeks)
- [ ] 2. Remove fixed commitments; record the capacity they consume
- [ ] 3. Normalize the list: one line per item, same granularity (epics, not tasks)
- [ ] 4. Choose the method (table below)
- [ ] 5. Score with sources; calibrate against a reference item
- [ ] 6. Run the stability check; re-examine unstable items near the cut
- [ ] 7. Apply explicit overrides (strategy, dependencies) and label them
- [ ] 8. Draw the cut line against capacity; write the trade-off memo
Choose the method
| Situation | Method | Why |
|---|---|---|
| Growth or product backlog, reach varies a lot between items | RICE | Reach separates niche from broad work |
| Early stage, little data, many small experiments | ICE (1–10 each) | Fast; honest about guesswork |
| Value decays with time (deal deadline, seasonal, competitor launch) | Cost of delay / WSJF | Captures urgency that RICE ignores |
| Deciding which features customers expect vs. would love | Kano survey | Separates must-be basics from delighters |
| Fixed date, fixed team, too much scope | MoSCoW | Forces what gets cut, not what gets ranked |
| Two to five big bets | Written bet comparison | Scores add false precision at this size |
RICE
RICE = Reach x Impact x Confidence / Effort
| Input | Definition | Scale |
|---|---|---|
| Reach | Users or accounts affected per quarter, from data | Absolute count |
| Impact | Effect per user on the objective | 3 massive, 2 high, 1 medium, 0.5 low, 0.25 minimal |
| Confidence | Evidence strength for reach and impact | 100% data-backed, 80% some evidence, 50% gut |
| Effort | Total team person-months incl. design, QA, rollout | ≥ 0.25 |
Calibration rules:
- Pick a reference item everyone knows (a shipped feature) and score it first. Score every other impact relative to it.
- If more than half the items are impact 2+, the scale is inflated; re-score.
- If more than half claim 100% confidence, ask for the data behind each.
- Effort under 0.25 person-months inflates scores; floor it at 0.25.
- Reach must use the same window and unit for every item (accounts per quarter, not a mix of daily users and total signups).
Worked RICE table
Objective: increase paid team retention in Q3. Capacity: 6 person-months after commitments.
| Item | Reach / qtr | Impact | Conf | Effort (pm) | RICE | Source |
|---|---|---|---|---|---|---|
| Dark mode | 4,000 | 0.5 | 80% | 1 | 1,600 | 31% of users toggle OS dark mode; requests in 40 tickets |
| Onboarding checklist | 6,000 | 1 | 50% | 2 | 1,500 | All new signups; impact unproven |
| Bulk invoice export | 1,200 | 2 | 80% | 2 | 960 | 38 tickets; churn interviews |
| Slack alerts | 2,500 | 1 | 50% | 1.5 | 833 | Survey interest only |
| SSO (SAML) | 300 | 3 | 100% | 3 | 300 | 3 enterprise renewals require it |
| Audit log | 400 | 2 | 80% | 4 | 160 | Security questionnaires |
Reading it: dark mode tops the list on reach, but its impact on retention is a guess dressed as 0.5. The onboarding checklist runs at 50% confidence, so the first move is a cheap test that raises confidence, not a 2-month build. SSO scores low because RICE ignores deadlines; three renewals with a hard date make it a cost-of-delay question. Handle it as a labeled override, not by inflating its impact to 30.
Stability check
Scores built from estimates move. Before drawing the cut line, jitter every input and see which items change sides:
python3 scripts/prioritize.py items.csv --top 3 --noise 0.3
The script auto-detects RICE (item,reach,impact,confidence,effort) or ICE
(item,impact,confidence,ease), prints the ranked table, the share of simulated runs in
which each item lands in the top N, and warnings for inflated impact, 100% confidence,
and tiny efforts. Items flagged unstable (20–80% top-N share) are the ones worth an
extra hour of evidence gathering; items at 95%+ or 0% do not need more debate.
On the table above, dark mode and the checklist stay in the top 3 in ~95% of runs, while bulk export (63%) and Slack alerts (46%) swap places depending on guesses. Get real reach and impact data for those two before deciding.
Cost of delay and WSJF
Use when value depends on when it ships.
| Urgency profile | Example | Handling |
|---|---|---|
| Expedite | Security hole, outage risk | Do now, outside scoring |
| Fixed date | Renewal requires SSO by Sep 30 | Value collapses after the date; schedule backward from it |
| Standard | Most features | Value lost per week of delay is roughly constant |
| Intangible | Platform upgrade | Low now, rising later; reserve 10–20% capacity |
WSJF = cost of delay / duration. Score cost of delay as the sum of user value, time
criticality, and risk reduction (each on a 1, 2, 3, 5, 8, 13 scale, relative to the
smallest item = 1). Shorter jobs with equal cost of delay go first.
Kano and MoSCoW, briefly
- Kano: ask each surveyed user a functional question (“How would you feel if export included all columns?”) and a dysfunctional one (“…if it did not?”). Must-be items (absence angers, presence is neutral) ship before delighters. Use ≥ 30 responses per segment before trusting the category.
- MoSCoW for fixed dates: Must ≤ 60% of capacity, Should ~20%, Could ~20%, Won’t listed explicitly. If Must exceeds 60%, the date or the scope is unrealistic; say so.
Overrides
Overrides are allowed and must be visible. Valid reasons: contractual date, dependency
that unblocks higher-ranked work, strategic bet the leadership owns, regulatory need.
Write each as: Override: SSO moved above cut — 3 renewals ($410k ARR) due Sep 30; owner: VP Sales.
More than ~20% of capacity going to overrides means the scoring objective is wrong.
Gotchas
- HiPPO in disguise: a stakeholder’s pet item gets impact 3 and 100% confidence. Ask for the evidence behind any max score.
- Effort from the PM alone. Effort comes from the people who will build it, as a range; use the midpoint and note the range (see work-estimation).
- Scoring tasks against epics. A 2-day fix and a 3-month platform beside each other distorts everything. Normalize granularity first.
- Ignoring confidence-raising work. A 50%-confidence item often deserves a 1-week test, not a decision either way (see product-discovery).
- Re-scoring until the favorite wins. Freeze inputs before revealing rankings; log any change with a reason.
- Saying no without a reason. Every item below the cut needs a one-line why and a condition that would move it up.
Verification
- Every input has a source or is marked as an assumption with 50% confidence.
- The stability check ran; unstable items near the cut were investigated or flagged.
- Sum of effort above the cut line ≤ available capacity after commitments.
- Overrides are labeled with reason and owner and total ≤ ~20% of capacity.
- Each “not now” item has a reason and a trigger to revisit.
Output contract
# Prioritization: <scope> — <horizon>
Objective: <metric/OKR> Capacity: <X person-months> after commitments (<list>)
Method: RICE | ICE | WSJF | MoSCoW — why
## Ranked list
| Rank | Item | Inputs... | Score | Top-N stability | Source |
## Cut line
Above the line (committed): ... Below the line (not now): ...
## Overrides
- <item>: <reason>, owner
## Not now, and what would change that
| Item | Why not now | Revisit if |
## Open questions / evidence to gather Bundled files
Loaded only when a step in the skill needs them. Review them here before installing.
scripts/prioritize.py
#!/usr/bin/env python3
"""Score and rank backlog items with RICE or ICE, plus a rank-stability check.
Usage:
python3 prioritize.py items.csv [--method rice|ice] [--top 5] [--noise 0.3]
[--runs 5000] [--seed 7]
RICE columns (header required): item,reach,impact,confidence,effort
reach : people/accounts affected per period (e.g. per quarter)
impact : 0.25 minimal, 0.5 low, 1 medium, 2 high, 3 massive
confidence : 0-1 or 0-100 (percent); 0.5 = low, 0.8 = medium, 1.0 = high
effort : person-months (or any consistent unit) > 0
ICE columns: item,impact,confidence,ease (each 1-10)
Prints the ranked table, warnings about suspicious inputs, and how often each
item lands in the top N when every input is jittered by +/- noise (uniform,
multiplicative). Items whose top-N share is between 20% and 80% are "unstable":
their rank depends on guesses, so validate those inputs before deciding.
Standard library only. Exits 1 on bad input.
"""
from __future__ import annotations
import argparse
import csv
import random
import sys
RICE_IMPACT = {0.25, 0.5, 1.0, 2.0, 3.0}
def fail(msg: str) -> None:
print(f"error: {msg}", file=sys.stderr)
sys.exit(1)
def load(path: str, method: str | None) -> tuple[str, list[dict], list[str]]:
warnings: list[str] = []
try:
fh = open(path, newline="", encoding="utf-8-sig")
except OSError as exc:
fail(str(exc))
with fh:
reader = csv.DictReader(fh)
cols = {c.strip().lower() for c in (reader.fieldnames or [])}
if method is None:
method = "rice" if "reach" in cols else "ice"
need = {"rice": {"item", "reach", "impact", "confidence", "effort"},
"ice": {"item", "impact", "confidence", "ease"}}[method]
if not need <= cols:
fail(f"{method.upper()} needs columns {sorted(need)}; got {sorted(cols)}")
rows = []
for line, raw in enumerate(reader, start=2):
row = {k.strip().lower(): (v or "").strip() for k, v in raw.items() if k}
if not row.get("item"):
continue
try:
vals = {k: float(row[k]) for k in need - {"item"}}
except ValueError:
fail(f"line {line} ({row['item']}): non-numeric value")
name = row["item"]
if method == "rice":
if vals["confidence"] > 1:
vals["confidence"] /= 100.0
if not 0 < vals["confidence"] <= 1:
fail(f"line {line} ({name}): confidence must be in (0, 1] or (0, 100]")
if vals["effort"] <= 0:
fail(f"line {line} ({name}): effort must be > 0")
if vals["reach"] < 0:
fail(f"line {line} ({name}): reach must be >= 0")
if vals["impact"] not in RICE_IMPACT:
warnings.append(f"{name}: impact {vals['impact']:g} is off the 0.25/0.5/1/2/3 scale")
if vals["effort"] < 0.25:
warnings.append(f"{name}: effort {vals['effort']:g} is tiny; tiny denominators inflate scores")
else:
for k in ("impact", "confidence", "ease"):
if not 1 <= vals[k] <= 10:
fail(f"line {line} ({name}): {k} must be 1-10")
rows.append({"item": name, **vals})
if not rows:
fail("no items found")
if method == "rice":
high = sum(1 for r in rows if r["confidence"] >= 1.0)
if len(rows) >= 4 and high / len(rows) > 0.5:
warnings.append(f"{high}/{len(rows)} items claim 100% confidence; that is rarely honest")
big = sum(1 for r in rows if r["impact"] >= 2)
if len(rows) >= 4 and big / len(rows) > 0.5:
warnings.append(f"{big}/{len(rows)} items rated high or massive impact; recalibrate against a reference item")
return method, rows, warnings
def score(method: str, r: dict) -> float:
if method == "rice":
return r["reach"] * r["impact"] * r["confidence"] / r["effort"]
return r["impact"] * r["confidence"] * r["ease"]
def stability(method: str, rows: list[dict], top: int, noise: float, runs: int, seed: int) -> dict[str, float]:
rng = random.Random(seed)
keys = [k for k in rows[0] if k != "item"]
hits = {r["item"]: 0 for r in rows}
for _ in range(runs):
scored = []
for r in rows:
j = {k: r[k] * rng.uniform(1 - noise, 1 + noise) for k in keys}
scored.append((score(method, j), r["item"]))
scored.sort(reverse=True)
for _, name in scored[:top]:
hits[name] += 1
return {k: v / runs for k, v in hits.items()}
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("csv")
ap.add_argument("--method", choices=["rice", "ice"])
ap.add_argument("--top", type=int, default=5, help="size of the 'make the cut' set (default 5)")
ap.add_argument("--noise", type=float, default=0.3, help="+/- fractional jitter per input (default 0.3)")
ap.add_argument("--runs", type=int, default=5000)
ap.add_argument("--seed", type=int, default=7)
a = ap.parse_args()
if not 0 <= a.noise < 1:
fail("--noise must be in [0, 1)")
method, rows, warnings = load(a.csv, a.method)
top = max(1, min(a.top, len(rows)))
for r in rows:
r["score"] = score(method, r)
rows.sort(key=lambda r: r["score"], reverse=True)
share = stability(method, rows, top, a.noise, a.runs, a.seed)
width = max(len(r["item"]) for r in rows)
if method == "rice":
print(f"{'#':>2} {'item':<{width}} {'reach':>8} {'impact':>6} {'conf':>5} {'effort':>6} {'RICE':>9} top{top}")
else:
print(f"{'#':>2} {'item':<{width}} {'impact':>6} {'conf':>5} {'ease':>5} {'ICE':>6} top{top}")
for i, r in enumerate(rows, 1):
s = share[r["item"]]
flag = " unstable" if 0.2 <= s <= 0.8 else ""
if method == "rice":
print(f"{i:>2} {r['item']:<{width}} {r['reach']:>8g} {r['impact']:>6g} {r['confidence']:>5.0%} "
f"{r['effort']:>6g} {r['score']:>9,.0f} {s:>4.0%}{flag}")
else:
print(f"{i:>2} {r['item']:<{width}} {r['impact']:>6g} {r['confidence']:>5g} {r['ease']:>5g} "
f"{r['score']:>6.0f} {s:>4.0%}{flag}")
unstable = [r["item"] for r in rows if 0.2 <= share[r["item"]] <= 0.8]
print(f"\nStability: inputs jittered +/-{a.noise:.0%} over {a.runs} runs; 'top{top}' = share of runs in the top {top}.")
if unstable:
print("Unstable near the cut line (validate their inputs first): " + ", ".join(unstable))
else:
print("Ranking around the cut line is stable at this noise level.")
for w in warnings:
print(f"warning: {w}")
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.