Module 7: Synthesizing And Deciding

Persevere, pivot, or kill: `decide`

Overview

With synthesize() you know what's signal and what's noise (lessons 2 through 4), and with reachedSaturationAt() you know you've already collected enough evidence to trust that synthesis (lesson 5). But the step that gives meaning to everything before it is still missing: the team deciding what to do with that evidence. This lesson builds decide(), a model with three possible outputs —not two— that reflects the real options a product team has facing a bet: keep investing as it is, change approach without abandoning the problem, or stop entirely.

How this connects to the module. decide() takes a number —signalStrength— that summarizes how strong the accumulated evidence is, and compares it against thresholds agreed on ahead of time. In this lesson those numbers are illustrative scenarios, chosen to show the three possible outputs; in lesson 7 you're going to see, with a real model, where recommendations's corresponding signalStrength comes from — and in lesson 8's project you're going to run decide() with that real number, not with an example scenario.

An everyday analogy: the recipe that doesn't turn out as expected

You're cooking a new recipe and something doesn't turn out as expected. You actually have three options — not just two. You can follow the recipe as it is, if what went wrong was a minor detail and the rest is on track. You can change one ingredient — the recipe was still a good idea overall, but one specific part didn't work with what you had available, so you adjust that part and keep cooking the same dish with a variation. Or you can close the kitchen for today — if what went wrong reveals the whole recipe wasn't a good idea from the start, continuing to try to fix it plate after plate just wastes more ingredients.

Those three options —continue, change one ingredient, close the kitchen— are exactly persevere, pivot, and kill. The most common temptation, facing a product bet, is to think there are only two options: continue or abandon. decide() forces you to consider the third, the one almost always forgotten: the evidence can confirm the problem is real and worth solving, without confirming the specific solution you were building is the right one.

Worked example: decide() over three evidence scenarios

decide() takes a signalStrength —a number between 0 and 1 summarizing the accumulated evidence's strength— and a threshold object with two cutoffs, persevere and pivot, agreed on before seeing any result. If signalStrength clears the persevere cutoff, the bet continues as it is; if it doesn't reach that but does clear the pivot cutoff, there's real evidence the problem matters, but not that this specific solution is the right one; if it doesn't even reach that, the bet gets abandoned.

// L6: decide() -- given an accumulated evidence strength score
// (signalStrength, 0-1) and the cutoffs agreed on AHEAD OF TIME
// (threshold), decides whether the bet should continue (persevere),
// change approach within the same opportunity (pivot), or get
// abandoned (kill).
function decide(signalStrength, threshold) {
  if (signalStrength >= threshold.persevere) {
    return {
      signalStrength,
      action: 'persevere',
      reason: 'the accumulated evidence clears the threshold to keep investing as-is',
    };
  }
  if (signalStrength >= threshold.pivot) {
    return {
      signalStrength,
      action: 'pivot',
      reason: 'there is real signal but not enough to continue unchanged -- change solutions within the same opportunity',
    };
  }
  return {
    signalStrength,
    action: 'kill',
    reason: 'the accumulated evidence is not even enough to pivot -- abandon this bet',
  };
}

// The cutoffs get agreed on BEFORE seeing any result, just like module 5's
// fake door threshold.
const THRESHOLD = { persevere: 0.6, pivot: 0.3 };

console.log('=== decide() over three accumulated-evidence scenarios ===\n');
console.log('agreed thresholds: persevere >= ' + THRESHOLD.persevere + '  |  pivot >= ' + THRESHOLD.pivot + '\n');

const scenarios = [
  { label: 'strong evidence (several signals + positive fake door)', signalStrength: 0.75 },
  { label: 'mixed evidence (one clear signal, the rest ambiguous)', signalStrength: 0.45 },
  { label: 'weak evidence (only noise, or the test refuted the hypothesis)', signalStrength: 0.15 },
];

scenarios.forEach((s) => {
  const d = decide(s.signalStrength, THRESHOLD);
  console.log(s.label + ':');
  console.log('  signalStrength=' + d.signalStrength + '  -> ' + d.action.toUpperCase());
  console.log('  ' + d.reason + '\n');
});

What to expect. When you run the file with Node, the output is exactly this:

=== decide() over three accumulated-evidence scenarios ===

agreed thresholds: persevere >= 0.6  |  pivot >= 0.3

strong evidence (several signals + positive fake door):
  signalStrength=0.75  -> PERSEVERE
  the accumulated evidence clears the threshold to keep investing as-is

mixed evidence (one clear signal, the rest ambiguous):
  signalStrength=0.45  -> PIVOT
  there is real signal but not enough to continue unchanged -- change solutions within the same opportunity

weak evidence (only noise, or the test refuted the hypothesis):
  signalStrength=0.15  -> KILL
  the accumulated evidence is not even enough to pivot -- abandon this bet

Three scenarios, three different outputs, no ambiguity — each falls cleanly into one of the three ranges THRESHOLD defines. Notice the middle scenario: signalStrength: 0.45 doesn't reach the 0.6 cutoff for persevere, but it does clear the 0.3 cutoff for pivot — so decide() doesn't treat it as "failure," it treats it as "change approach." That's exactly the option a two-output-only model —continue or abandon— would have forced onto the wrong side: with mixed evidence, killing the whole bet would waste a real, already-confirmed opportunity, and continuing with no changes would ignore that the current solution, as it stands, doesn't have enough evidence behind it.

What pivot means in the context of a bet like recommendations

pivot, in Eric Ries's and The Lean Startup's sense, doesn't mean "abandon everything and start from scratch with a completely different idea" — it means changing direction while keeping one foot in what you already learned. For recommendations, a pivot result wouldn't discard the whole opportunity —"buyers don't find what they'd like"—, which module 3's tree already confirmed as real and high-impact. It would discard, instead, the specific solution —automatic personalized recommendations— in favor of another solution tackling the same opportunity, like the alternative already in the tree since module 3: hand-curated categories by trend. The user's problem stays the same; what changes is the bet on how to solve it.

