Essays / Every Safety Rail Worked.

Essay · 1 August 2026 · Adrian Verdan

Every Safety Rail Worked. Nothing Happened.

This report first appeared on Medium. The full original text is below. The same version on Medium →

My marketing bot publishes on its own, behind a review gate with veto power and every guardrail a careful engineer would want. In late July, those guardrails quietly multiplied into zero output — and the dashboard stayed green the whole time.

On July 27, 2026, my marketing bot ran its full schedule: four content cycles, inbox sweeps, the nightly maintenance job, all on time. It produced eight drafts. It published none. It did not crash. A crash sends an alert; this sent nothing.

Drafted with AI assistance; however, every number, failure, and opinion in here is mine

I run a one-person software business, so the bot is the entire marketing department. When it goes quiet, no colleague leans over to ask why the feed looks thin. There is a dashboard. The dashboard was green.


The Night Shift That Produced Nothing

I want to be precise here, because "the bot was dead for five days" is the version I told a friend, and it is wrong. The truth is quieter and worse.

July 23: zero posts. The 24th: three. The 25th: two. The 26th: one, the last of them going out at 16:01. Then two more zero days, the 27th and the 28th. That is not a flatline. It is a degradation window with three zero days and, in between, counts small enough to pass for ordinary variance. A flatline gets investigated. A slow fade gets explained away.

I would love to tell you which switch flipped on the 23rd. There wasn't one. The supply of topics had been rotting for weeks, and that week's plan was simply the first one bad enough to lose whole days to it — with two separate mechanisms, which I will get to, waiting further down to turn a bad week into a locked system.

The machine was busy the entire time. On July 22 it wrote ten drafts; on July 26, eleven. The writing session drafted, the gate reviewed, the scheduler scheduled. All of that effort arrived at the publish step and evaporated.

Bar chart of drafts vs. published posts per day, July 18-31, 2026, with zero published on July 23, 27 and 28.

Tap to enlarge.

Drafts generated versus posts published per day, July 18–31, 2026. Three zero days (July 23, 27, 28) inside a slow fade that never looked like an outage. Data: author's production database, July 2026

Eight Drafts, Zero Posts

Two words on the machinery, because it earns its keep in this story. Every draft passes a mechanical style linter — banned phrases, length, formatting — and then a review gate: a second model session with veto power, which reads each draft against the recent feed. The writing session can never approve its own output; that rule is a database constraint, not a policy document.

July 27 is the day that tells the story. Eight drafts entered the pipeline. Four failed the linter. Four died further down. Zero reached a feed. And the gate's written verdicts from that stretch read less like rejections and more like a diagnosis. On a July 27 draft: "'Kein Vorhang, keine Show' reuses the same authenticity-over-performance framing … same metaphor schema … only the surface wording differs" (the quoted phrase translates to "No curtain, no show"). One day later, on yet another: "Severe structural/thematic repetition: the closing formula 'Wir bauen lieber das erste/zweite' … has already run at least five times in recent posts … only the noun changed."

That closing formula translates to "We'd rather build the first/second one." My bot had developed a catchphrase. It had been ending posts with the same sentence for days, swapping a single noun, and the gate had finally seen enough.

My incident notes from July 28 contain the sentence "the review gate is too strict." I wrote that, and it is exactly backwards. The gate was the only component in the pipeline doing its job.

The defect sat upstream. The weekly topic planner, the thing that decides what the bot writes about, was supposed to draw on 72 curated topic seeds: 37 German, 35 English, sitting ready in two files. It received none of them. It also received no record of what it had already planned. So every week it improvised from a blank page, saved its improvisations into the topic pool, and drew from that same pool the next week — a feedback loop that had quietly stabilized on about twenty entries' worth of the same aphorisms. The post-mortem measurements are almost funny: the pool held two pairs of entries with literally identical titles, and a third pair sat at 0.634 trigram similarity against the 0.6 duplicate threshold.

The rotation then made it worse. On July 24 a schema migration added a last-used timestamp to every topic, defaulting to NULL, on the logic that NULL means never used and should sort first. For new topics that logic holds. For the existing pool it did not: NULL just meant nobody had written the history down — and the two most-recycled topics, used 18 and 14 times, both carried NULL. The rotation placed the two most worn-out topics in the pool at the very front of the queue. The backfill migration I wrote afterwards carries a one-line docstring: "The gate was never the problem. The rotation lied to it."

If a quality gate rejects everything, audit the supply line first.

Every Rail, Taken Alone, Was Right

The obvious response: you built too many gates, rip some out.

No. Walk the stack with me. The review gate exists because an autonomous system publishing repetitive slop under a name I am responsible for is a real cost, and the verdicts above show it catching exactly that. The no-self-approval constraint: I do not trust a model to grade its own homework. Per-channel daily caps (ten posts on one platform, six on the other) turn a scheduling bug into an annoyance instead of a spam incident. Before the bot may comment on a stranger's post, relevance must clear 0.89 — without that, you are running a reply-guy operation with extra steps. The image budget caps spend on a metered API. And the circuit breaker exists because a platform outage should not be met with my retries.

Any one of these survives a design review easily. None of them is speculative; each was added after a specific incident or a specific priced risk. "Fewer gates" is the advice of someone who has not yet paid for the incident a gate prevents. Most essays about autonomous AI are written by people who have never given a system publish rights, and it shows: from a safe distance, guardrails look like paranoia. Up close they look like scar tissue.

