One post a day from a cron job

Christian Drexler

One of my sites publishes one article every morning at 07:40 and has done so since mid September. Nobody writes it on the day. The shape is simple enough to describe in a paragraph, and the interesting parts are the small decisions around it.

The shape

A folder holds source material: notes, briefs, product sheets, old drafts, each one a file. A job runs once a day. It picks the oldest file that has not been used, drafts an article from it with a language model, builds the static site, commits and pushes. The host deploys on push. A log gets one line per run that says [posted] <slug> or [skipped] <reason>.

40 7 * * *  /usr/bin/flock -n /tmp/daily.lock  /home/me/daily/run.sh >> /home/me/daily/run.log 2>&1

The flock matters more than it looks. If yesterday's run hangs on a network call, you do not want today's run starting on top of it and publishing twice.

Used means used

The job marks a source file as consumed by moving it to a done/ folder, not by keeping a list somewhere. A list can drift from the folder; the folder cannot drift from itself. If the source folder is empty the job logs [skipped] no source and exits zero. An empty inbox is not an error, it is a signal that I need to drop in more material, and the log line is enough of a reminder.

Idempotent by date

Before doing anything the job checks whether a post with today's date already exists. If it does, it exits. This single check means I can run the script by hand at any hour to test it, or re-trigger it after fixing something, without ever producing a second post for the day. Most "the cron fired twice" problems disappear once the job refuses to repeat itself.

The human week

For the first seven days I read every post before it went live. The job wrote to a staging branch, I read the draft on my phone over coffee, and I merged or rejected. Three got rejected: one for saying nothing, one for a claim the source did not support, one for a tone that would have embarrassed the site. Each rejection changed the prompt or added a rule to the drafting step. After the week the job moved to the main branch and I stopped reading daily.

I still skim once a week. The draft prompt has a line that says to refuse and log [skipped] thin source when the input does not carry enough to write from, and that line came out of the rejected posts.

What I would watch

A build failure after the article is written leaves a draft committed but not live; the log says [posted] while the site shows nothing. The fix is to log only after the deploy check passes, with a fetch of the new URL as the last step. Also: the model's idea of today's date is whatever you tell it, so pass the date in explicitly rather than letting it guess.

None of this is clever. It is one job, one folder, one log, and a week of reading before trusting it. That week is the part most people skip.