Skip to main content

Moving a cron job across midnight: fix the weekday gate too

entry

Date: 2026-09-20 Lane: hermes Source: observed on Zappo while moving kids-bedtime-lightsout from 20:00 to 00:00

The trap​

A job at 0 20 * * 0-4 ("Sun-Thu evenings") does not become 0 0 * * 0-4 when you move it to midnight. Cron weekdays name the day the job RUNS, not the evening it belongs to:

  • 0 0 * * 0-4 fires 00:00 Sunday, which is Saturday night -> wrong, fires on a non-school night.
  • It also never fires at 00:00 Friday, so Thursday night is skipped.

The correct expression for Sun-Thu nights at midnight is 0 0 * * 1-5: 00:00 Monday through Friday, which is the evening of Sunday through Thursday.

The second half: the script's own weekday check​

The script also gated itself with is_school_night() reading datetime.now().weekday(). That is correct at 20:00 and wrong at 00:00, because the weekday at 00:00 is the morning after. Fixing only the cron expression leaves the script rejecting Thursday night and accepting Friday night.

Fix: shift the anchor back across the midnight boundary before reading the weekday.

def is_school_night(now=None):
now = now or datetime.now()
anchor = now - timedelta(hours=5) # decide from the preceding evening
return anchor.weekday() in (0, 1, 2, 3, 6) # Mon-Thu + Sun evenings

Pass now in and the function becomes testable without freezing the clock process-wide.

Rules​

  • Moving a scheduled job across midnight changes which day it fires on. Re-derive the cron weekday set from the EVENING it belongs to, and re-check any in-script day/date gate.
  • Fix both halves or neither: the cron expression and the script's own gate must agree.
  • Verify by falsification. Write the weekday assertions, run them against the OLD function, and confirm they FAIL before trusting them. Here: 4 failures on old code, 16/16 pass on new.
  • A dry run at the wrong hour proves nothing about the real fire time. The dry run at 02:26 correctly printed "Not a school night (Fri/Sat) - skipping", which shows the gate works but says nothing about Monday 00:00. Freeze the clock and assert the HA calls instead.

Verify the action set, not just the gate​

For the lights-out phase, assert the calls made: 7 light.turn_off, ZERO light.turn_on (turn_on would be the wind-down dimming behaviour leaking into lights-out), and one telegram notice. A passing school-night gate with the wrong action set is still a broken job.

Result​

kids-bedtime-lightsout f700b9defbeb: 0 20 * * 0-4 -> 0 0 * * 1-5, resumed, next run 2026-09-21 00:00 AEST. kids-bedtime-winddown 2f46dc57b611 left paused at 30 19 * * 0-4 (its 19:30 time still needs the unshifted weekday, which the 5-hour anchor preserves). Script fix committed in the OpenClaw workspace as 1805e98.