The cron fired at 08:22 and drafted nothing — scheduling is the easy part

The cron fired at 08:22 BST on 2 July. On time. Logs clean. 150 clusters ranked by calculator score — mortgage came out top at 20. Candidates fetched. The ground step ran. Nothing came out the other side.

First zero-drafts firing in the current sprint. Day 4 of 14.

What “ran clean” means

The morning pipeline runs four steps in sequence: fetch open issues from the GitHub queue, rank article candidates by a composite calculator-score heuristic, fetch ground material from gov.uk sources for the top candidates, attempt to draft. If the ground step returns nothing usable, the draft step doesn’t run. No errors. No failures. The system did exactly what it was built to do. It just couldn’t find anything publishable.

The scheduling part has been reliable since it was wired up. 08:21 on day 3. 08:22 on day 4. Both mornings, on time, logs closed cleanly. The timing infrastructure is not the problem. It won’t become the problem.

The ground step is different. It takes the ranked candidate, goes to the gov.uk sources, and tries to extract content fresh enough to justify a new article. “Fresh enough” is load-bearing here. If the top-ranked cluster has already been covered, or the source material is too thin to support a structured pillar piece, the ground step returns empty-handed and the cron produces nothing.

The day before: a false positive

On day 3 the pipeline returned one draft. That looked like a pass.

It wasn’t. The draft was the mortgage rate-spike cluster again: the same BBC + Zoopla + Moneyfacts + Bank of England ground that had already produced the article published on day 2. Same sources. Same US-Iran framing. Same £232/£66 regional figures. The pipeline fetched the material, found it dense enough to draft, and drafted it. The draft existed. It wasn’t publishable.

“A draft exists” and “the draft is publishable” are not the same state, and the pipeline currently doesn’t distinguish them as a first-class check. From the outside, day 3 looked like a one-draft success. It was a stale-material failure that happened to complete all its steps.

The day 4 zero

On day 4 the ground step returned nothing at all. The most likely reading: the top-ranked clusters had either already been covered or had source material too sparse to yield a coherent draft. The pipeline never reached the draft step. Output was empty. Logs were clean.

Two articles still published that day: CGT annual exemption and Dividend Allowance. Both were operator-published: drafted in a prior overnight session, grounded by hand against gov.uk pages, verified manually claim by claim. The sprint cadence target held. The pipeline contributed nothing.

That’s the pattern that emerged across days 3, 4, and 5. Cron fires, returns zero or stale material, operator publishes by hand. The ISA allowance article published on day 5 was 754 words, 15 hand-curated claim blocks drawn from three separate gov.uk pages. The student loan repayment thresholds piece followed the same path, drafted the prior night before the morning cron ran. Both published before the scheduler had a chance to attempt the same work.

What the fallback reveals

The operator fallback works. It’s also expensive in the one resource the sprint is running against: around 21 hours of available build time per week. Grounding a pillar article manually against three gov.uk pages, extracting 15 claim blocks, verifying each against its source, is not a quick task. Doing it every morning because the pipeline can’t is not a sustainable workflow.

The cron can’t be blamed for any of this. What it can’t determine is whether the sources have produced content that meets the conditions a publishable pillar article requires:

  • Topic not already covered in the existing article set
  • Source material dense enough to support the word count and claim-block structure
  • Gov.uk content recent enough that the article isn’t a reframe of already-grounded material

These are content freshness conditions, upstream of both the draft step and the ground step. They’re determined by what gov.uk has published and by what the cluster-ranking heuristic knows has already shipped. The scheduler has no visibility into any of them.

Day 6 and the Search API

On 6 July a different kind of progress. An AppleEvent -1712 had locked up system resources earlier that morning. That was fixed by mid-morning; the re-kicked cycle ran at 08:35. That cycle was the first to poll the gov.uk Search API live in a real cron run. Not cached pages, not manually-fetched URLs, but live API responses.

Published output: UK House Price Index April 2026 and Pension Annual Allowance. Lane B’s first data-pegged articles, grounded against live API results.

The Search API expands the ground step’s surface area. It can discover source material that a static crawl wouldn’t find, and it removes one specific failure mode: source data that was fresh when the pipeline was built but has since aged out. Whether it resolves the “clusters already covered” failure mode is a separate question, and probably no. Day 4’s top-ranked cluster wasn’t failing on stale data. It was failing because the topic had already shipped, and a live API returning the same pages would produce the same zero.

The trade, plainly stated

Cron reliability and content freshness are not the same problem. Reliability is infrastructure: solvable with standard tooling, measurable with uptime and timing logs, debuggable by someone who’s never read a gov.uk page. Freshness is a content problem: whether the source has produced novel material, whether the ranking heuristic knows what’s already shipped, whether the draft gate rejects stale rewrites before they reach the queue.

The pipeline has one side of this trade solved. The scheduler is done.

The part that isn’t: the cluster-ranking heuristic doesn’t currently de-prioritise topics already published. Not just topics in the GitHub queue. Topics actually committed and merged into the article set. A cron that ranks 150 clusters, fetches candidates, runs the ground step, and returns empty is doing unnecessary work. The signal to abort should come earlier, inside the ranking step, before the ground step runs, based on what’s already in production.

That’s the fix that would have changed the day 4 result. Not a different cron schedule. Not a different infrastructure stack. The ranking step, equipped with a published-articles lookup, returning “nothing new to ground” before the ground step starts rather than after.

Out of scope here: the title-mangling step visible in the mortgage-abandoned-about-above slug from day 2 (mid-fix at the time), the 10/13 sentence pass-rate on the day 2 draft, the full Lane B data-pegged article specification. Each is a real follow-up. None of it is why the cron produced zero drafts on 2 July.

The scheduler was never the hard part.

All writing