AiBook · Jeremy Schoemaker · 2026 · ch-08.html

Stop Jamming the Whole Novel in One Prompt

(Spine Ch. 8.)

“Everyone knows that debugging is twice as hard as writing a program in the first place. So if you’re as clever as you can be when you write it, how will you ever debug it?” Brian W. Kernighan, The Elements of Programming Style, 2nd edition, chapter 2 (1978)

Three days. That is how long the bad reply took to track down, and I spent most of it re-reading the same 3,000-word prompt I wrote myself (the one that “mostly works”), looking for the paragraph that was lying. I could not tell you which one it was. Nobody could. Reading, deciding, writing, and formatting were welded into a single blob of instructions, and every time I nudged a sentence near the top, something changed at the bottom for reasons I could not name. The fix, when it finally showed up, was one number in one place.

Bottom line: Split the work. Pass structured output, not vibes. One giant prompt that reads, decides, writes, and formats in a single shot fails in ways you can’t debug: when the output is wrong you don’t know which step lied. Chains of small steps with typed handoffs fail loudly at exactly one joint.


When it bites


The pattern

A chain is steps with contracts: step N produces typed output that step N+1 consumes. Not vibes, schema.

  1. Decompose by failure, not by topic. Split where you’d want a different retry policy or a different model. “Extract → decide → act → verify” splits naturally because extraction failures and decision failures need different fixes.
  2. Type every handoff. JSON schema, enum, required fields. I have no standards body to wave at you here, just my own pipelines and the scar tissue: if step 2 accepts free text from step 1, you don’t have a chain, you have a relay race where the baton is a rumor.
  3. Smallest model that clears each step. Extraction is a Haiku job. The judgment call might be Sonnet. Paying flagship rates for regex-grade work is how pilots die in budget review (Ch. 7, Ch. 28).
  4. Retry the step, not the chain. Each step gets its own retry budget and its own fallback. The five steps that worked keep their results.
  5. Log the joints. Every handoff logged with input hash, output, latency. When prod breaks you open the trace and point at ONE step (Ch. 47).

What a chain is not: seven prompts stapled together with “hopefully the context carries it.” If step 4 needs something from step 1, pass it explicitly. Context windows are big, implicit dependencies still get you haunted, and they cha-cha across your prod traces like a 1996 Dancing Baby GIF nobody can close. I have assumed “the context carries it” more than once and been wrong every single time, which is a suspiciously consistent record for a guy who keeps trying it.


One worked example

On 9 September 2026 the pipeline that makes this book’s chapter pictures broke at its one handoff, on the morning I was writing the chapter about handoffs. I could not have scheduled that.

Two stages. Stage 1 is an Opus “scene writer” agent: read all 63 chapters, invent one sight gag apiece, emit an array of JSON objects, one per chapter, {file, id, title, scene}. Stage 2 is assets/thumbs/gen.sh, a shell runner that takes an id on the command line, pulls that object out of the array, fills a prompt template, and calls the image model. Here is a real stage 1 object, the one for the chapter you are reading:

{
 "file": "08-Stop_Jamming_One_Prompt.md",
 "id": "08",
 "title": "Stop Jamming the Whole Novel in One Prompt",
 "scene": "Clark works a hydraulic press to cram an entire library, shelves included, into a plastic funnel whose spout is the width of a coffee stirrer. Pages erupt from the seams in a fifteen-foot geyser and the press is glowing red. The one thing that has made it through the spout into the cup below is a receipt."
}

Clean. Now here is what I actually typed into the stage 1 prompt at 10:06 CDT that morning. One field, one sentence, three definitions:

"id": "<first 3 chars of filename, e.g. 00-, 00b, 01->> use the filename prefix before the first dash, e.g. '00', '00b', '01'"

Count them. First three characters. Then examples where two of the three include the dash I had just told it to count. Then a different rule entirely, prefix before the first dash. Nobody made me write that. I did it to myself, in one sentence, in a chapter’s own tooling.

Stage 1 did what a model does with a field defined three ways: it quietly stopped trusting the field. It returned all 63 objects, on time, valid JSON, correct count, every scene genuinely funny. And every value shifted one slot left. file was the chapter title. id was also the chapter title. title was the chapter’s bottom line. Dumping object zero at 10:09 CDT:

dict_keys(['file', 'id', 'title', 'scene'])
{'file': "Don't Read This in Order", 'id': "Don't Read This in Order",
 'title': 'This is an onboarding packet, not a course. You got promoted', ...}

