There's a step in my content pipeline that I used to think was boring.
The scheduler.
Draft approved, schedule set, post goes live. What's there to explain?
Turns out, quite a bit.
Once I started actually reading the code that runs it, I found timezone bugs, a visibility incident that leaked members-only content to the open web, and a homemade safety mechanism bolted onto a CMS that definitely never asked for one.
This is deep dive #4 in the series on the AI system behind this site. If you haven't read that hub piece yet, the short version is: approved drafts on this site go from a file on my machine to a live Ghost post with no browser open and no human clicking publish. This piece is about the part of the system that actually does that final push.

What the scheduler actually does
If you want the broader picture of how the full pipeline is built, that's covered separately — but here's what happens when a draft gets approved here.
It lands as a plain text file on my machine. At the top of that file is a block called YAML frontmatter — metadata about the content rather than the content itself: the title, the publish date, the type of post, which series it belongs to, whether it's members-only. Below that block is the actual article, written in Markdown.
A cron script — a timer that runs automatically in the background — picks that file up, strips out all the internal working notes that have no business being on a live post, converts the Markdown to Ghost's native format, and pushes it to the site via the Ghost Admin API using a short-lived access token it generates itself.
No browser. No logged-in session. Just a script, a token, and an API call.
That's the clean version. Getting there had some rough edges.
The timezone bug that shipped wrong dates
Here's one that I suspect a lot of people building their own automation have hit without realizing it.
In April, evening posts started showing up with tomorrow's date. Not every post — just ones scheduled after around 5 PM. The cause was exactly what you'd guess once you see it: some parts of the code were reading the local wall-clock time, some were reading UTC, and after 5 PM Pacific they disagreed by enough to flip the date.
The fix was to stop letting different parts of the system have different opinions about what time it is. There's now a single file — date-utils.js — that owns the timezone for the entire project. Every date calculation runs through one function, which figures out the correct time offset for the target date, not just today. That matters because the offset between your local time and UTC changes when daylight saving time kicks in. No hardcoded values. It gets calculated fresh each time.
The symptom feels like a scheduling problem. The cause is a data consistency problem. Those require completely different fixes — which is why it took me a minute to figure out.
The visibility incident
This one was more consequential.
For a stretch of time, the publisher script never explicitly told Ghost what the visibility setting should be when pushing a new post. It just... didn't include that field. Ghost has a default visibility — public — and it applied that default to everything, including content that was supposed to be members-only.
Eight posts that should have been behind the membership wall ended up on the open web.
The fix had two parts. First, content tagged as AEGIS now hard-defaults to members-only visibility in the publisher code itself — it doesn't rely on Ghost's default, and it doesn't rely on whoever is building the file to remember to set it. Second, there's an audit script that can be run to catch any strays — posts that have the AEGIS tag but somehow ended up public anyway.
A missing field is a production bug when the downstream system's default disagrees with what you intended. That gap is where a lot of automation bugs live, in my experience.
How it avoids publishing the same post twice
This one is the part I find most interesting from a systems perspective, even though it's also the part most readers will reasonably not care about.
The problem: if the script sends a post to Ghost, Ghost accepts it, and then the script crashes before it can clean up — what happens on the next run? Without any protection, it would try to publish the same post again. Ghost would create a duplicate. Subscribers would get two emails.
The solution borrows a pattern called a two-phase commit. Before the script sends anything to Ghost, it writes an "in-flight" marker tied to a fingerprint of that specific file. If the process dies between the API call and the cleanup, the next run sees that marker and investigates instead of just re-publishing. On a successful publish, the marker promotes to "published" and the file gets moved out of the queue.
This is a pattern from distributed systems engineering. It's a lot of care for what looks from the outside like "the publish button."
I didn't design this from scratch. I worked through it with Claude during a coding session — if you're curious about how I actually use Claude Code day to day, that's worth a read — and I'll be honest: I wouldn't have thought to build it this way on my own. I would have probably just hoped the script didn't crash at the wrong moment.
Routing and revisions
Two more pieces worth knowing about, because they come up in practice.
Remember that YAML frontmatter block I mentioned earlier? One of the fields in it is type. That value maps to Ghost tags and site routes automatically. A post goes to the main feed. A dispatch or lexicon-news goes to the members-only /aegis/ terminal. A cwc goes to the conversations-with-code section. The publisher reads the type and routes accordingly — it's not a manual tagging step, it's derived from the file itself.
Revisions work similarly. If a file includes a revises_slug: field — the URL handle of the post it's meant to update — the publisher does an update to the existing live post instead of creating a new one. The body, title, and excerpt update. The URL stays the same. Subscribers don't get re-emailed for a correction.
That last part matters more than it sounds. If you're running a newsletter and you find a typo after sending, you want to fix the post without triggering another send. The revises_slug field makes that possible without any manual intervention.
Why I'm writing about this
When I started building this pipeline, I was not thinking about two-phase commits or daylight-saving-safe time calculations. I was thinking about not having to manually copy-paste drafts into Ghost. The extra care came later, mostly because the simpler version broke in ways I didn't anticipate.
That's usually how this goes. You build the thing that does the job, it breaks in an interesting way, and the fix teaches you something about the problem you were actually solving.
The visibility bug taught me that automation systems have opinions about defaults, and you need to know what those opinions are. The timezone bug taught me that "time" is not a simple value when multiple systems are involved. The two-phase commit taught me that "did it work" is a more complicated question than it looks when you're crossing a network boundary.
None of this required a computer science degree — building something without a CS background is genuinely on the table now. It required breaking things and reading error messages.
If you're building your own publishing automation — whether that's a Ghost pipeline, a Substack integration, a WordPress cron job, whatever — the specific code here isn't the point. The patterns are. Centralize your time handling. Explicitly set fields you care about instead of relying on defaults. Build some kind of protection against duplicate publishes before you need it, not after.
And if you're just curious what's running under the hood of this site, now you know a bit more of it. The next deep dive goes into the distributor — the piece that handles where content goes after it's live.