Cron Jobs for AI Agents — Automate Your Agent's Schedule
Your agent is useless if you have to manually trigger it every time. The whole point of automation is that it runs without you. Cron is how you make that happen.
I run over a dozen cron jobs that keep my entire operation moving — social posting across 8+ accounts, bounty scanning, lead generation, content queue management, X reply monitoring. All of it fires on schedule, logs its results, and only bothers me when something breaks.
Here's how I set it up and what I learned the hard way.
The Basics
If you've never used cron, it's a scheduler built into every Linux system. You define when a command runs using a five-field expression:
# minute hour day month weekday command
0 9 * * * /usr/bin/python3 /home/billk/brand_cron.py --brand bmx
0 12 * * * /usr/bin/python3 /home/billk/brand_cron.py --brand axon
That's it. 9 AM, the BMX cron fires. Noon, the Axon cron fires. No SaaS platform, no workflow builder, no monthly fee. Just the operating system doing what it was designed to do.
My Actual Cron Schedule
# Social posting - BMX brand
0 9 * * * brand_cron.py --brand bmx
# Social posting - Axon brand
0 12 * * * brand_cron.py --brand axon
# Pinterest - every 2 hours
0 */2 * * * social_poster.py --platform pinterest
# Bounty scanner - every 5min during SF peak hours
*/5 10-11 * * * bounty_monitor.py
*/5 17-19 * * * bounty_monitor.py
# Bounty scanner - every 15min off-peak
*/15 0-9,12-16,20-23 * * * bounty_monitor.py
# Lead engine - twice daily
0 10,16 * * * lead_engine.py
# X reply monitor - every 30min
*/30 * * * * x_reply_monitor.py
The bounty scanner schedule is intentional. Expensify and other repos post bounties during SF business hours. If you're not scanning every 5 minutes during peak, someone else claims it first. I learned this when $250 Expensify issues were getting claimed within hours — my old 30-minute interval was too slow.
The Architecture: brand_cron.py
Each cron invocation runs brand_cron.py with a brand flag. Inside, it:
- Loads the content queue for that brand
- Picks the next piece of content
- Generates a caption if needed (GPT-4.1)
- Extracts a thumbnail from video via ffmpeg
- Posts to each platform via
social_poster.py - Updates the state file (
daily-axon-state.json, etc.) - Logs everything to
cron-axon.logorcron-bmx.log
The key design decision: brand_cron.py uses a ThreadPoolExecutor to run sync Playwright sessions inside an async context. This lets multiple platforms post concurrently within a single cron invocation. Instagram, Pinterest, and X can all fire at the same time instead of sequentially.
from concurrent.futures import ThreadPoolExecutor
def run_brand(brand: str):
platforms = get_platforms_for_brand(brand)
with ThreadPoolExecutor(max_workers=len(platforms)) as executor:
futures = [executor.submit(post_to_platform, brand, p) for p in platforms]
for f in futures:
try:
f.result(timeout=120)
except Exception as e:
log_error(brand, str(e))
Lessons Learned
Don't Route Through a Proxy
My original setup routed all platforms through an Azure GPT-4.1 proxy at port 18795. When that proxy wasn't running, every cron timed out after 3 minutes and got killed by SIGKILL. The fix: all platforms now call social_poster.py directly. The proxy is optional for content generation, never required for posting.
Use Absolute Paths
Cron runs with a minimal environment. Relative paths, ~ expansion, and environment variables don't work the way you expect. Use absolute paths for everything:
0 9 * * * /usr/bin/python3 /home/billk/scripts/brand_cron.py --brand bmx
Log to Separate Files
Don't dump all cron output to syslog. Each brand gets its own log file. Errors get their own file. When something breaks at 3 AM, I want to look at one file, not grep through a unified log.
State Files Over Databases
After each successful post, the cron writes a tiny JSON state file:
If the state file's timestamp is stale, the next cron run knows something went wrong. No database, no connection strings, no ORM. Just a JSON file on disk.
Cooldowns Prevent Spam
The Axon X queue rotates through 18 products with a 72-hour cooldown per product. The cron checks axon-post-state.json before posting — if the product was posted within the last 72 hours, it skips to the next one. This prevents the same product from appearing twice in a row.
systemd for Long-Running Agents
Some agents don't fit the cron model. The Discord bot, for example, needs to stay connected to the gateway permanently. For those, I use systemd services:
[Unit]
Description=Axon Discord Bot
[Service]
ExecStart=/usr/bin/python3 /home/billk/projects/axon-discord-bot/discord_axon_bot.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Restart=always means if the bot crashes, systemd brings it back in 5 seconds. No manual intervention needed.
Start Here
If you have an agent that you're running manually, put it on a cron. Right now. It takes 60 seconds:
crontab -e
# Add your line
0 9 * * * /usr/bin/python3 /path/to/your/agent.py
That single line turns a script you run when you remember into a system that runs whether you remember or not.
The full cron architecture — brand scheduling, bounty scanning, lead generation, state management — is part of the Axon toolkit at axon.nepa-ai.com.
