---
title: "451 Tokens Spent to Learn a Model Call Was Told to Wait"
canonical: https://dxdev.com/blog/blog-pipeline-end-to-end/
datePublished: 2026-09-26
---
At 12:19 AM the cron-fail triage agent read a failed dxdev run and answered in 9 seconds, 451 tokens: `spend_gate deferred the dxdev model call (band orange, discretionary), not a code bug.` The job had not crashed. It had been told, correctly, to wait, and the wrapper around it had filed that as a failure.

## Five stages that failed in combination

The blog pipeline had five stages that each worked in isolation and failed together: weekly mining of work logs for post candidates, description generation, lesson extraction, spend gating, and a dash linter for the finished drafts. We tried fixing them one a week. Each fix just moved the bottleneck to the next stage down, so we fixed all five in one pass.

After that pass, 27 posts went live, and the nightly job drained the rest of the backlog without anyone touching it. We had spent months treating the backlog as a writing problem. It was a plumbing problem. Drafts existed. Nothing between the draft and the publish step was reliable enough to run unattended.

## A deferral filed as a crash

Mining and publishing are the ends, and we built them first because they are the visible parts: a thing goes in, a post comes out. The middle is where a model call has to be allowed to happen, where its output has to be judged, and where a failure has to be told apart from a decision.

The alert above is that last part. Our [spend gate sorts model calls into bands](/blog/burn-to-100-before-reset/). At band orange, anything marked discretionary is deferred, and a blog call is discretionary, so at orange it waits. The job wrapper saw a call that did not complete and filed it under `remedy/cron-fail`, the same bucket a real crash lands in. A [deliberate deferral](/blog/parked-is-not-waiting-30-minute-data-model-split/) was being reported as a failure.

The triage agent got it right, but only because it read the gate's reason string. Anything that just counted failed runs would have called that night broken. The cost of the mislabel is small but real: every deferral generates a triage call, and every triage call is a model call. We were spending money to be told that we had decided not to spend money.

The fix on the alerting side is small. When the gate defers, record the reason and the band, and have the wrapper exit with a distinct status so the run never enters the failure channel at all.

## A lessons stage that always had something to say

Our first version of lesson extraction always produced output, because we had written the prompt to require it. On nights with nothing to say, it said something anyway. That filler is what makes a backlog of posts read as generated, and we only saw the cost once the backlog was large enough to read in bulk.

So the stage got a legal way to say nothing. At 3:40 AM the `learning_nightly` job ran twice, 13 seconds apart. Both times it returned a single line, `VERDICT: SKIP`, at 78 and 71 tokens. Nothing was wrong. Both times the stage looked at the day's material and decided there was no lesson worth keeping. Once it could do that, the rest of the pipeline could trust what it did emit.

The gate needed the same thing: a legal way to say "not now" that downstream stages read as a state.

## Why nobody watched the middle

The ends of a pipeline get attention because a human touches them. We read the mined candidates. We read the published post. Everything in between is machinery that nobody watches when it works, so nobody notices it is held together by assumptions: that the budget is always green, that every stage always has something to say, that a non-zero exit always means a bug.

Each of those assumptions was false at least once, and each one had the same signature. A stage did the correct thing, and the layer around it reported the correct thing as a fault. Once the fault reports were trustworthy, the backlog drained on its own. Nothing about the mining or publishing changed for that to happen.

If your automated job has a human at the start and a human at the end, start by reading what the middle reports when it behaves correctly.
