Seven stages from a schedule tick to a deployed page. Four of them can end the run early, and that is the point - most ticks should produce nothing.
- 1
1.The tick
Every ten minutes an n8n schedule trigger opens an SSH connection to the VPS and runs a single command. A run can hold Claude for two to eight minutes, so ticks overlap by design. The entry script takes an exclusive lock and, if a previous run still holds it, prints an empty array and exits clean - a skipped tick has to look like a quiet news cycle, not a failure, or n8n starts sending error mail.
run-window.sh · flock -n out/run.lock
- 2
2.The ingest gate
Before spending a single token, a Python gate pulls the news APIs and RSS feeds using a stored cursor, then diffs against a seen-links state file. Three exit codes drive the run: new items found, so proceed with a priority list; sources fine but nothing new, so print an empty array and skip Claude entirely; error, so fall open to the raw headline prompt. Skipping the model on a quiet cycle is where most of the cost saving lives.
ingest-gate.py · exit 0 / 10 / 20
- 3
3.Generation
The prompt is assembled from a versioned prompt file, the priority news list, the feed list, and the newest 400 published titles as an explicit do-not-repeat block. Claude runs headless with a JSON schema attached, so the output is structurally valid or the call failed - there is no parsing of prose. It returns zero to two articles. Zero is a normal outcome.
generate-article.sh → run-claude.sh
- 4
4.The judge
A second model call, this one with web tools disabled, scores the draft against a rolling fourteen-day event map: is this a genuinely new real-world event, a follow-up with a materially new fact, or a rehash of something already covered? It shipped in shadow mode - always logging a verdict, changing nothing - until the verdicts were trustworthy enough to enforce. It fails open by design: any error yields "new" and the article proceeds.
judge-article.sh · eventmap.py
- 5
5.The review window
The draft is POSTed to an n8n webhook, which reads the file back over SSH, renders an approval email through Resend, and waits. The editor approves, rejects, or requests a revision. Approval writes a provenance record and publishes. Silence for sixty minutes is also a decision - see the next step.
NPapproveFlow01 · review_apply.py
- 6
6.The lapse path
An article nobody touched within sixty minutes plus a fifteen-minute grace period publishes itself, labeled as unreviewed rather than editor-approved. The grace period exists so n8n's own timeout branch acts first and the cron sweep stays a backstop rather than a competitor. Anything still unpublished after forty-eight hours is stale news and gets parked, not published.
auto-publish-sweep.sh · 60 + 15 min, 48h cap
- 7
7.Publish
Rebase onto origin, rotate the article into the content JSON, attach a licensed stock photo if the generator produced none, fold the event into the event map so the judge keeps cross-day memory, then commit and push with one retry on conflict. Vercel builds from the push. If any step fails, the article stays in the queue and the next run picks it up.
publish-to-site.sh · rotate_content.py