Module 7: Saying No And The Roadmap
WSJF: Weighted Shortest Job First
Overview
You already have the two pieces: costOfDelay (lesson 3, how much Mercado loses per week a bet isn't alive) and jobSize (the same effort from RICE, how much it costs to build). This lesson combines them into a single formula: WSJF, Weighted Shortest Job First, the model the Scaled Agile Framework (SAFe) uses to order a backlog by value delivered per unit of time, not just total value. You're going to write the function, run it over Mercado's full backlog, and watch the order change against RICE's — sometimes dramatically.
How this connects to the module. RICE (module 3) answers "how much is this worth?". WSJF answers a different question: "how much value does this deliver for every week the team spends on it, taking into account what it costs to wait?". Both questions are legitimate and both matter, but they don't always agree — and when they don't, WSJF is the criterion specifically designed to decide build order, which is the question that opens this module.
An analogy: the supermarket line
You're at the supermarket with two open registers. Register A has a single person with a full cart —it's going to take a while—. Register B has three people, each with two or three items —it looks longer, but each person gets through fast—. If you choose a register by "which has fewer people", you choose A and you're wrong: A has a single person, but that person is going to make you wait longer than the three at B combined. The right question isn't "how many people are there?" nor even "how much is my time worth?" in the abstract — it's "how much do I advance per minute of waiting in each line?". That's exactly WSJF: it doesn't ask only how much a bet is worth (like RICE) nor only how much it costs to wait for it (like cost of delay alone); it asks how much value you receive per unit of time the team dedicates to it.
RICE answers: "how much is this worth" (reach x impact x confidence / effort)
Cost of delay answers: "how much do I lose waiting" ($ per week it isn't alive)
WSJF answers: "how much value per week costOfDelay
of team work" = ───────────
jobSize
Verifying the formula before coding it
Before writing a single line, it's worth confirming the formula exactly as SAFe, WSJF's source, defines it: WSJF is the relative cost of delay divided by the relative duration of the work — WSJF = Cost of Delay / Job Duration. The idea comes from Donald Reinertsen's work (who called it CD3, "Cost of Delay Divided by Duration"), and SAFe adopted it under that name. The intuition behind the formula, and why it divides instead of subtracting or adding: two bets can have the same cost of delay, but if one takes three times as long to build, that bet "blocks" the team three times as long while generating the same value per week of waiting — that's why job size goes in the denominator, just like effort in RICE.
Worked example: Mercado's full backlog, with time included
We reuse, unchanged, module 3's five-bet backlog —same reach, impact, confidence, and effort— and add the costOfDelay each bet carries from lesson 3 (or an equivalent estimate for the ones you hadn't seen yet). jobSize reuses exactly the same effort that already fed RICE — it isn't a new estimate, it's the same number seen from a different angle.
// WSJF (Weighted Shortest Job First) = costOfDelay / jobSize (SAFe).
// We reuse EXACTLY module 3's 5-bet backlog (same reach/impact/confidence/
// effort), and add costOfDelay ($/week, team estimate, pedagogical model).
// jobSize reuses the same effort (person-months) that already fed RICE.
function riceScore({ reach, impact, confidence, effort }) {
return (reach * impact * confidence) / effort;
}
function wsjf({ costOfDelay, jobSize }) {
return costOfDelay / jobSize;
}
function prioritize(items) {
return items.map((item) => ({ ...item, score: riceScore(item) })).sort((a, b) => b.score - a.score);
}
function sequence(items) {
return items
.map((item) => ({ ...item, wsjfScore: wsjf({ costOfDelay: item.costOfDelay, jobSize: item.effort }) }))
.sort((a, b) => b.wsjfScore - a.wsjfScore);
}
const backlog = [
{ feature: 'fasterCheckout', reach: 8000, impact: 2, confidence: 0.8, effort: 2, costOfDelay: 8000 },
{ feature: 'recommendations', reach: 5000, impact: 1, confidence: 0.5, effort: 3, costOfDelay: 18000 },
{ feature: 'sellerTools', reach: 1200, impact: 2, confidence: 0.8, effort: 2, costOfDelay: 6000 },
{ feature: 'reviews', reach: 6000, impact: 0.5, confidence: 0.8, effort: 1, costOfDelay: 9000 },
{ feature: 'improvedSearch', reach: 9000, impact: 1, confidence: 0.5, effort: 3, costOfDelay: 6000 },
];
console.log('=== Order by RICE (module 3): value/effort, no time ===\n');
const byRice = prioritize(backlog);
byRice.forEach((b, i) => console.log(' ' + (i + 1) + '. ' + b.feature.padEnd(16) + 'riceScore=' + (Math.round(b.score * 100) / 100)));
console.log('\n order: ' + byRice.map((b) => b.feature).join(' > '));
console.log('\n=== Order by WSJF: costOfDelay / jobSize ===\n');
const byWsjf = sequence(backlog);
byWsjf.forEach((b, i) =>
console.log(
' ' + (i + 1) + '. ' + b.feature.padEnd(16) +
'costOfDelay=$' + b.costOfDelay.toLocaleString('en-US') + '/wk' +
' jobSize=' + b.effort + 'pm' +
' -> wsjf=' + b.wsjfScore
)
);
console.log('\n order: ' + byWsjf.map((b) => b.feature).join(' > '));
console.log('\n=== What changed ===\n');
const riceOrder = byRice.map((b) => b.feature);
const wsjfOrder = byWsjf.map((b) => b.feature);
riceOrder.forEach((feature, i) => {
const newPos = wsjfOrder.indexOf(feature);
const moved = newPos === i ? 'same' : newPos < i ? 'moved up' : 'moved down';
console.log(' ' + feature.padEnd(16) + 'RICE #' + (i + 1) + ' -> WSJF #' + (newPos + 1) + ' (' + moved + ')');
});
What to expect. When you run the file with Node, the output is exactly this:
=== Order by RICE (module 3): value/effort, no time ===
1. fasterCheckout riceScore=6400
2. reviews riceScore=2400
3. improvedSearch riceScore=1500
4. sellerTools riceScore=960
5. recommendations riceScore=833.33
order: fasterCheckout > reviews > improvedSearch > sellerTools > recommendations
=== Order by WSJF: costOfDelay / jobSize ===
1. reviews costOfDelay=$9,000/wk jobSize=1pm -> wsjf=9000
2. recommendations costOfDelay=$18,000/wk jobSize=3pm -> wsjf=6000
3. fasterCheckout costOfDelay=$8,000/wk jobSize=2pm -> wsjf=4000
4. sellerTools costOfDelay=$6,000/wk jobSize=2pm -> wsjf=3000
5. improvedSearch costOfDelay=$6,000/wk jobSize=3pm -> wsjf=2000
order: reviews > recommendations > fasterCheckout > sellerTools > improvedSearch
=== What changed ===
fasterCheckout RICE #1 -> WSJF #3 (moved down)
reviews RICE #2 -> WSJF #1 (moved up)
improvedSearch RICE #3 -> WSJF #5 (moved down)
sellerTools RICE #4 -> WSJF #4 (same)
recommendations RICE #5 -> WSJF #2 (moved up)
Look at the size of the change. recommendations was RICE's last (#5, with the backlog's lowest score, dragged down by its low confidence) and jumps to WSJF's second spot — because its costOfDelay ($18,000/week, the backlog's highest) is so large it completely offsets its longer jobSize. fasterCheckout, which dominated RICE by more than double the second place, falls to third on WSJF — its costOfDelay is solid but not exceptional, and it isn't enough to keep first place against bets that return more value per week of the team's work. Only sellerTools stays in exactly the same spot (#4 on both) — a reminder WSJF doesn't invert the entire order, it only reorders where time makes the difference.
Why this doesn't invalidate RICE
It's worth saying this with the same clarity module 3 used to defend scoring's limits: WSJF doesn't "correct" RICE nor make it useless. They answer different questions, and both are necessary at different moments in the process. RICE is the right tool for deciding which bets deserve to enter the prioritized backlog, with the information you have at the start —reach, impact, confidence, effort—. WSJF is the right tool for deciding in what order to build them, once you already know how much it costs to wait for each one. A team that only uses RICE can end up building, first, the bet that's least urgent — as happened here with fasterCheckout. A team that only uses WSJF, without having gone through RICE first, risks sequencing bets that never should have been in the backlog to begin with — because WSJF doesn't ask "is this worth it at all?", it only asks "in what order, given we already decided yes?".
Common mistakes
Calculating WSJF with one person's jobSize when the team works in parallel. What happens: the team estimates jobSize in person-months (total effort), but when building the roadmap assumes that time translates directly into calendar weeks, ignoring that several people can work on the same bet at once. Why it happens: person-months and calendar weeks look similar enough to be confused without a second thought. How to spot it: if your recommendations's jobSize of 3 person-months assumes it takes exactly 3 calendar months, without considering how many people are building it at once, your real-time estimate is probably wrong. How to fix it: use jobSize as a relative size measure (to compare bets against each other, which is what WSJF needs), and convert it to real calendar time only once you already know how many team members are going to be assigned — that adjustment belongs to the engineering team, not to the formula.
Using WSJF to justify a decision already made for another reason. What happens: someone already decided they want to build a certain bet first (personal preference, stakeholder pressure) and "adjusts" costOfDelay upward until WSJF proves them right. Why it happens: the same temptation to manipulate confidence in RICE module 3 already warned about, now applied to costOfDelay. How to spot it: if someone's "favorite" bet's costOfDelay changed right after WSJF didn't give it first place, and there's no new evidence justifying it, it's manipulation, not a legitimate revision. How to fix it: the same traceability discipline from the whole guide — every costOfDelay must be explainable with a concrete, verifiable reason, regardless of who benefits from the result.
Treating WSJF's order as permanent, without recalculating it. What happens: the team runs sequence() once, at the start of the quarter, and follows that order even as conditions change —a bet turns out more expensive than expected, or evidence appears that another is more urgent than thought—. Why it happens: recalculating feels like reopening an already-closed decision. How to spot it: if the build order hasn't been rerun since the quarter started, despite the team now knowing things it didn't know at the start, the WSJF you're following may be outdated. How to fix it: like with RICE's sensitivity analysis in module 3, WSJF should be recalculated when real evidence appears that changes costOfDelay or jobSize — not to artificially move the order, but so the order keeps reflecting what the team knows today.
Exercises
Exercise 1 — Calculate a sixth bet's WSJF. Mercado's team adds a new candidate to the backlog: sellerAnalytics (a metrics panel for sellers), with costOfDelay: 4500 and jobSize: 1. Calculate its wsjfScore and say where in this lesson's ranking it would land.
See solution
wsjf = 4500 / 1 = 4500. Compared to the lesson's ranking (reviews 9000, recommendations 6000, fasterCheckout 4000, sellerTools 3000, improvedSearch 2000), 4500 lands between recommendations (6000) and fasterCheckout (4000) — in position #3, pushing fasterCheckout, sellerTools, and improvedSearch down one spot each. The full order would be: reviews (9000) → recommendations (6000) → sellerAnalytics (4500) → fasterCheckout (4000) → sellerTools (3000) → improvedSearch (2000).
Exercise 2 — Predict without calculating. Without doing the math yet, what would happen to improvedSearch's wsjfScore (currently the backlog's lowest, at 2000) if the team discovers evidence its real costOfDelay is double the estimate ($12,000/week instead of $6,000/week)? Would it beat sellerTools (3000)? Now calculate it and confirm your prediction.
See solution
Reasonable prediction: doubling costOfDelay doubles wsjfScore (the formula is a direct division, linear in the numerator), so improvedSearch would go from 2000 to 4000 — and that would indeed beat sellerTools (3000). Calculation: wsjf = 12000 / 3 = 4000. Confirmed: 4000 beats sellerTools (3000) and ties fasterCheckout (4000) but doesn't beat recommendations (6000) nor reviews (9000). improvedSearch would rise from last place to third (tied with fasterCheckout), a big jump caused by a single revised input — the same sensitivity lesson module 3 taught with RICE, now applied to WSJF.
Exercise 3 — Explain recommendations's reversal to someone who only knows RICE. A coworker who saw module 3's ranking asks, confused: "how is it possible that recommendations, which had the worst RICE score of the five, is now the second priority?". Write, in 2-3 sentences, the explanation you'd give them, using this lesson's exact numbers.
See solution
A possible answer: "RICE gave recommendations a low score (833.33) because its confidence is low (0.5) — we're not that sure how much it's going to move the metric—. But that measures certainty, not urgency. When we add costOfDelay —how much we lose for every week recommendations isn't alive, which is the backlog's highest at $18,000/week— and divide it by its jobSize (3 person-months, same as before), the result is a WSJF of 6000, the second highest of the five. It's not that RICE was 'wrong': RICE never asked how much it costs to wait, and it turns out waiting for recommendations is very expensive." The key to the answer: naming that the two formulas use different inputs and answer different questions, not that one is more "correct" than the other.
Summary and next step
In this lesson you built wsjf({costOfDelay, jobSize}) and sequence(backlog), verified SAFe's formula (cost of delay ÷ job duration), and ran it over Mercado's full backlog. The result dramatically inverted RICE's order: recommendations jumped from last place to second, and fasterCheckout fell from first to third — not because RICE was wrong, but because WSJF answers a different question: how much value each bet delivers per week of team work, taking into account what it costs to wait for it.
Before moving on you should be able to: write the WSJF formula from memory and explain each of its two terms; run sequence() over a backlog with costOfDelay and jobSize; and explain, with a concrete example, why WSJF's order can differ so much from RICE's.
You already have the order. Lesson 5 takes the next step: turning that sequence into something more than an ordered list — a real roadmap, with outcomes tied to each bet, and numeric proof of why that specific order, and not another, costs Mercado the least overall.
Resources
- Scaled Agile Framework, "WSJF" — framework.scaledagile.com/wsjf. The formula's primary source:
Cost of Delay / Job Duration, with the full breakdown of cost of delay's three factors. In English. - Donald G. Reinertsen, The Principles of Product Development Flow — goodreads.com/book/show/6278270. WSJF's mathematical origin (under the name CD3) and the proof of why this sequence minimizes a portfolio's total cost of delay — the argument lesson 5 picks back up. In English.
- Intercom, "RICE: Simple prioritization for product managers" — intercom.com/blog/rice-simple-prioritization-for-product-managers. Useful to reread with this lesson fresh: compare what question RICE answers and at what point in the process, against WSJF. In English.