Phase 2: Discovery & AI Product Strategy · 40 min · Jobs-to-be-done · Opportunity Solution Tree · Python
The Problem
A VP of Product at a mid-size logistics SaaS company comes back from a conference with a mandate: "We need an AI story for the board deck by Q3." The PM team spends two weeks brainstorming. Someone suggests a chatbot for the support page. Someone else wants an "AI insights" widget on the dashboard because a competitor just shipped one. A third idea is a document-summarization feature nobody asked for, because the engineering team already has a RAG pipeline half-built from a hackathon.
None of these started from a user's job-to-be-done. They started from "we have a model, what do we point it at" — and that's backwards. Six months later, two of the three features ship, adoption is under 4%, and the board asks why the "AI story" didn't move any real metric. This is the single most common failure mode in AI product work right now: teams that are excellent at shipping model capability and terrible at identifying which capability actually removes friction from a job a user is already trying to do.
The fix isn't a better model. It's discipline about where opportunities come from. An AI opportunity is not "a place we could use AI" — it's a job the user is already hiring some worse solution to do (a spreadsheet, a manual process, three tabs open at once, a coworker who's the "expert"), where AI closes the gap between what exists and what the job actually requires.
Jobs-to-be-done, applied to AI
Jobs-to-be-done (JTBD) reframes discovery around the progress a user is trying to make, not the feature they're asking for. The classic form is: when [situation], I want to [motivation], so I can [expected outcome]. Applied to AI opportunities, this framing does something specific and useful: it forces you to describe the job in a way that doesn't mention AI at all. If you can't state the job without saying "AI," you don't have a job yet — you have a technology.
Take the logistics company's support chatbot idea. The lazy version: "We should add an AI chatbot to support." The JTBD version: "When a customer's shipment is delayed and they can't find a tracking update, they want a fast, specific answer without waiting in a phone queue, so they can decide whether to reroute inventory." Notice what changed — the second version tells you what "good" looks like (fast, specific, no queue), which lets you evaluate whether a chatbot is even the right tool, versus a better-surfaced tracking page, a proactive delay notification, or nothing at all.
Teresa Torres' Opportunity Solution Tree makes this a structured practice: a desired outcome at the root, opportunities (unmet needs, pain points, desires uncovered through user research) as the branches, and solutions as leaves considered only after opportunities are mapped and prioritized. The discipline that matters for AI specifically is resisting the urge to skip straight from outcome to "add AI" as a solution — you still have to walk through opportunities, and most teams don't.
If the job survives without the word "AI," you have a real opportunity
Write the job-to-be-done for your candidate feature, then delete every instance of "AI," "chatbot," "model," or "GenAI" from the sentence. If what's left still clearly describes a real, specific job a user struggles with today, you have an opportunity worth sizing. If the sentence collapses into nothing without those words, you had a technology pitch wearing a JTBD costume — and it will show up later as low adoption, because you never actually validated that users had the underlying problem.
Sizing the opportunity
Once you have a real job-to-be-done, size it before you get attached to it. Three inputs do most of the work:
- Frequency — how often does this job come up? A job a power user hits fifty times a day is a different opportunity than one a user hits once a quarter, even if the pain is identical.
- Pain intensity — how bad is the current experience? Rage-inducing manual work scores higher than mild annoyance, independent of frequency.
- Current workaround cost — what is the user doing today instead, and what does that workaround cost them (time, error rate, a coworker's attention, a $40k-a-year "senior analyst who remembers everything")? The more expensive the workaround, the more room an AI solution has to demonstrate value, because you're not competing against "nothing," you're competing against a specific, measurable cost.
A fourth factor that's easy to skip and expensive to skip: AI fit. Not every high-frequency, high-pain job is a good AI opportunity. A job that requires a deterministic, auditable answer (calculating a regulated tax figure) is a bad fit for a probabilistic system even if the pain is real — that's a job for a validated rules engine, not an LLM. Save the deterministic-vs-probabilistic judgment call for the build/buy/model decisions later in this phase; at the sizing stage, just flag it so a bad-fit opportunity doesn't get scored artificially high.
Two candidate opportunities surface from user interviews at the logistics company. Opportunity A: warehouse managers manually reconcile shipment manifests against inventory counts every morning, a rigid, auditable calculation that must match exactly for compliance reasons — takes 45 minutes, done by everyone, every single day. Opportunity B: new hires occasionally (roughly once a month) struggle to find the right internal policy document when a customer asks an unusual question, and end up guessing or escalating. Which is the stronger AI opportunity, and why?
Frequency and pain intensity matter, but they don't override AI fit. Opportunity A is high-frequency and high-pain, but the job itself demands an exact, auditable, deterministic match for compliance — exactly the shape of problem a probabilistic model handles badly, and where a validated reconciliation tool or rules engine is the right solution, not an LLM. Opportunity B is lower-frequency, but the underlying job — finding the right answer inside a pile of unstructured policy documents — is a textbook strong fit for retrieval-augmented generation. A smaller, well-fit opportunity beats a larger, badly-fit one, because the badly-fit one will either underperform or need so many guardrails that it stops looking like an AI win at all.
From opportunity list to opportunity scorecard
The output of this lesson isn't a gut feeling about which idea to build — it's a ranked list you can defend in a roadmap review. That means writing down, for every candidate opportunity: the job-to-be-done statement (AI-word-free), frequency, pain intensity, workaround cost, and an AI-fit judgment, then combining them into a comparable score. This is exactly the kind of artifact that survives contact with a skeptical engineering lead or a time-constrained VP, because it shows the reasoning, not just the conclusion.
It's also the first building block of the phase's capstone project (Lesson 6), where you'll pick one real opportunity and carry it through build/buy decisions, model selection, and a full roadmap — so the discipline of writing this down cleanly now pays off directly later.
A scorecard ranks five opportunities. Opportunity X has the highest raw score (frequency + pain + workaround cost combined) of the group, but a MODERATE AI-fit rating. Opportunity Y has a lower raw score but a STRONG AI-fit rating, and ends up ranked above X once the fit-adjusted score is computed. A stakeholder asks why you're recommending the "smaller" opportunity Y over the "bigger" one X. What's the correct answer?
Raw score alone measures the size of the underlying job — frequency, pain, and workaround cost — but says nothing about whether an AI solution can actually deliver on that job well. A MODERATE-fit opportunity carries real execution risk: it may need extensive guardrails, human review, or simply underperform relative to its apparent size, while a STRONG-fit opportunity is far more likely to convert its (smaller) raw size into real, realized value. Recommending the fit-adjusted leader over the raw-score leader is exactly the judgment call this lesson is teaching — bigger is not better if the job is a bad match for what AI actually does well.
Build
Build it
The code artifact lesson_02_01_spotting_ai_opportunities.py implements an opportunity-sizing scorer:
Opportunity — a dataclass capturing the JTBD statement, frequency (times/week), pain intensity (1-5), workaround cost in dollars/year, and an AIFit ratingAIFit — an enum (STRONG, MODERATE, WEAK) representing how well the job matches a probabilistic AI solution versus a deterministic onescore_opportunity() — combines frequency, pain, and workaround cost into a 0-100 opportunity score, then applies an AI-fit multiplier so a WEAK-fit opportunity can never outrank a STRONG-fit one regardless of raw sizerender_opportunity_scorecard() — produces a markdown-ranked table of all candidate opportunities with their component scores and a one-line rationale- A worked example scoring five candidate opportunities for "Northbeam Logistics," a mid-size freight-tracking SaaS company, including one deliberately high-frequency but poor-fit opportunity to show the multiplier at work
"""
lesson_02_01_spotting_ai_opportunities.py
An AI opportunity-sizing scorer applying jobs-to-be-done framing.
Takes a list of candidate opportunities -- each stated as a JTBD, sized by
frequency, pain intensity, and current workaround cost, and rated for AI
fit -- and produces a ranked, defensible opportunity scorecard. This is the
artifact a PM brings into a roadmap prioritization review instead of a list
of feature ideas.
Usage:
python lesson_02_01_spotting_ai_opportunities.py
No external dependencies, no network calls.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
class AIFit(Enum):
"""How well the underlying job matches a probabilistic AI solution."""
STRONG = 1.0 # unstructured, judgment-heavy, tolerant of occasional error
MODERATE = 0.65 # partially structured, some tolerance for error with review
WEAK = 0.25 # requires exact, auditable, deterministic output
@dataclass
class Opportunity:
name: str
jtbd: str # "When [situation], I want to [motivation], so I can [outcome]"
frequency_per_week: float
pain_intensity: int # 1-5
workaround_cost_per_year: float # dollars
ai_fit: AIFit
fit_rationale: str
@dataclass
class ScoredOpportunity:
opportunity: Opportunity
frequency_score: float
pain_score: float
cost_score: float
raw_score: float
fit_adjusted_score: float
def _normalize(value: float, ceiling: float) -> float:
"""Scale a raw value to 0-100 against a practical ceiling, capped at 100."""
return round(min(value / ceiling, 1.0) * 100, 1)
def score_opportunity(opp: Opportunity) -> ScoredOpportunity:
# Weekly frequency ceiling of 35 (5x/day) treated as "maximally frequent"
frequency_score = _normalize(opp.frequency_per_week, ceiling=35)
# Pain intensity is already 1-5; scale to 0-100
pain_score = _normalize(opp.pain_intensity, ceiling=5)
# Workaround cost ceiling of $150k/year treated as "maximally expensive"
cost_score = _normalize(opp.workaround_cost_per_year, ceiling=150_000)
raw_score = round(
0.35 * frequency_score + 0.30 * pain_score + 0.35 * cost_score, 1
)
fit_adjusted_score = round(raw_score * opp.ai_fit.value, 1)
return ScoredOpportunity(
opportunity=opp,
frequency_score=frequency_score,
pain_score=pain_score,
cost_score=cost_score,
raw_score=raw_score,
fit_adjusted_score=fit_adjusted_score,
)
def render_opportunity_scorecard(opportunities: list[Opportunity]) -> str:
scored = sorted(
(score_opportunity(o) for o in opportunities),
key=lambda s: s.fit_adjusted_score,
reverse=True,
)
lines = [
"# AI Opportunity Scorecard",
"",
"| Rank | Opportunity | Fit-Adjusted Score | Raw Score | AI Fit | Freq/wk | Pain (1-5) | Workaround Cost/yr |",
"|---|---|---|---|---|---|---|---|",
]
for i, s in enumerate(scored, start=1):
o = s.opportunity
lines.append(
f"| {i} | {o.name} | {s.fit_adjusted_score:.1f} | {s.raw_score:.1f} | "
f"{o.ai_fit.name} | {o.frequency_per_week:g} | {o.pain_intensity} | "
f"${o.workaround_cost_per_year:,.0f} |"
)
lines += ["", "## Job-to-be-done statements and fit rationale", ""]
for s in scored:
o = s.opportunity
lines.append(f"### {o.name} — fit-adjusted score {s.fit_adjusted_score:.1f}")
lines.append(f"- **JTBD:** {o.jtbd}")
lines.append(f"- **AI fit ({o.ai_fit.name}):** {o.fit_rationale}")
lines.append("")
top = scored[0]
lines.append(
f"**Recommendation:** Prioritize **{top.opportunity.name}** "
f"(fit-adjusted score {top.fit_adjusted_score:.1f}/100). "
)
return "\n".join(lines)
# ── Worked example: Northbeam Logistics, a mid-size freight-tracking SaaS ──
def build_sample_opportunities() -> list[Opportunity]:
return [
Opportunity(
name="Manifest-to-inventory reconciliation",
jtbd=(
"When a warehouse manager closes out the morning shift, they "
"want shipment manifests to match inventory counts exactly, "
"so they can pass a compliance audit without manual rechecking."
),
frequency_per_week=35,
pain_intensity=4,
workaround_cost_per_year=60_000,
ai_fit=AIFit.WEAK,
fit_rationale=(
"Requires an exact, auditable match for compliance; a "
"probabilistic model introduces error risk a rules-based "
"reconciliation tool does not carry. High frequency and pain "
"do not overcome a fundamentally poor fit."
),
),
Opportunity(
name="Policy-document lookup for support reps",
jtbd=(
"When a support rep gets an unusual customer question, they "
"want to find the exact relevant policy passage in under a "
"minute, so they can answer without escalating or guessing."
),
frequency_per_week=40,
pain_intensity=3,
workaround_cost_per_year=45_000,
ai_fit=AIFit.STRONG,
fit_rationale=(
"Unstructured document retrieval with occasional imprecision "
"tolerated (rep reviews the passage before answering) is a "
"strong RAG fit."
),
),
Opportunity(
name="Delay-cause summarization for account managers",
jtbd=(
"When a shipment is delayed, an account manager wants a "
"plain-language summary of why, pulled from scattered carrier "
"logs, so they can proactively brief the customer before the "
"customer calls angry."
),
frequency_per_week=18,
pain_intensity=4,
workaround_cost_per_year=80_000,
ai_fit=AIFit.STRONG,
fit_rationale=(
"Summarizing unstructured, multi-source log text into a "
"readable narrative is a core LLM strength, and occasional "
"imprecision is low-stakes since the AM reviews before calling."
),
),
Opportunity(
name='"AI insights" dashboard widget',
jtbd=(
"A competitor shipped something with 'AI' in the name and "
"leadership wants parity."
),
frequency_per_week=2,
pain_intensity=1,
workaround_cost_per_year=0,
ai_fit=AIFit.MODERATE,
fit_rationale=(
"No underlying user job identified yet -- this is a "
"technology-parity request, not a sized opportunity. Included "
"here as a cautionary low scorer, not a real candidate."
),
),
Opportunity(
name="New-hire onboarding Q&A assistant",
jtbd=(
"When a new support hire doesn't know an internal process, "
"they want an immediate, accurate answer, so they can resolve "
"the ticket without pulling a senior teammate off their queue."
),
frequency_per_week=12,
pain_intensity=3,
workaround_cost_per_year=35_000,
ai_fit=AIFit.STRONG,
fit_rationale=(
"Same underlying retrieval job as the support-rep lookup "
"opportunity, applied to a different user segment; strong fit "
"for the same reasons."
),
),
]
if __name__ == "__main__":
sample = build_sample_opportunities()
print(render_opportunity_scorecard(sample))
print()
print(
"Takeaway: the highest-frequency, highest-workaround-cost opportunity "
"(manifest reconciliation) still ranks below two smaller, better-fit "
"opportunities once the AI-fit multiplier is applied. Size and fit are "
"both required inputs -- neither one alone produces a defensible rank."
)