System ESM Reference Build 01
It drafts a post every other day, in my voice, anchored to things that actually happened. Then it stops and waits for me, because it is my real professional profile.
Reference build. I built this for myself, not for a paying client. It runs in production on my real LinkedIn profile, which is the part that makes it worth showing.
The problem
I ship work constantly and almost none of it was visible. Posting consistently is the kind of task that loses to whatever is on fire that day, so it just never happened.
The obvious fix is to have an LLM write the posts. The obvious fix is also how people end up with a feed full of content that sounds like nobody, says nothing, and occasionally claims something that isn't true under their own name.
So the interesting problem was never "can a model write a LinkedIn post." It was: can a system write on a schedule, in my voice, about things that actually happened, and be safe to leave running.
What I built
It picks the next content pillar from a fixed five-way rotation held in state. It pulls real source material, either from my decision log for posts about actual work, or from a commentary bank for the opinion pieces. It writes the post in my voice, following a style encode built from things I actually wrote. It runs structural checks, saves the draft, renders a preview I can read in a browser, and generates a card image.
Then it stops and waits for me. I approve or reject. Nothing publishes without that.
deterministic code the model human gate
The hard part
A content generator is a toy. A content generator wired to your real professional reputation is a liability, and it fails in four specific ways. Each one needed its own guard, and I found three of the four the hard way.
The voice encode is not a description of my voice, it is samples of it. Real paragraphs I actually wrote, pasted in, plus explicit rules: short sentences, plain words, no jargon stacking, minimal emoji and never in the hook.
Then a deterministic checker enforces the mechanical parts. Length band, hook length, hashtag count, no URLs in the body, and a banned list so it never names a product I sell instead of describing the outcome. The checks flag, they never block, because a human reads every draft anyway.
This is the one that matters, and it is the one that bit me.
The rule is that every post has to be anchored in something real. Posts about my own work pull from my decision log. Commentary pulls from a bank of takes I actually hold. If there is nothing specific and true to say, the instruction is to say less.
That was not enough. On 2026-08-26 a draft claimed the automation had published "for the first time." The agent had inferred that from a missing dependency, reasoning that a method which had ever run would have left its library installed. Plausible. Also a guess presented as a fact, and it had already reached a log entry too.
The fix was not a better prompt. It was a change to the record: every ledger row now states how each post was published, by hand or through the API, so the question is answerable from data next time instead of inferable. Plus a standing rule, don't claim a first unless the record shows it.
That is the pattern I trust. When generated text makes a claim, the fix is to make the claim checkable, not to ask the model more nicely.
Rotation lives in a state file, not in the prompt, because a model asked to remember what it posted last week will confidently make something up. State is bookkeeping and bookkeeping is code.
It still went wrong. Two posts made the same argument three days apart. The cause was two layers down: rotation only advances when a post is actually published, which is correct, but the finalize step was crashing on a file copy before it got to the advance. A bookkeeping step that crashes looks exactly like a bookkeeping step nobody ran.
Fixed by making finalize idempotent, and by making "state agrees with the ledger" a checked precondition before drafting rather than an assumption.
The worst failure for anything unattended. Mine did a version of this: nothing was published for a week, rotation only advances on publish, so every scheduled run redrew the same pillar and three near identical drafts stacked up from one source file. The approval gate was working exactly as designed and the queue cost was completely invisible.
The rule that came out of it: a draft sitting unreviewed for more than two days is not neutral, it costs the next slot. Post it, reject it, or archive it.
Evidence
| Claim | Artifact |
|---|---|
| It publishes real posts | 9 live LinkedIn URLs, one per row in the content ledger |
| It runs on a schedule, unattended | A launchd job, every other day at 15:07 |
| The approval gate is real | 14 drafts logged, 9 published. One rejected outright, four archived unposted |
| The structural checks exist | methods/validate_post.py, runnable |
| Rotation is deterministic | rotation-state.json, advanced only on publish |
| It publishes through the API | Three posts, 2026-08-25 to 2026-08-27. Earlier rows do not record the method, so I do not claim them |
| The failures are documented | Five incidents, each with a dated ledger entry and a structural fix |
That last row is the one that matters. Nine published posts is the least interesting number here. Anyone can show you output. The five documented failures are the actual evidence, because they show what happens when an unattended system goes wrong and nobody is watching, which is the only question worth asking before you leave one running.
Every one of the five was silent. A duplicate post. A stalled rotation. An unsupported claim. A cropped image. Not one of them threw an error a human would see.
One number I am not going to give you. Engagement. I have not wired view or reaction data into the ledger, so I have no performance claims to make and I am not going to invent any. What I can show you is that it runs, that it is safe, and that when it broke I found out and fixed it properly.
Stack
The dividing line I use: deterministic where the input is structured and the decision is a rule you could write down, agentic where the input is messy and the output is a judgement or a piece of prose.
Writing a post in a specific voice from a decision log is squarely the second kind. Picking the next pillar, checking hook length, advancing state, writing the ledger row, all of that is the first kind. So the split is the architecture. Eight small Python methods do every deterministic thing. The model does exactly one job, writing, and it is not allowed to own state.
Where a workflow tool would have won. If this were pulling structured rows and posting them on a threshold, n8n would be the right answer and I would have used it. It is the tool I reach for when the work is plumbing. This isn't plumbing.
It is also not fully autonomous, on purpose. I approve every post. The workspace supports a mode that publishes without asking and I have not turned it on, because it is my actual professional profile and the cost of one bad post outweighs the convenience. The gate is the feature.
Cost
The drafting runs under my existing Claude Code subscription rather than the API, so each draft has no marginal cost. Publishing through LinkedIn is free at this volume. There is a standalone runner built and documented that would use the API and would cost real money per run, but that is not the path in use, so I have not measured it and I am not going to quote it.
A client build would pay API rates. Anyone showing you $0 for a system like this is either riding a subscription the way I am, or not counting properly.
For a real client
The limits
Worth saying plainly, because most case studies you'll read won't.
What it does prove: I can build a scheduled generation system that writes in a specific voice, stays anchored to real source material, keeps its own state honestly, fails safely, and gets fixed properly when it breaks. That is the machine. The output on top of it is interchangeable.