Common mistakes

Treating any result that isn't "kill" as if it were "continue unchanged". What happens: the team runs decide(), sees the result isn't kill, and communicates it at the planning meeting simply as "we're continuing with recommendations" — without distinguishing whether the real result was persevere (continue as is) or pivot (change solution). Why it happens: kill is the only option that feels clearly negative, so anything else gets mentally grouped as "green light," losing the important distinction between the other two. How to spot it: if you ask the team "are we building exactly what we had planned, or does something change?", and the answer is "we're continuing, that's it," with no ability to say whether the result was persevere or pivot, the communication lost critical information. How to fix it: always communicate the three labels by their exact name —persevere, pivot, or kill— never collapse them into a "continuing" or "not continuing" binary; the difference between the first two completely changes what the team builds next week.

Defining persevere and pivot's thresholds after calculating the real signalStrength. What happens: the team runs the full synthesis, gets a signalStrength of, say, 0.55, and only then does someone propose "let's set the persevere threshold at 0.5" — conveniently, just below the number they already have. Why it happens: it's exactly the same mistake you already saw with module 5's fake door threshold, and with signalMinUsers in this module's lesson 4 — setting a criterion after seeing the result lets it get adjusted, without anyone consciously noticing, to confirm what the team already wanted to do. How to spot it: check the date the THRESHOLD cutoffs got agreed on against the date the real signalStrength got calculated — if the threshold got decided afterward, or the same day as the result, there's a reasonable suspicion of manipulation, intentional or not. How to fix it: THRESHOLD's cutoffs get agreed on at the same time the test gets designed (module 4) or, at the latest, before starting to synthesize the collected evidence — never after calculating the number that's going to be compared against them.

Exercises

Exercise 1 — Calculate the result at the exact boundary. Without running Node, what action does decide() return if signalStrength is exactly 0.6, with the worked example's same THRESHOLD (persevere: 0.6, pivot: 0.3)? Explain why, looking at the operator used in the code.

See solution

persevere. decide()'s first if uses >= (greater than or equal), not > (strictly greater) — so signalStrength: 0.6 does satisfy signalStrength >= threshold.persevere (0.6 >= 0.6 is true), and the function returns persevere on the first check, without even getting to evaluate the second if. This is a detail worth checking in any threshold function: if the operator were > instead of >=, a result exactly equal to the threshold would fall, by the edge, into the category below (pivot) — a small code difference with big consequences for a real bet.

Exercise 2 — Design a fourth scenario. Without running Node, propose a signalStrength value that would result in kill, different from the worked example's 0.15, and as close as possible to the boundary with pivot without crossing it.

See solution

Any value less than 0.3 (the pivot threshold) gives kill; the closest to the boundary without crossing it, within these examples' usual precision, would be 0.29 — just below the cutoff. It's worth noting that a result this close to the boundary (0.29 against a 0.3 threshold) deserves the same caution you already saw with module 5's fake door's tight margin: a signalStrength right at the edge probably warrants getting a bit more evidence before making a decision as final as kill, instead of blindly trusting the number landed, by very little, on the wrong side.

Exercise 3 — Explain why pivot isn't a third name for "I don't know". In 2-3 sentences, explain the difference between a pivot result from decide() and a real situation of team indecision ("we don't have enough information to decide anything").

See solution

A pivot result is a decision — specific and actionable: the evidence confirms the user's problem is real (that's why signalStrength clears pivot's minimum threshold), but it doesn't confirm the current solution is the right one, so the concrete action is to change solutions within the same opportunity, not to stand still. A real situation of indecision would be, instead, a signalStrength that couldn't even be calculated with confidence — for example, because lesson 5's synthesis showed saturation hadn't been reached yet—; in that case, the right response isn't running decide() with an unreliable number, but getting more evidence before deciding anything.

Summary and next step

In this lesson you built decide(signalStrength, threshold): it compares the accumulated evidence's strength against two cutoffs agreed on ahead of time, and returns one of three actions —persevere, pivot, or kill—, never just a continue-or-stop binary. Over three illustrative scenarios —strong, mixed, and weak evidence—, the function clearly told apart the three cases, including the middle one a simpler model would have forced toward the wrong extreme.

Before moving on you should be able to: explain the real difference between persevere and pivot, with recommendations's example against the curated-categories alternative; and manually calculate, for a given signalStrength, which of the three actions decide() would return.

One pending question remains, which this lesson left as an example number: where does recommendations's signalStrength come from, with a real model? Lesson 7 answers that, connecting this module's synthesis with the RICE confidence that's stayed capped at 0.3 since product-thinking-for-engineers.

Resources

  • Eric Ries, "Pivot, don't jump to a new vision" — startuplessonslearned.com/2009/06/pivot-dont-jump-to-new-vision.html. The original post where Ries defines the pivot as keeping one foot in what was learned while changing direction — this lesson's framework's direct source. In English.
  • Marty Cagan (Silicon Valley Product Group), "The Four Big Risks" — svpg.com/four-big-risks. A useful reminder before deciding: recommendations is, specifically, a value risk — and pivot, in this case, would mean changing the solution without changing the bet on which user problem is worth solving. In English.
  • Annie Duke, Thinking in Betsannieduke.com/annie-duke-thinking-in-bets. Already cited in product-thinking-for-engineers: her idea that a good decision isn't judged by whether the outcome turned out well, but by whether it was made with the right process, is exactly the spirit behind fixing THRESHOLD ahead of time in this lesson. In English.