💻 Coding
Crontab to systemd Timer Converter: OnCalendar Expressions, Oneshot Service Units with User and EnvironmentFile, Persistent Catch Up, Sandboxing Options, OnFailure Alerts, and journalctl Checks
Move a server's cron jobs to systemd timers without breaking them: translate each crontab line to an OnCalendar expression you can verify, write the matching oneshot service with the right user, working directory, and environment, add catch up for missed runs, sandbox the job, replace MAILTO with an OnFailure alert, and check runs with systemctl and journalctl.
0Reviews
Prompt
Act as a Linux systems engineer who migrates legacy cron jobs to systemd timers on Debian, Ubuntu, and RHEL servers, and who knows the usual breakages: scripts that relied on cron's environment, overlapping runs, and timers that were written but never enabled. Inputs: - The crontab lines exactly as they are, including which user's crontab or /etc/cron.d file they came from: [CrontabLines] - What each script does, what it writes to, and how long it usually runs: [JobBehavior] - Environment the scripts need (PATH additions, variables, secrets files, working directory): [JobEnv] - Distro and systemd version from systemctl --version: [SystemdVersion] - How failures are reported today (MAILTO, a log file, nothing): [Alerting] - Server timezone and whether jobs must run in a different zone: [TimeZone] - Output format: [Format] Generate: 1. A translation table: each line in CrontabLines next to its OnCalendar expression, plus the systemd-analyze calendar command to verify the next run times. Handle @reboot with OnBootSec or a service enabled at boot, and @daily style shortcuts explicitly. 2. For each job, a .service unit with Type=oneshot, User and Group matching the original crontab owner, WorkingDirectory, Environment or EnvironmentFile from JobEnv, and ExecStart with absolute paths. 3. A matching .timer unit with OnCalendar, Persistent=true for jobs that must catch up after downtime, RandomizedDelaySec where many servers would otherwise run at once, and AccuracySec when exact timing matters. Explain that a oneshot service will not start a second copy while one is still running, which replaces flock wrappers for overlap. 4. Sandboxing options sized to JobBehavior: NoNewPrivileges, PrivateTmp, ProtectSystem=strict with ReadWritePaths for the directories the job writes, ProtectHome, and a note to test each one because a too strict setting fails the job. 5. Alerting: replace MAILTO with OnFailure pointing to a templated notify unit, with a simple example that sends mail or posts to a webhook. 6. A cutover plan: install units under /etc/systemd/system, systemd-analyze verify, daemon-reload, enable --now the timers only, comment out the crontab lines the same minute, then confirm with systemctl list-timers and journalctl -u. 7. TimeZone handling: if SystemdVersion supports a timezone in OnCalendar, show it; otherwise explain the job follows the server zone. Constraints: - Never put secret values in unit files; use an EnvironmentFile readable only by root or the service user. - Do not remove the crontab entries until the timer has run once successfully. No em dashes.
Instructions
Replace every [bracket] with your details before running. Works on ChatGPT, Claude, and Gemini.
Generated Output
This image was generated using the prompt above.

Examples
Example Input
CrontabLines: from user deploy's crontab: '*/15 * * * * /home/deploy/bin/sync-inventory.sh'; '30 2 * * * /opt/backup/run-backup.sh >> /var/log/backup.log 2>&1'; '0 9 * * 1 /home/deploy/bin/weekly-report.py' JobBehavior: sync-inventory calls an API and writes /var/lib/inventory, about 40 seconds; backup dumps Postgres to /srv/backups, about 25 minutes; weekly-report emails a CSV, about 1 minute JobEnv: needs /usr/local/bin in PATH, API_TOKEN and DB password currently exported in .bashrc, scripts expect to run from /home/deploy/app SystemdVersion: Ubuntu 24.04, systemd 255 Alerting: MAILTO=ops@example.com at the top of the crontab TimeZone: server in UTC, weekly report must go out 9:00 New York time Format: table, unit files, cutover steps
Example Output
Translation table
| Cron | OnCalendar | Verify |
| */15 * * * * | *:0/15 | systemd-analyze calendar '*:0/15' |
| 30 2 * * * | *-*-* 02:30:00 | systemd-analyze calendar '*-*-* 02:30:00' |
| 0 9 * * 1 | Mon *-*-* 09:00:00 America/New_York | systemd-analyze calendar 'Mon *-*-* 09:00:00 America/New_York' |
Secrets first
Move API_TOKEN and the DB password out of .bashrc (cron never read it anyway unless the scripts sourced it) into /etc/deploy/jobs.env, owner root:deploy, mode 0640.
/etc/systemd/system/sync-inventory.service
[Unit]
Description=Sync inventory from API
OnFailure=notify-failure@%n.service
[Service]
Type=oneshot
User=deploy
Group=deploy
WorkingDirectory=/home/deploy/app
Environment=PATH=/usr/local/bin:/usr/bin:/bin
EnvironmentFile=/etc/deploy/jobs.env
ExecStart=/home/deploy/bin/sync-inventory.sh
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/inventory
/etc/systemd/system/sync-inventory.timer
[Unit]
Description=Every 15 minutes
[Timer]
OnCalendar=*:0/15
RandomizedDelaySec=30
[Install]
WantedBy=timers.target
Overlap: if a sync ever takes longer than 15 minutes, the timer will not start a second copy while the oneshot is still active.
backup.service and backup.timer
Same pattern, ExecStart=/opt/backup/run-backup.sh, ReadWritePaths=/srv/backups, no >> redirect: output goes to the journal. Timer: OnCalendar=*-*-* 02:30:00, Persistent=true so a backup missed during a reboot runs at the next boot.
ProtectHome is left off here because the script lives in /opt but test with ProtectHome=read-only before adding it.
weekly-report.timer
OnCalendar=Mon *-*-* 09:00:00 America/New_York, Persistent=true. systemd 255 supports the timezone suffix, so it follows New York daylight saving while the server stays in UTC.
Failure alerts (replaces MAILTO)
/etc/systemd/system/notify-failure@.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/notify-failure %i
The script runs: journalctl -u "$1" -n 50 --no-pager | mail -s "FAILED $1 on $(hostname)" ops@example.com
Cutover
1. sudo systemd-analyze verify /etc/systemd/system/{sync-inventory,backup,weekly-report}.{service,timer}
2. sudo systemctl daemon-reload
3. sudo systemctl start sync-inventory.service and check journalctl -u sync-inventory -n 30
4. sudo systemctl enable --now sync-inventory.timer backup.timer weekly-report.timer
5. crontab -e as deploy and comment out the three lines at the same time
6. systemctl list-timers shows each NEXT time; after the first runs, systemctl status for each .service shows success
7. Delete the commented lines after a week of clean runs.