OnCalendar= is systemd’s calendar syntax — more readable than cron and validatable. Here’s the daily case plus common variants.
Daily at 03:30
/etc/systemd/system/nightly.service:
[Unit]
Description=Nightly maintenance
[Service]
Type=oneshot
ExecStart=/usr/local/bin/nightly.sh
/etc/systemd/system/nightly.timer:
[Unit]
Description=Run nightly maintenance at 03:30
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true records the last run on disk; if the machine was off at 03:30, the job runs shortly after the next boot instead of being silently skipped.
Common OnCalendar values
OnCalendar=daily # 00:00 every day
OnCalendar=*-*-* 03:30:00 # 03:30 every day
OnCalendar=Mon *-*-* 09:00:00 # 09:00 every Monday
OnCalendar=Mon..Fri 18:00 # 18:00 on weekdays
OnCalendar=*-*-01 04:00:00 # 04:00 on the 1st of each month
OnCalendar=*-*-* *:00:00 # top of every hour
OnCalendar=hourly # same as above
The full form is DayOfWeek Year-Month-Day Hour:Minute:Second; * is “any”, and ranges (Mon..Fri), lists (Mon,Wed), and steps (0/5) are allowed.
Enable and verify
sudo systemctl daemon-reload
sudo systemctl enable --now nightly.timer
systemctl list-timers nightly.timer
# Test the expression without enabling anything:
systemd-analyze calendar 'Mon..Fri 18:00'
Timezone: OnCalendar uses the system local time by default; append a zone like OnCalendar=*-*-* 03:30 America/New_York, or set the unit’s Timezone=. Gotchas: enable the .timer. Use AccuracySec=1s if you need the job to fire exactly on time (systemd batches timers within a 1-minute window by default to save power).