The scoping conversation that stops a project going wrong in month three
Scope failures are discovered late and paid for by the engineer. These are the questions that surface them early.
TL;DR
A scoping conversation usually establishes what the client wants built. That is the easy half, and it is not where projects fail.
Projects fail on things the client knows and nobody asks about: who signs off, what the data actually looks like, which system is the source of truth, what happens when the legacy integration is not as documented. Each is cheap to ask before pricing and expensive to discover in month three — and the cost lands on whoever quoted a fixed number.
The fix is a fixed question set, asked in a specific order, where each answer changes something concrete in the statement of work. A question whose answer changes nothing is theatre.
The case this post is about
A six-week integration: sync a client's inventory system into a storefront. Well-specified by the standards of the work — a document, a data dictionary, an API reference.
scope bidirectional inventory sync, 4 entity types
duration 6 weeks
shape fixed price
spec 11 pages, supplied by the client
contacts one: the product lead who wrote the spec
I asked a lot of questions about the spec, which was the thing in front of me. I asked almost none about the environment the spec lived in.
The symptom
Week three, the data was not the data. The dictionary described a clean sku as the join key.
In production:
total SKUs 48,210
null or empty sku 3,104 (6.4%)
duplicate sku across warehouses 1,890
sku with trailing whitespace 6,422
"TBC-" placeholder prefixes 811
None of this was hidden. Nobody had been asked, so nobody had said.
Week four, a second approver appeared. The warehouse operations manager had not been in any conversation and had a veto over anything touching stock counts — reasonably, since they owned the consequences. Two of my four entity types needed redesigning to satisfy constraints that had never been stated because the person holding them was not in the room.
Week five, "source of truth" turned out to be unresolved internally. When inventory disagreed between the two systems, I asked which one won. The answer was a two-week discussion among three people, during which I could not write the reconciliation logic that was the core of the project.
And the project ran eleven weeks against a six-week fixed price. None of the overrun was engineering difficulty. All of it was discovering facts that existed before I quoted.
Why the obvious fix failed
"Write a more detailed specification." The spec was eleven pages and accurate about what it covered. More pages about the same subject would not have surfaced a stakeholder nobody mentioned or data quality nobody had examined. Depth on the wrong axis.
"Add a contingency buffer." I had added 20%. The overrun was 83%. Buffers price estimation risk, and this was discovery risk — the client held information I did not, and a percentage cannot size something neither party has looked at.
"Charge time and materials." Correct, and I have written about that elsewhere. It changes who pays for the discovery and does not reduce it: eleven weeks is still eleven weeks of the client's money and my calendar, and a project that overruns by 83% damages the relationship even when the invoice is defensible.
"Ask better questions." True and useless as advice. I thought I was asking good questions; I was asking good questions about the specification. What was missing was not effort or thoughtfulness, it was coverage — a defined set, so that the question I would not have thought of gets asked anyway.
The actual mechanism
Scope risk has a shape, and the shape explains why it is discovered late.
The client holds the information, and does not know it is information. Dirty SKU data is not a secret. It is so normal to the people living with it that it does not occur to them as something to mention — the same way you would not tell a new colleague that the build is sometimes flaky. Nothing is being withheld; the question simply was not asked.
Discovery cost is paid by whoever carries the estimate. A fact that surfaces in week three has already been designed around. The rework is real work, and under a fixed price it is unpaid.
And the facts that matter are not about the software. They are about people (who approves), reality (what the data looks like), authority (which system wins), and history (what was tried before). None of them appear in a technical specification, because a specification describes an intended end state rather than the conditions it has to survive.

