ASK KNOX
beta
LESSON 366

Putting It on a Schedule (Cron)

A pipeline that only runs when you manually trigger it is a chore — a pipeline on a cron is infrastructure that compounds while you focus on other things.

9 min read·Your First AI Content Pipeline

Introduction

Every pipeline in this track has been described as running "automatically." But there is a gap between a pipeline that works when you run it manually and a pipeline that works on its own, on schedule, while you are doing something else.

That gap is the cron configuration, the state tracking, and the failure handling. Without all three, you have not built infrastructure — you have built a useful script you still have to babysit.

This lesson covers how the jeremyknox.ai pipeline is scheduled, how state tracking prevents repetition and double-triggers, and what to do when the cron fails at 9 AM and nobody is watching.

Core Concept

The Cron Schedule

The jeremyknox.ai blog autopilot runs as a registered cron job (shown throughout this lesson with the example ID a1b2c3d4 — yours will differ) with the following configuration:

  • Schedule: Three fixed days per week (Monday, Wednesday, Friday) at 9:00 AM Eastern
  • Command: python3 /path/to/blog-autopilot/run.py
  • Environment: API keys loaded from a local .env file at runtime
  • Failure notification: Discord alert to the logs channel

Three days per week is the right cadence for this pipeline and site. It produces 12 to 13 articles per month — enough to build consistent publishing velocity and search authority without saturating the review queue or overwhelming readers.

The cadence you choose should match your review capacity. If you can review one article per day, a daily cron is fine. If reviews happen in batches, a three-per-week schedule gives you natural grouping.

The schedule itself is the five-field cron expression 0 9 * * 1,3,5. The fields, left to right, are minute, hour, day-of-month, month, and day-of-week:

Field 1: 0       → minute        → at :00
Field 2: 9       → hour          → 9 AM
Field 3: *       → day-of-month  → any date
Field 4: *       → calendar-month→ any
Field 5: 1,3,5   → day-of-week   → Mon, Wed, Fri (0=Sun … 6=Sat)

So 0 9 * * 1,3,5 reads as "at minute 0 of hour 9, on any date, in any calendar-month, but only on weekdays 1, 3, and 5." An asterisk means "every value." To change the cadence, edit the day-of-week field: 1,3,5 becomes 1,2,3,4,5 for weekdays only, or * for daily. Cron evaluates the expression in the machine's local timezone, so set the host clock (or the scheduler's timezone setting) to the zone you intend — here, Eastern.

State Tracking

State tracking is what transforms a pipeline from a one-shot script into an infrastructure that runs across hundreds of cycles without repetition.

The state file for the jeremyknox.ai pipeline is a simple JSON object written by each successful run:

{
  "last_run": "2026-05-30",
  "last_category": "ai",
  "published_topics": [
    "ai-memory-systems",
    "polymarket-arbitrage-mechanics",
    "claude-code-production-patterns"
  ],
  "run_count": 47
}

At the start of each cron run, gather.py reads this file. It uses last_category to select a different category this run (round-robin). It uses published_topics to exclude topics already covered (deduplication). It uses last_run for the "already ran today" guard.

At the end of a successful run, deliver.py writes back to the state file with the updated values: today's date as last_run, the selected category as last_category, and the new article slug appended to published_topics.

This is the minimum viable state model. As the pipeline matures, you might add:

  • A per-category run counter to detect drift in category balance
  • A rolling quality score computed from recent editing pass durations
  • A "topics waiting for trend alignment" secondary queue

Start with the minimum. Add complexity only when the pipeline shows you it needs it.

The Category Round-Robin

The round-robin logic is simple: maintain a fixed ordered list of categories, track which one ran last, and always select the next one in the list.

CATEGORIES = ["ai", "engineering", "strategy", "crypto"]

def next_category(last_category: str) -> str:
    current_index = CATEGORIES.index(last_category)
    return CATEGORIES[(current_index + 1) % len(CATEGORIES)]

This ensures even distribution across categories over time. If the pipeline skips a run (failure, holiday, manual override), the round-robin picks up from where it left off — it does not lose its place.

