On this page
Take inventory
Start by writing down what the crontab actually does. Here is the one we'll migrate: four entries on a single server whose clock is set to America/Chicago, which nobody remembered until this exercise.
# m h dom mon dow command*/15 * * * * /usr/bin/node /srv/app/scripts/sync-inventory.js0 3 * * * /usr/bin/python3 /srv/app/scripts/cleanup_sessions.py30 6 * * 1 /usr/bin/node /srv/app/scripts/weekly-digest.js0 0 1 * * /usr/bin/python3 /srv/app/scripts/close_invoices.py >> /var/log/invoices.log 2>&1Give every line a task name, a queue and an explicit time zone. Fast, frequent work and slow monthly work belong on different queues.
| Cron | Task | Queue | Time zone | Language |
|---|---|---|---|---|
| */15 * * * * | sync-inventory | maintenance | UTC | Node.js |
| 0 3 * * * | cleanup-sessions | maintenance | UTC | Python |
| 30 6 * * 1 | weekly-digest | emails | America/Chicago | Node.js |
| 0 0 1 * * | close-invoices | billing | America/Chicago | Python |
Migrate step by step
Turn each script into a task
Don't rewrite the scripts. Move their top-level code into a function, accept a
logcallback, and register that function as a task. The cron entry can keep calling the script directly until you switch.// scripts/sync-inventory.js (before: ran top to bottom, then exited)export async function syncInventory({ log = console.log } = {}) { const changed = await pullSupplierFeed(); await applyStockChanges(changed); log(`Synced ${changed.length} SKUs`); return { changed: changed.length };} // worker.jsimport { Worker } from "@halyard/node";import { syncInventory } from "./scripts/sync-inventory.js";import { weeklyDigest } from "./scripts/weekly-digest.js"; const worker = new Worker(process.env.HALYARD_KEY, { queues: ["maintenance", "emails"] }); worker.task("sync-inventory", (job) => syncInventory({ log: (m) => job.log(m) }));worker.task("weekly-digest", (job) => weeklyDigest({ log: (m) => job.log(m) })); worker.start();# scripts/cleanup_sessions.py (before: ran under if __name__ == "__main__")def cleanup_sessions(log=print): deleted = db.sessions.delete_expired() log(f"Deleted {deleted} expired sessions") return {"deleted": deleted} # worker.pyimport osfrom halyard import Workerfrom scripts.cleanup_sessions import cleanup_sessionsfrom scripts.close_invoices import close_invoices worker = Worker(os.environ["HALYARD_KEY"], queues=["maintenance", "billing"]) @worker.task("cleanup-sessions")def cleanup_sessions_task(job): return cleanup_sessions(log=job.log) @worker.task("close-invoices")def close_invoices_task(job): return close_invoices(log=job.log) worker.start()Create the schedules, paused
Create all four schedules in one go with
paused: true. They exist, show up in the dashboard and can be reviewed, but won't fire until you resume them, so cron and Halyard never both run the same work.// scripts/create-schedules.jsimport { Halyard } from "@halyard/node"; const halyard = new Halyard(process.env.HALYARD_KEY); const schedules = [ { cron: "*/15 * * * *", task: "sync-inventory", queue: "maintenance", timezone: "UTC" }, { cron: "0 3 * * *", task: "cleanup-sessions", queue: "maintenance", timezone: "UTC" }, { cron: "30 6 * * 1", task: "weekly-digest", queue: "emails", timezone: "America/Chicago" }, { cron: "0 0 1 * *", task: "close-invoices", queue: "billing", timezone: "America/Chicago" },]; for (const s of schedules) { const created = await halyard.schedules.create({ ...s, overlap: "skip", paused: true }); console.log(created.id, s.task);}# scripts/create_schedules.pyimport osfrom halyard import Halyard halyard = Halyard(os.environ["HALYARD_KEY"]) schedules = [ {"cron": "*/15 * * * *", "task": "sync-inventory", "queue": "maintenance", "timezone": "UTC"}, {"cron": "0 3 * * *", "task": "cleanup-sessions", "queue": "maintenance", "timezone": "UTC"}, {"cron": "30 6 * * 1", "task": "weekly-digest", "queue": "emails", "timezone": "America/Chicago"}, {"cron": "0 0 1 * *", "task": "close-invoices", "queue": "billing", "timezone": "America/Chicago"},] for s in schedules: created = halyard.schedules.create(**s, overlap="skip", paused=True) print(created.id, s["task"])Switch over one line at a time
Start with the most frequent, least risky entry. Comment it out in the crontab, resume its schedule and watch one run succeed. Repeat for the next line the following day.
Terminal# 1. Comment out the line in the crontabcrontab -l | sed 's|^\*/15 .*sync-inventory.js|# moved to Halyard: &|' | crontab - # 2. Resume the matching schedulehalyard schedules resume sch_2Qm7Tz4Lw9 # 3. Watch the first runhalyard jobs list --task sync-inventory --limit 1 --followPrefer the API? Resuming is a single request:
Terminalcurl -X POST https://api.halyard.example/v3/schedules/sch_2Qm7Tz4Lw9/resume \ -H "Authorization: Bearer hy_test_51Hq8Zx2rTmV9cKpL3d"Retire the cron server
Once the last schedule has run through a full cycle (a month, if you have a monthly job), delete the crontab and the wrapper scripts'
mainentry points. Keep a copy of the old crontab in your repository's history for the record.
What behaves differently
- Failures retry. A cron job that fails waits for its next slot. A Halyard job retries with backoff, so make sure each task is safe to run twice (see Make jobs idempotent).
- Output goes to job logs, not to local mail or
/var/log. Anything passed tojob.logis kept for 30 days and searchable in the dashboard. - No overlapping runs. With
overlap: "skip", a 15-minute sync that takes 20 minutes skips a slot instead of running twice in parallel, which cron would happily do. - Time zones are explicit. Each schedule names its zone; nothing depends on a server's clock setting.
Day-of-month and day-of-week
Halyard follows the same rule as classic cron: when both the day-of-month and the day-of-week fields are restricted, a run happens when either matches. 0 9 1 * 1 runs on the 1st of every month and every Monday, not only on Mondays that fall on the 1st. Expressions copied straight from your crontab keep their meaning.
Recap
- Inventory first: every crontab line gets a task name, a queue and a time zone.
- Scripts become tasks by wrapping their main function, not by rewriting them.
- Schedules are created paused and resumed one at a time as cron lines are commented out.
- Cron's server-local time becomes an explicit time zone on each schedule.
Next steps
Keep going with a related guide, or look up every option used here in the Jobs API reference.