Stage 2 has no opinion about any of that. gen.sh looks up the scene like this:

sc=[s for s in json.load(open(".../scenes.json")) if s["id"]==sid][0]

Ask for id 05, match nothing, index an empty list, and get the loudest, least informative sentence in Python:

Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
IndexError: list index out of range
FAIL 05

10:14:52 CDT, line 3. That is the entire crash. Look at what it does not say. It does not say the scene writer shifted your fields. It says a list was empty. But it said it at exactly one joint, in one file, on one line, seven seconds after it started, and that is the whole argument for chains: the error was useless and the location was perfect.

The fix was not a better prompt. I stopped asking stage 1 for the id at all. There was already an authoritative list of chapters in file order, chapters.tsv, sitting right there. So re-join on it by position and assert before the next stage gets to run:

rows=[l.rstrip('\n').split('\t') for l in open(f"{S}/chapters.tsv")]
d=json.load(open(f"{S}/scenes.json"))
assert len(rows)==len(d)==63
for (f,t,b),s in zip(rows,d):
    assert s['id'].strip()==t.strip() or s['file'].strip()==t.strip(), (s['id'],t)
    out.append({"file":f,"id":f.split('-')[0],"title":t,"scene":s['scene']})

Three lines of contract. The counts have to match. Whatever the model wrote has to line up with the chapter I think it is. And the id gets computed by me, from the filename, instead of being requested from a model I could not describe an id to. Sixty-three thumbnails later, that joint has not moved. The corrected output is manuscript/assets/thumbs/scenes.json.

Now the bigger chain, the one that wrote the sentence you are reading. Five stages per chapter: research, fold, unslop, cite-check, patch. Haiku hunts down the receipts, Opus folds them into the prose, Sonnet strips the em dashes and the banned words, Haiku tries to refute every claim I just made, Opus patches or flags whatever could not be defended. Different model per stage because the jobs are different sizes. Separate retry per stage because a blown cite-check should not send anybody back to re-research anything.

The batons are typed. Verbatim from the workflow script:

const RESEARCH = { type:'object', required:['items'], properties:{ items:{ type:'array', items:{ type:'object', required:['receipt','status','finding','outlet','date','url','numbers','funny_angle'], properties:{
  receipt:{type:'string'}, status:{type:'string', enum:['sourced','jeremy_answered_verified','jeremy_answered_unverifiable','no_source']}, finding:{type:'string'}, outlet:{type:'string'}, date:{type:'string'}, url:{type:'string'}, numbers:{type:'string'}, funny_angle:{type:'string'} } } } } }
const FOLD = { type:'object', required:['path','words','replaced','left_open'], properties:{ path:{type:'string'}, words:{type:'number'}, replaced:{type:'number'}, left_open:{type:'array', items:{type:'string'}} } }

That status enum has four values and three of them mean “do not print this yet.” The enum is the rail: the fold stage cannot receive a vague maybe from the research stage, because the schema gives it no way to say one. And FOLD hands back left_open, the receipts it could not close, so the next run knows what is still missing instead of me finding out in a printed proof.

That is the difference in one paragraph. The thumbnail chain had funny scenes and no contract, and it died on line 3 of a shell script. The chapter chain has a contract at every joint, and when a stage lies the next stage refuses the baton.


The quiet failure

The loud failure is the mega-prompt that obviously doesn’t work. The quiet failure is the chain that works and rots:

Untyped handoffs. Steps passing prose to each other, each adding a little drift, until step 5 is acting on step 1’s rumor of a rumor. It is five agents playing telephone, and the last one is the only one allowed to talk to your customer. It passes every demo because the drift is small per step. In prod the drift compounds and nobody can say where the truth got lost, because no joint ever checked.

Second quiet failure: chains with no step-level evals. You eval the final output (good, Part VII) but never the joints, so a degrading step hides inside a passing chain until the margin runs out. Eval the handoffs, not just the finale. The scene writer passed every check I had, count and shape and comedy, while the values sat one slot to the left, and I took that as proof I was fine.


Do / don’t

Do

Don’t


Where this sits in the book

Part I opens with the foundational move: decomposition with contracts. Everything after (personas in Ch. 9, routing in Ch. 16, parallel fan-out in Ch. 25, the merge in Ch. 27) assumes work arrives in typed pieces. Next: how to lie to your chatbot, the persona-and-pressure chapter nobody admits works.


Sources and receipts

Prompt-chaining pattern in Jeremy’s mouth: sentences original.

Verified