The failure lived in none of the rails. It lived in the one place no single rail can see: their product.

The Multiplication Nobody Computed

The arithmetic I never did before July goes like this. Nominal ceiling across the two channels: sixteen posts a day. That sounds roomy. Now multiply through the stack. Only drafts that pass the linter count. Of those, only drafts the gate accepts — and the gate's acceptance rate is not a constant. It moves with whatever the planner feeds it; feed it five rephrasings of one aphorism and its acceptance rate is, correctly, zero. Keep multiplying: in that final July week every post needed an image to publish, and images are rendered while a draft is being prepared — not at the moment it publishes. The renders draw from a shared daily budget of eight, and every attempt counts against it, including renders for drafts that die downstream: they cost money whether or not the post survives. Eight doomed drafts could eat a whole day's images without publishing a word. They did.

Every factor, taken alone, was generous. The product of the factors, in the last week of July, was zero. The dashboard displayed the factors. Nobody displayed the product.

Bar chart: platform caps allowed 16 posts on July 27, 2026; 8 drafts were produced; 0 were published.

Tap to enlarge.

What the platform caps allowed versus what the pipeline delivered on July 27: every stage individually roomy, the composed output zero. Data: author's production database, July 2026

Strictly, these were two different failures. The rotting topic supply drove one factor — gate acceptance — to zero on the 27th. Two self-tightening mechanisms held other factors at zero on the 28th. What they share is the shape: in a multiplication, any factor at zero is the whole story, and no per-factor dashboard will tell you which one it was.

This happens by default, and it will happen to you, because every individual limit has an author and a reason, while the composition has neither. Joint capacity is a number someone has to sit down and compute, worst case rather than average case. In my system, nobody was that someone.

The Ratchet Problem

Those two mechanisms deserve their own chapter, because they share a property that turned a bad week into a locked system: both could tighten, and nothing could loosen them.

First, the circuit breaker. Three consecutive errors on a channel flip it to paused. That part is sensible. Except errors that named no channel counted against every channel; the counter reset only on a successful publish, which is exactly what a paused channel cannot produce; and no code path ever set a channel back to active. I had built a ratchet and believed it was a valve. On July 28 both channels sat at two errors of three; an image-generation timeout supplied the third, and both went dark — permanently, as written. The breaker did exactly what I designed. That was the problem.

Second, the image floor. Policy: at least 30 percent of recent posts should carry an image, and when the ratio dips below 0.30, every new post must include one. By late July the ratio stood at 0.233, seven of the last thirty posts, so every new post required an image. The daily image budget was exhausted at eight of eight — see above for who ate it — so no post could go out. No posts meant the ratio could never rise. A deadlock, each link of which I would have approved in review.

A ratchet, in this sense, is any rule that can only remove capacity. The breaker could only pause; the floor could only demand. Both were reacting to a condition that only publishing could repair, and both prevented publishing.

A limit the producer cannot influence must never be a hard limit.

What I Changed (and What I Refused to Change)

The streak ended the way these things end in a one-person company: not with an alert, but with a human — me — finally asking the system, out loud, why the feed looked thin. The repair took one long day: July 28, thirteen commits between 14:10 and 20:47.

The planner now receives the 72 seeds and the list of everything already planned; it curates instead of improvising. The rotation got its history backfilled, so "never used" finally means never used; the pool, which had been cycling the same 21 entries, was reseeded to 81 fresh topics, with 8 worn-out ones retired. The circuit breaker got a recovery path: after six hours a paused channel comes back with one strike left, and a single further attributable failure re-pauses it. An independent audit of that same fix, by a different AI model, caught that my recovery code ran after the point where an all-paused system had already given up — which would have made the fix unreachable in exactly the case that needed it. Scar tissue on the scar tissue. The image deadlock got a valve, deliberately narrow: a post may go out without an image only when the daily image budget is spent, the one condition the post itself can do nothing about. Not a general "skip images on failure" switch. One named condition, one escape, one audit-log line per use.

What I refused to change: the review gate's standards, by even a point, and both founder-set image numbers. The rails' standards stand; what got rebuilt was the plumbing of two rails that were never what I thought I had built. 1,572 tests pass, and, more to the point, eight new negative controls go red when I reintroduce each defect on a branch. The first post after the repair went out on July 29 at 07:48:20.

The bill, honestly itemized: about a week of marketing output degraded toward zero, for a company whose entire marketing is this bot, and zero euros of lost revenue, because the products it promotes had close to nothing to lose. The expensive part is a piece of knowledge I cannot return: my green dashboard will watch this system starve and say nothing.

Three changes to how I build now. Every filter that discards work silently gets a counter, because a rejection you cannot count is a rejection you cannot notice — that one is already shipped. Every limit gets asked one question in review: what loosens this again? The breaker's answer is now a six-hour half-open; the floor's answer is a named valve; a limit with no answer is a ratchet, and it does not merge. And the alarm I actually needed — drafts up, published at zero, for days — is embarrassingly simple and, as I write this, still on my list. I told you the dashboard stayed green. I have not yet taught it to blush.

The longer version of that checklist ended up in The AI-First Operating Playbook, filed next to the other lessons I paid full price for. The short version is the paragraph above, and it is free.

Read on

All reports at a glance.

Each report is built on a specific failure, with the date, the log, and the change that followed: to the essay index →