For pipelines covering more than four or five categories, a round-robin can feel mechanical. An alternative is a weighted selection: each category has a weight that decreases after each use and recovers over time. This produces more natural variation while still preventing domination by any single category.

The "Already Ran Today" Guard

A cron that fires more than once per day — due to misconfiguration, a manual re-trigger, or a cron system restart — should not produce two articles. The guard is a date check at the start of gather.py:

state = load_state()
if state["last_run"] == today():
    print("Already ran today. Exiting.")
    sys.exit(0)

Note the exit code: 0, not 1. This is an expected no-op, not a failure. If you exit with 1, your monitoring system will fire a false failure alert. Exit 0 with a log message, and let the monitoring system confirm the expected artifact (the new PR) was created — separate from the exit code.

Practical Application

Here is what a full cron setup looks like on a Mac Mini using Agent Gateway's cron system:

Cron ID:  a1b2c3d4           (example — yours will differ)
Schedule: 0 9 * * 1,3,5    (9 AM on Mon, Wed, Fri)
Command:  env -u CLAUDECODE python3 ~/dev/agent-skills/blog-autopilot/run.py
Env:      AGENT_GATEWAY_TOKEN, LEONARDO_API_KEY, OPENAI_API_KEY, GH_TOKEN
On fail:  Discord alert to channel <your-alerts-channel-id>

The env -u CLAUDECODE prefix is intentional: it strips the Claude Code environment variable so the cron runs as a clean system process, not inside a Claude Code session context. This prevents environment contamination from an active development session.

On systems without Agent Gateway, a launchd plist or a systemd timer provides the same schedule. The key configuration requirements are the same regardless of the scheduler:

  1. The full path to the Python interpreter (do not rely on python3 resolving correctly in a non-interactive shell)
  2. All API keys available as environment variables at run time
  3. A failure notification mechanism that is not the pipeline itself (a monitoring service, a separate health check)
  4. An idempotency guard so double-fires produce no-ops

Common Mistakes

Using relative paths in cron commands. A cron job's working directory is unpredictable. If your script opens state.json without a full path, it will fail to find the file when the cron fires. Use absolute paths everywhere inside scripts that run as cron jobs.

Storing API keys in the crontab. Do not inline API keys in the cron command or crontab entry. Load them from an env file at script startup. If your crontab is accidentally exposed (in a git commit, in a log), you do not want keys in it.

No failure notification. A cron that fails at 9 AM and produces no Discord alert is invisible until you notice the PR queue is empty. Add a failure notification in the cron configuration itself — most cron systems support a MAILTO or a failure webhook. The pipeline should also send its own Discord alert on any exit code 1, but the cron-level notification is a backstop for cases where the pipeline crashes before reaching the notification step.

Treating exit code 0 as sufficient success verification. The pipeline can exit 0 and produce no artifacts if the guard fired or an exception was silently caught. After each run, verify the expected artifact was produced: a new PR was opened, a new file exists in the branch. Artifact verification is more reliable than exit code checking.

Not testing the cron in production conditions. A script that works when you run it manually in your development environment may fail in the cron environment due to missing PATH entries, different working directories, or missing environment variables. Before relying on a cron, trigger it manually in the exact environment the cron will use and verify the full artifact chain.

Summary

  • The jeremyknox.ai cron fires Monday/Wednesday/Friday at 9 AM ET — three articles per week across rotating categories.
  • State tracking (last run date, last category, published topics) enables round-robin rotation and deduplication across hundreds of pipeline cycles.
  • The "already ran today" guard prevents duplicate articles from double-triggers; exit 0, not 1.
  • Use absolute paths and explicit env loading in all cron-executed scripts.
  • Artifact verification (did a PR actually open?) is more reliable than exit code checking for confirming a successful run.

What's Next

The next lesson closes the loop — how to measure what actually works, track engagement signals back to your sourcing decisions, and use that data to make the pipeline progressively smarter over time. This is the capstone lesson.