A specification tells you what the client wants. The scoping conversation has to tell you what the client has — and those are different documents with different authors, one of which does not exist yet.
The fix
A fixed question set, asked before pricing, where every answer maps to a clause. The mapping is the discipline: an answer that changes nothing was decoration.
from dataclasses import dataclass
@dataclass
class Answer:
question: str
value: str | None # None = "they do not know", which is itself the finding
changes: str # what this answer alters in the SOW
QUESTIONS = [
("Who, by name, signs off on this being done?", "approval"),
("Who else can say no after that?", "approval"),
("Can I see 1,000 real rows today, not the schema?", "data"),
("When the two systems disagree, which one is right?", "authority"),
("Has anyone attempted this before? What happened?", "history"),
("Which integration has no documentation you trust?", "discovery"),
("What date does this become worthless if missed?", "deadline"),
]
def assess(answers):
blockers, clauses = [], []
for a in answers:
if a.value is None:
# The CLIENT not knowing is the highest-value outcome of the whole
# conversation. It is an unresolved internal question that will become
# my blocked week if it is not resolved before the work starts.
blockers.append(f"UNRESOLVED: {a.question}")
continue
clauses.append(f"{a.changes}: {a.value}")
if blockers:
return ("discovery-phase-first", blockers,
"Price 2-3 days of paid discovery. Deliverable is the answers "
"and the estimate — not code.")
return ("proceed-to-pricing", [], clauses)
Run against the eleven answers I eventually had, scored as they would have been on day one:
>>> assess(day_one_answers)
('discovery-phase-first',
['UNRESOLVED: Who else can say no after that?',
'UNRESOLVED: When the two systems disagree, which one is right?'],
'Price 2-3 days of paid discovery. Deliverable is the answers and the estimate — not code.')
Two unresolved answers, both available for the asking in the first conversation, and both of them are precisely what cost weeks four and five. The warehouse manager existed on day one. The source-of-truth question was unanswered on day one — it took the client two weeks to decide, and those two weeks could have happened before the project started instead of in the middle of it.
The None branch is the one I would defend hardest. The instinct when a client cannot answer is
to help them along, offer a reasonable default, and move on — it keeps the conversation
comfortable and it converts their open question into my assumption. Recording it as a blocker
instead keeps it theirs, which is both more accurate and more useful to them.
Three notes on asking these well:
"Can I see real rows" is the highest-yield question in the set, and it has to be the rows rather than the schema. Every data surprise in this project would have been visible in a thousand-row sample in about four minutes.
"Who else can say no" has to be asked explicitly, because the person in front of you genuinely believes they are the approver. They are not lying; organisations have veto-holders who do not appear on org charts, and the question is the only way they surface early.
And "has anyone tried this before" catches the most expensive category. A previous failed attempt means there is a reason it failed, the reason is usually still true, and someone in the building knows what it is.
How we proved it
Applied to the next six projects, with the question set run before any number was quoted:
project unresolved answers action taken estimate vs actual
A 0 priced directly 1.05×
B 2 discovery phase, 3d 1.10×
C 0 priced directly 0.95×
D 3 discovery phase, 2d 1.15×
E 1 discovery phase, 2d 1.00×
F 0 priced directly 1.20×
Against the previous ten projects, where the median was 1.25× and the worst was 2.8×. The spread is what changed more than the median: nothing in the six ran past 1.20×, and the three that would have been the dangerous ones were converted into paid discovery before a fixed number existed.
That the discovery phases paid for themselves. Three of them, 2–3 days each, all billed. Each produced an estimate the project then hit — and project D's discovery found a legacy integration with no documentation that would, on the previous way of working, have been another week five.
And one that is worth reporting because it undercuts the tidy story. Project F had zero unresolved answers, was priced directly, and still ran 1.20× over — on genuine engineering difficulty in a component I had misjudged. The question set addresses discovery risk. It does not address estimation risk, and nothing in these six projects suggests otherwise.
Limits. Six projects is a small sample, they were my own work scored by me, and I chose which projects to take during that period. The strongest claim I will make is the one about project spread rather than the median.
What this generalizes to
Ask about conditions, not just requirements. A requirement describes the destination. The conditions — who approves, what the data is, which system wins — describe the terrain, and the terrain is what the estimate is actually a claim about.
"The client does not know" is a finding, not a dead end. It is the single most valuable answer in the set, because it names an internal decision that will otherwise block you from the inside of the project rather than the outside.
A question that changes nothing should not be asked. Every question in a scoping conversation should map to a clause, a price, a phase or a risk. Questions that only demonstrate diligence cost the client's patience and buy nothing.
And look at the data before you price the work on it. Not the schema, not the dictionary, not the API docs — a thousand real rows. It is four minutes, it is the cheapest risk reduction available in this entire job, and in my case it would have paid for itself about two hundred times over.
Related
- Fixed price, three unpaid weeks, and the risk I did not know I was holding — who holds which risk, by contract shape
- Scope creep is a contract problem, not a communication problem — what to do when scope changes anyway
- An estimate is a distribution, and you are reporting the mode — why the number you give is the wrong statistic
Keep reading
This is part of Freelancing & consulting for engineers — the commercial side, with the numbers attached. Follow MobileByteSensei for the rest.
One post a week, no digest
New writing when it lands. No roundups, no course pitch, no "10 tips".