Anyone can generate a blog post. Making it publishable is the work.
The hard part of an AI writer is not generation — it's the fact ledger, voice scanner, fresh-context critique, and locks that make its prose publishable.
The most useful thing our blog-writing agent has done so far was refuse to write something. Asked in Slack for a post about how great the engineer who built it is, it declined: every claim in a post like that would have to be invented, so it offered to write something grounded instead.
That refusal is a preview of the whole design. Generating a thousand words of plausible prose has been a solved problem for a while. Getting a thousand words a company will put its name on, that a reviewer approves, that won't quietly assert a made-up number, is where all the engineering went.
Blog Bot came out of our Applied AI team; I'm an engineer on this team and I noticed that teammates across the org are always doing great work but either don't think of themselves as writers or are too busy on their next ship to share their technical learnings on their latest one.
The result is that we have a ton of great stories locked away in the heads of people who work here, but we weren't sharing them at the cadence we wanted to.
We're hiring on the Applied AI team, and this project is one of many that our team builds and maintains to support the entire company.

Generation was never the bottleneck
The shape of the tool is simple: post an idea in a Slack thread, get back a drafted post staged in the CMS for review. Underneath, most of the work has nothing to do with writing sentences.
An agent that writes about real systems needs to read real systems. Blog Bot pulls source material from Slack threads and uploaded attachments, GitHub repos and issues, Granola meeting transcripts, Notion pages, YouTube transcripts, and previously published WorkOS posts. Search results don't count: a snippet is a lead, and the primary page gets fetched before anything from it can be cited.
Everything after that is a series of mechanisms built around one assumption. The model will drift, and the system should catch it rather than trust it.
Claims carry receipts or they don't ship
The core primitive is a fact ledger. Before drafting, the agent extracts literal claims from its sources and registers them, and every specific claim in the body has to cite one of those registered ids. A number, a version, a feature name, a quote: each one points back to something a source actually said.

Citations that only exist in the prose would be decoration, so a checker validates every one in the body against the ledger and reports any id that doesn't resolve. It runs the check in the other direction too, flagging sentences that make specific claims while carrying no citation at all. A draft asserting a latency number with no matching ledger entry behind it gets flagged before a human ever reads it. A confident, uncited sentence is the exact shape of a hallucination, and it's cheap to detect mechanically.
The practical effect is a change in failure mode. An ungrounded draft doesn't come back polished and wrong. It comes back shorter, or with a question attached. Thin sources produce a thin post, which is annoying and honest, and those are the two properties you want when the alternative is invented detail.
The voice check is a scanner
Style guidance in a prompt is a suggestion the model is free to forget forty paragraphs later. The specific tells are predictable enough to enumerate, so they're enumerated: contrastive setups, fake-surprise openers, and the throat-clearing transitions that announce a point instead of making it are all banned outright.
Enforcement is the part that matters. A scanner checks the draft against those patterns deterministically instead of asking the model whether it complied. Self-assessment is worthless here, because the same weights that produced the throat-clearing sentence will happily rate it as clean.
Deterministic scanning has a ceiling. It catches phrases. It can't catch the sag of three consecutive paragraphs with identical rhythm, or hedging that technically commits to nothing. So after the scanner passes, a separate refiner pass goes looking for the tells a regex can't see: rhythm, hedging, structural tics.
The author is the worst reviewer of its own draft
Then the draft goes to a different reviewer entirely. A separate model with fresh context critiques it, seeing only the saved draft, the fact ledger, and the voice rules, and none of the conversation that produced any of it.
Withholding the conversation is deliberate. The context that produced a draft also produces attachment to it; a reviewer that watched the reasoning will accept a weak paragraph because it remembers why the paragraph exists. A reviewer that sees only the artifact judges only the artifact, the way a reader will.
That critique is advisory and never blocks anything. It's a second opinion to weigh, and treating it as a veto would trade one model's judgment for another's.
Knowing when not to write
Sometimes a human arrives with prose that's already finished, like an incident report or a pre-written announcement. The failure mode there is an agent that helpfully improves it.
For that case the agent stops being a writer. The text is imported word for word without passing through the model at all, then locked with a SHA-256 hash. A locked draft refuses body rewrites outright, and corrections apply only as exact-match string replacements. No voice pass, no tightened headings.

An agent with strong opinions about prose quality becomes a liability when the prose is someone else's incident report. The lock makes deference structural rather than a matter of the model's discretion.
Two writers, one document
The detail that took the most real engineering is the least glamorous: humans edit these drafts live in a web editor while the agent is working. Two writers, one document, no coordination.
Naive agent design loses that race constantly. The agent reads a body, thinks for ninety seconds, writes back what it remembers, and silently erases the paragraph a human fixed in the meantime. So body saves require the version the edit was based on, and a save built on a stale version is rejected rather than applied. On rejection the agent re-reads the current document, re-applies its change on top of the human's, and saves again.
Nobody notices this working, which is the goal. The alternative is an agent that occasionally eats your edits, and an agent you can't leave alone with a document isn't saving anyone time.
Getting the byline right
Of the remaining checks, the one with real stakes is authorship. Bylines resolve to a canonical CMS author record, and when a person can't be verified the agent asks a human instead of guessing. A plausible guess does real damage here, because the failure mode is publishing under the wrong person's name.
The rest is mundane and load-bearing. Links get fetched over HTTP, and broken ones block publish. The draft gets scanned for competitor mentions. Finished posts land in the CMS as drafts, never as published items, so everything up to the irreversible step is automated and the irreversible step waits.
Restraint as a default
One last piece of the design has nothing to do with writing. Blog Bot acts only when someone @-mentions it, and other messages in its threads reach it later as context rather than triggering a run. Its threads are built so people can chat in them freely without summoning it, which means a team can argue about an angle in front of the bot without the bot volunteering an opinion.
That came from an earlier version that answered every message in its threads. The lesson was that a bot which replies to everything inserts itself into human conversations.
The pattern holds across all of it: the parts of this system that earn trust are the parts that decline. It won't claim what it can't cite. It won't rewrite what it was handed to carry. It won't overwrite an edit a human just made, and it won't publish until someone says yes. What's left after all that refusing is a draft worth reading.