A skill file is read; a script is run. We wrote a channel retirement in prose, and two scripts spent 33 days still willing to act on it
We shut a paid advertising channel down in July, and for thirty three days after that two of our own scripts were still willing to spend money on it. The retirement was written in exactly one place, the opening lines of the instruction file an agent reads before it goes near that channel: never create, relaunch, or re-enable a campaign.
That sentence is perfectly clear, and it only reaches anyone who opens the file. Nothing opens it on the way to running something.
This is the part worth taking away from the incident, and it is not specific to ads. An instruction file, a skill, a runbook, a policy doc, a system prompt fragment, is a document with a reader. It works by being opened, understood and obeyed, and every one of those three steps is conditional on something happening at the moment of action. A stop is not conditional. If the answer is fixed for every future call, storing it as advice is storing it in the one form that can be silently skipped.
The reader and the runner are not the same consumer
An instruction file has two kinds of consumer and it can only speak to one of them.
The first is a reader: a person or a model that arrives without a plan, opens the file to work out what to do, and takes the prose as input. For that consumer the file is exactly the right tool. Judgment is genuinely required, the file supplies it, and prose is the only format that can carry nuance.
The second is a runner: a script that already knows what it is going to do and does it. It has no step where it consults anything. Our watchdog for campaigns the ad platform has limited is that second kind. When it finds one, it assembles a block of text and hands it to a session that runs without stopping to ask permission. Inside that text it names the retired instruction file, and then spells out the action anyway: pause and rename the old campaign, then create a fresh one with its status set to enabled. That is money leaving an account for a channel we closed.
Note what that script does with the file, because it is the strongest evidence against the prose approach. It does not ignore the file. It names it. Passing a document by reference into a prompt still leaves the model reconciling two texts that disagree, in the same blob, with the concrete instruction stated in full and the retirement stated as a pointer. A gate has nothing to reconcile, the process is gone before the state is read.
The script was written before the file said retired. When the channel closed, we edited the file that describes the work and left the file that dispatches it alone. The second script is milder and has the same shape: an ad metric moves, and it fires an unattended session to go fix and deploy whatever the traffic is landing on, for traffic that no longer exists.

What was actually holding it
Nothing was scheduled. We went through every job definition in the folder launchd loads on this machine, and not one of them points at either script. The only job that touches this channel at all runs a different file, an hourly geographic check. So the thing standing between a retired channel and a live campaign was the absence of a line in a schedule, and one run by hand from a terminal would have cleared it.
| Where the retirement was written | What it says there | What it stopped |
|---|---|---|
| The instruction file's opening lines | Never create, relaunch, or re-enable a campaign | A person or a model that opens the file |
| The same file's one line summary, which loads into every session | Retired 2026-07-23, kept as a historical runbook | Nothing on its own. A description is not a check |
| The watchdog that builds the prompt | Pause and rename the old campaign, then create a new one with status enabled | Nothing, until 2026-08-25 |
| The second script, watching ad metrics | Fire an unattended session to fix and ship the app the traffic is landing on | Nothing, until 2026-08-25 |
| launchd | No job on this machine points at either script | Both, by accident |
The second row is the one that catches people, us included. That summary line loads into every session, so the retirement was in front of far more eyes than the file itself ever was. Breadth of distribution is not enforcement. A widely broadcast description is still a description.
Why the fix does not belong in the file
The obvious remedy is to write the retirement more forcefully, in more files, higher up. That is the remedy that cannot work, and it is worth being precise about why, because the sentence we had was not the problem. It was unambiguous, it was first in the file, it was current. Being correct and being consulted are different properties, and only the second one stops a process.
Three things follow from that, and they are what separate a document from a gate.
Prose fails silently. When a rule in a file does not reach the thing acting, nothing happens: no error, no log line, no exit code, no artifact anywhere that says a rule was skipped. You cannot observe a document failing to be read. We only found this by going and looking, and what we found was that the only thing between us and a live campaign was an accident of scheduling.
Prose has no enumerable scope. Ask what a rule in an instruction file covers and the honest answer is "whoever opens it", which is not a set you can list, audit or test. A guard at the top of a script covers exactly one thing, that script, and the set of scripts is finite and greppable. You trade reach for certainty, and for a stop that is the right trade every time.
Prose points the dependency the wrong way. In the file version, the rule sits somewhere and hopes; the code runs regardless. A gate inverts that: the action cannot proceed without passing through the rule, because the rule is the first thing on the only path the action has. That inversion is the whole of the difference, and no amount of emphasis in a document produces it.
So on 2026-08-25 we put four lines at the top of both files, above everything else they do. Each one now prints a retired line and stops before it reads any state, unless the variable GADS_CHANNEL_REVIVED is set to 1. It stops without reporting a failure, deliberately, because a switched off channel producing no work is a hold rather than a fault, and a failure there would trip the breaker in the wrapper every scheduled job runs through for no reason at all.
That last choice is only available at the code level, and it is a small demonstration of the general point. A gate has exit semantics you get to design, hold here, fail there. A paragraph has none. The env var is the same story from the other side: reviving the channel is now a named switch with an owner and a value, rather than an edit to a document plus a hope that everything downstream notices.
The test we now apply
For any rule going into an instruction file, list the things that can perform the action without opening that file. Scripts, scheduled jobs, prompt builders, anything with a plan of its own. That list is the gate list, and each item on it needs the rule expressed as code sitting on its own path.
Then ask what kind of rule it is. If there is a case where the right move depends on circumstances, prose is correct and a gate would be wrong, that is judgment, and judgment is what documents are for. If the answer is fixed for every future call, as a retirement is, there is no judgment left to exercise and no reason for it to live anywhere it can be missed. The retirement now lives in the two files that could act on it, not only in the one that describes them.
Compound Labs designs, builds and runs AI-native products end to end, and this is the operations layer underneath them.
<caption>We switched our ads off in one file, but two scripts could still switch them back on, so both now refuse to run.</caption>
---
One shipped product, taken apart, once a month. What it does, what it cost to build, what the pipeline behind it looks like, and what the numbers did, read off the repository and the live site, not written from memory. Join the list.