Run Codex on a Schedule: Cron, Webhooks, and Always-On Jobs

Schedule codex exec with cron or systemd timers, wire failure alerts with exit codes and webhooks, and pick the job's home: laptop, VPS, or a hosted endpoint.

Scheduling Codex is two solved problems and one real decision. The solved problems: codex exec runs a prompt non-interactively and exits with a status code, and both cron and systemd timers know what to do with a command like that. The real decision is where the schedule lives, because a job is only as reliable as its machine: a laptop that sleeps misses runs, a VPS runs but makes you the operator, and a hosted endpoint makes reliability someone else’s contract.

The job itself

Any recipe from the exec cookbook can be scheduled as-is. The one property worth adding for unattended runs is a checkable contract: a fixed output location, a verdict line, or JSON that a follow-up step can validate. A scheduled job whose failure looks identical to its success will fail for weeks before anyone notices.

codex exec --sandbox read-only \
  "Summarize yesterday's commits and open TODOs, 10 bullets max, flag anything risky."

Sign the machine in once with codex login --device-auth (documented for headless boxes), and every scheduled run reuses that session. The session file, ~/.codex/auth.json, is password-grade; it stays on the machine, mode 600, owned by the job’s user.

Cron that holds up

The naive crontab line works in your terminal and fails at 6 a.m. because cron strips the environment. This version survives:

# crontab -e
MAILTO=ops@example.com
PATH=/usr/local/bin:/usr/bin:/bin

# weekday digest at 06:15; flock prevents overlapping runs
15 6 * * 1-5  cd /srv/app && flock -n /tmp/codex-digest.lock codex exec --sandbox read-only "Summarize yesterday's commits and flag anything risky, 10 bullets max" >> /var/log/codex-digest.log 2>&1

The fixes, in order of how often they are the culprit:

  • PATH. Cron’s default PATH will not find an npm-global binary. Set it, or call /usr/local/bin/codex absolutely.
  • HOME. The CLI resolves ~/.codex for auth and config. Install the crontab for the user that ran the login, not root.
  • Output. Redirect to a log, or cron mails every run’s stdout. With the redirect in place, MAILTO only fires on output you did not capture, which makes it a crude but free alert channel.
  • Overlap. Agent runs take minutes, not milliseconds. flock -n skips a run rather than stacking two sessions on one window.

One more cron quirk: percent signs are special in crontabs, so a $(date +\%F) in your command needs the backslash.

systemd timers, with failure alerting built in

systemd earns its extra files by answering the question cron dodges: what happens when the job fails? OnFailure= fires a second unit, and that unit can post anywhere.

# /etc/systemd/system/codex-triage.service
[Unit]
Description=Codex morning triage
Wants=network-online.target
After=network-online.target
OnFailure=codex-alert.service

[Service]
Type=oneshot
User=deploy
Environment=HOME=/home/deploy
WorkingDirectory=/srv/app
ExecStart=/usr/bin/env codex exec --sandbox read-only "Read logs/ci-failures.log and produce a triage report: causes, owners, next steps"
StandardOutput=append:/srv/app/reports/triage.md
# /etc/systemd/system/codex-alert.service
[Unit]
Description=Alert when a Codex job fails

[Service]
Type=oneshot
ExecStart=/usr/bin/curl -fsS -X POST -H "Content-Type: application/json" \
  -d '{"text": "Scheduled Codex job failed. Check: journalctl -u codex-triage"}' \
  https://hooks.slack.com/services/T000/B000/XXXX
# /etc/systemd/system/codex-triage.timer
[Unit]
Description=Run codex-triage on weekday mornings

[Timer]
OnCalendar=Mon..Fri 06:15
Persistent=true

[Install]
WantedBy=timers.target

Enable with systemctl enable --now codex-triage.timer. Persistent=true runs a missed job at next boot, which is the difference between a server that rebooted overnight and a digest that silently never arrived. History lives in journalctl -u codex-triage, and the general server hygiene around all this is in the headless setup guide.

codex exec exits non-zero when it cannot complete: auth lapsed, network down, repo missing. One failure mode it cannot signal cleanly in advance is an exhausted usage window, where the run fails until the window resets on its own schedule. Alert on it like any other failure, and read what happens when you hit your Codex usage limit for the lane-based fix.

Webhooks instead of timers

Some “schedules” are really events: a deploy finished, an alert fired, a form arrived. The pattern stays identical, with the trigger swapped: a tiny HTTP handler or your CI’s webhook integration calls the same codex exec command the timer would have. Recipe 11 in the cookbook is a complete handler in 20 lines. The reliability question does not change at all, because a webhook receiver is just a scheduled job that listens instead of waits.

Laptop vs VPS vs hosted endpoint

ConcernLaptopVPS (~$5/mo)Hosted endpoint
Survives sleep and rebootsNoYesYes
Session refresh when auth lapsesYou notice eventuallyYou notice eventually, or build a checkWatched as part of the service
Overlapping runsflock, DIYflock or systemd, DIYQueued
Run history and logsA log file you grepA log file you grepPer-request log, with the serving lane named
Exhausted usage windowJob fails until resetJob fails until resetFalls back: second account, then your API key
Added cost$0~$5/mo plus your attention$129/mo flat

Honest readings of that table: a laptop is fine for nudges you would forgive for missing, like a morning summary. A VPS is the right home for one-account jobs you are willing to operate, and the VPS walkthrough takes you from provider choice to a survivable setup in an afternoon. A schedule is a promise, and the machine behind it decides whether you keep it.

The hosted column is Codex Hosted: the same official CLI, signed in with your own ChatGPT account in an isolated container, exposed as an OpenAI-compatible endpoint that cron, webhooks, n8n, or CI can call with one revocable key. When a window exhausts mid-schedule, the request falls back to a second connected account, then your own API key, and the log shows which lane served each run. The full cost-and-ops comparison with running it yourself is in Codex Hosted vs running Codex yourself.

Frequently asked questions

Can you run Codex on a cron schedule?

Yes. codex exec is the CLI's non-interactive mode: it runs a prompt to completion, prints to stdout, and exits with a status code, which is exactly what cron and systemd timers expect. Sign in once with device auth and scheduled runs reuse the saved session.

Why does my Codex cron job fail when the same command works in my terminal?

Almost always environment: cron starts with a bare PATH and sometimes the wrong HOME, so the codex binary is not found or the CLI cannot resolve ~/.codex and reports you as logged out. Set PATH and HOME in the crontab, use absolute paths, and redirect output to a log.

How do I get alerted when a scheduled Codex job fails?

Use the exit code. With cron, set MAILTO or chain a webhook with || curl after the command. With systemd, set OnFailure= to a oneshot unit that posts to Slack or any webhook. Treat an exhausted usage window as a failure mode worth alerting on too.

Should scheduled Codex jobs run on a laptop, a VPS, or a hosted endpoint?

A laptop misses runs whenever it sleeps, so it suits nudges, not jobs. A VPS at around $5/month runs reliably but leaves auth refresh, queueing, logs, and limit handling to you. A hosted endpoint covers those four and falls back to another lane when a usage window exhausts.

More on Codex CLI
Codex Hosted · the main feature

Run your AI workloads on your ChatGPT subscription.

ProxyLLM runs OpenAI's Codex for you, signed in with your own ChatGPT account. Your apps call one OpenAI-compatible endpoint and the work bills to your flat plan instead of per-token API pricing.