Back to Blog
Automation Cloud Architecture Risk Management DevOps AI Engineering

Nothing Publishes: Designing the Boundary Between Staged and Sent

By CloudGeeks Team | 18 August 2026 | 8 min read

Most automation has a dry-run flag. Almost none of it can prove the flag worked.

The usual shape is a boolean threaded through the code, checked at each point where something would be sent, and trusted. It is fine right up until someone adds a new publisher and forgets the check, or refactors a function that used to receive the flag and now does not. The failure is silent in the worst possible direction: the run reports a successful dry run and something has gone out.

Here is a different arrangement, from a content pipeline that reaches Planable, YouTube and a podcast host — three places where a mistake is public, immediate and not fully recoverable.

Three rules

1. Every outward call becomes a record on disk.

Nothing calls an external API inline. Each intended request is written into a manifest as data — target, endpoint, payload, and a sent boolean. Publishing is not something the pipeline does; it is something the pipeline describes.

2. A gate proves nothing was sent, from the records.

def publish_boundary_gate(staged, *, dry_run: bool):
    """In a dry run nothing may have been sent.
       Prove it from the staged records, not from intent."""
    sent = [f"{s['target']} {s.get('endpoint','')}"
            for s in staged if s.get("sent")]
    return Verdict("publish-boundary", not sent,
                   f"{len(staged)} requests staged, none sent" if not sent
                   else f"{len(sent)} request(s) were SENT during a dry run")

The distinction in that docstring is the whole design. The gate does not ask was the dry-run flag set? — that is asking the system to confirm its own intention. It walks the actual record of every outward call and checks that each one is still marked unsent. If a publisher ignored the flag, the evidence of that is in the manifest, and the gate reads the evidence rather than the promise.

3. The live switch is refused.

Refusing --live: sending to Planable/YouTube/the podcast host is
outward-facing and irreversible. Review runs/<id>/manifest.json,
then publish deliberately.

The flag exists and is parsed. It does not work. Passing it returns exit code 2 and an explanation.

That looks like a joke until you consider what it prevents. --live is the flag someone adds to a cron entry at 2am to save a step. It is the flag that ends up in a shell history and gets recalled with an up-arrow next to a different argument. Making it a hard refusal means the only path to publication runs through a human reading a specific file.

Why “irreversible” deserves its own category

Automation risk is usually discussed as one thing. It is really two, and they need different engineering.

Reversible actions — writing a file, updating a database row, rebuilding a site. If these go wrong you restore, re-run, or revert. The right posture is speed with a rollback path.

Irreversible actions — sending an email, posting to a social channel, publishing a video, taking a payment, deleting anything. A social post that goes out at the wrong time has been seen. You can delete it; you cannot unsend it. Anyone subscribed already has it.

Most automation frameworks treat both the same way, because at the code level they look the same: a function call that returns success. The difference is not visible in the type system, so it has to be made visible in the architecture.

We were reminded of this from the other direction recently. A script in an unrelated repo, written months earlier to bootstrap a Google Sheet, opened with a loop that deleted every tab except the first. That was correct exactly once, on a fresh template copy full of junk tabs. The sheet has since grown to thirty tabs of real operational data. Nothing in the script’s name warned anyone. It sat there, correct-when-written, one run away from destroying months of work — because the blast radius grew while the code stayed still.

Staging plus a proving gate would have made that impossible. The run would have described thirty deletions and stopped.

What this costs, and what it buys

The honest cost is a step. Somebody has to open runs/<id>/manifest.json, read what the pipeline intends to do, and act on it. That is friction, and friction on a daily pipeline is a real tax.

What it buys:

A reviewable artifact. The manifest is the diff of an outward-facing action. You can read exactly what would go where before any of it goes anywhere — which is a much better review surface than reading the code that would generate it.

A safe default. The dangerous path requires a deliberate act. The easy path, the tired path and the automated path all stop at the boundary.

An audit trail that exists by construction. Every run leaves a record of intent whether or not it published, so “what did the pipeline try to do on Tuesday” has an answer.

Freedom to iterate. This is the underrated one. Because the pipeline genuinely cannot publish, you can run it constantly — on every change, against real data, in CI — without a rehearsal environment or a set of test credentials. The safety property is what makes the thing cheap to develop.

Applying it without rebuilding everything

You do not need a full pipeline rewrite to get most of this.

Find your irreversible calls and list them. Most systems have fewer than a dozen: send, post, publish, charge, delete. Everything else is recoverable. That list is the boundary.

Return the request instead of making it. Change those functions to build and return the request object. A thin executor at the top level makes the actual calls. Now there is one place where the outside world is touched.

Record every one before executing. Write the intent to disk first, then execute, then mark it sent. If the process dies mid-run you know exactly what went out.

Verify from the record, not the flag. Whatever your safety condition is, check it against what was written, not against the variable that was supposed to control it.

Make the dangerous flag inconvenient on purpose. A confirmation prompt, a required file path, an explicit refusal. If your automation can do something you would not want done by accident at 2am, the accident path should not be a single character.

The principle underneath all five is small: a system should not be trusted to report on its own behaviour. Ask it for the evidence, and check that instead.


Cloud Geeks designs and runs automation for Australian businesses — including the parts that decide when not to run.

Ready to upgrade your IT and cloud setup?

Let's talk about cloud, infrastructure, or cybersecurity. We help Sydney SMBs cut hosting costs, harden their stack, and stop firefighting.

Bella Vista, Sydney