What 40 Automated Drafts Taught Me About Building a Cron-Powered Technical Blog
Three runs per day, every day: an AI agent drafts technical blog posts from git history, project context, and recent working sessions. After 40+ automated drafts, here is what the architecture looks like and where it breaks.
Three times a day, a cron job wakes up an AI agent. The agent reads my recent git commits, project files, and the current state of my blog's API. It writes a technical draft, posts it to the production API, and exits. No human in the loop. No queue. No approval gate.
I built this system because I write code every day but publish sporadically. The drafts pile up in a SQLite database on a Fly.io machine. Some stay drafts. Some get polished and published. The gap between writing and shipping is where blog posts die.
Here is how the pipeline works, what went wrong in the first 40 drafts, and what I changed to fix it.
The Architecture
The system is a Hermes Agent instance configured as a cron job. Hermes Agent is a programmable AI assistant framework. It runs tool loops, reads files, executes shell commands, and calls APIs. The cron schedule is three runs per day: 09:00, 13:00, 17:00 EDT.
Each run follows the same flow:
-
Inventory. The agent calls GET /api/blog/posts?published=true to list every published post and GET /api/blog/posts?published=false for existing drafts. This prevents duplicates. The agent maintains a mental map of every slug and title in the database.
-
Topic selection. The agent browses recent project activity. It reads git log, checks recent working sessions, and examines project files. The topic must be original: no overlap with existing posts or today's earlier drafts. Three different category rotations: a technical deep-dive at 09:00, a system architecture post at 13:00, and a retrospective or workflow analysis at 17:00.
-
Drafting. The agent writes a Markdown post in the tone defined in its persona file: sharp, precise, sovereign. No em dash. No "I think" or "I believe." No AI disclaimer. 500-1500 words. YAML frontmatter with title, slug, excerpt, tags, published_at, and published=false.
-
Publishing. The draft is sent as JSON to POST /api/blog/posts with a Bearer token from the .env file. If the API returns 422, the agent corrects the format and retries. If 401, it reports the error and stops.
What Broke First
Duplicate Detection Is Not Trivial
The first version checked only titles. Two weeks in, the agent wrote a post about SQLite in production: same topic as an existing post but with a different title. The slug was different but the content overlapped heavily. I added slug and excerpt comparison to the inventory step. The agent now checks three signals: title, slug, and a semantic summary of the excerpt.
The Tone Drift Problem
Without explicit constraints, the agent drifted toward generic tech-blog style. Sentences like "In this article, we will explore..." appeared. The persona file now includes a specific ban on certain phrases, a constraint on maximum sentence length, and an explicit prohibition on the em dash character. The CSS theme file matches: amber on near-black, no gradients, no second accent colour. The tone enforcement is in the system prompt, not in post-processing.
API Token Rotation
The token lives in a .env file in the project root. When I rotate secrets, the cron job fails silently until the next run. I added a health check step at the start of every run: the agent calls a lightweight endpoint with the token before doing any work. If the token is invalid, the run aborts immediately and logs the failure. No partial drafts, no orphaned state.
Race Conditions in the Database
The API uses SQLite. Two concurrent requests (one from the agent posting a draft, another from Filament admin saving a post) deadlocked exactly once. The fix was a write queue in the Laravel controller: the blog post endpoint acquires an advisory lock before INSERT or UPDATE. The agent never sees the deadlock because the queue serializes writes.
What Works Well
The three-run cadence. Enforcing topic variety by time slot produces better coverage. The 09:00 run tends to write about code I wrote the previous evening. The 13:00 run catches morning sessions. The 17:00 run is retrospective: it synthesises patterns across the week.
The draft pile. After 40+ drafts, about 30% get published. The rest sit in the database as a searchable archive. When I need to write a proper post, I start from a draft. The draft gives me a structure and a first pass at the technical details. Editing is faster than writing from scratch.
The constraint file. The persona and project context files in the agent's configuration act as a spec sheet for the output. Every constraint (no em dash, no AI disclaimer, WCAG AA contrast ratios, amber-only accent colour) is enforced by the agent at generation time, not by a linter after the fact. This removes the edit cycle for style violations.
What I Would Change
The system has no feedback loop. The agent does not know whether a post was published, read, or ignored. A future version should expose a read-count endpoint and let the agent prioritise topics with higher engagement. But that is a nice-to-have. The current system already does what I wanted: it turns daily coding sessions into searchable technical notes without friction.
The Infrastructure
The agent runs on a Linux workstation. The blog lives on Fly.io. The cron job is a systemd timer with OnCalendar=--* 09,13,17:00:00. The agent's working directory is the project root, so git history and project files are always up to date.
The total cost of running this system is zero additional infrastructure. The same Fly.io machine that serves the blog handles API requests from the cron job. The agent runs on existing hardware. The only cost is the API credits for the agent model: about $0.02 per draft.
The Real Value
The automated drafts are not the output. The output is the constraint system. Defining what a good technical post looks like (tone, structure, scope, audience) forces clarity about what the blog is for. The cron job executes that definition consistently. The drafts are a side effect of a well-specified system.
When the agent produces a draft that needs heavy editing, I update the constraints instead of editing the draft. The fix propagates to every future post. This is the same principle as treating compiler warnings as errors: fix the process, not the output.