はじめに——引っ越さなくていい人
昨日の 1.0 宣言から来た方、はじめまして。この記事は crontab を持っている人向けの、いちばん実務寄りの入門である。
最初に断っておく。固定時刻の単純反復なら、cron で足りる。「毎時 0 分」「毎日 3 時」をKairos に書き換えるメリットはない。引っ越しを考えるのは、次のどれかに当たったときである——「月末」が書けない。「営業日」が書けない。サーバを移したら実行時刻がずれた。止まっていた間に何が飛んだか分からない。
以下、crontab の定番を 5 つ、順に翻訳する。
定番 1: 0 9 * * 1-5——平日 9 時
cron のこの行には、書かれていないことが二つある。どこの 9 時か(サーバの OS 設定任せ——移行事故の定番)と、祝日も動いてしまうこと(cron に祝日という概念はない)。Kairos ではどちらも定義が持つ:
premise JP {
calendar-system: Gregorian; tz: "Asia/Tokyo"; wkst: Mon
national = [2026-09-21, 2026-09-22, 2026-09-23, 2026-10-12, 2026-11-03, 2026-11-23] covering: 2026-08-01..2027-01-31
satSun = everyDay |> filter(d => weekday(d) == Sat or weekday(d) == Sun)
bizDay = everyDay \ (satSun | national)
}
@JP
bizDay |> at(T09:00)
シルバーウィークを挟む 2 週間(9/18〜9/28)で実測すると:
2026-09-18T09:00 2026-09-24T09:00 2026-09-25T09:00 2026-09-28T09:00
9/21〜23(祝日)と土日が正しく休んでいる。「東京の 9 時」は定義のテキストに書いてあるので、サーバをどこへ移しても変わらない。前文(premise)の書き方は連載のpremise 入門に詳しい。以降の例はすべてこの前文を使い回す。
定番 2: 55 23 28-31 * *——月末 23:55
cron に「月末」は無いので、28〜31 日に毎晩起こして「明日は 1 日か?」を判定するスクリプトを先頭に置くのが定番ハックである。Kairos では判定ごと式に消える:
monthEnd |> at(T23:55) #=> 2026-09-30T23:55 2026-10-31T23:55 2026-11-30T23:55 2026-12-31T23:55
定番 3: 給料日 25 日・土日祝なら前営業日——cron では書けない側
ここからが引っ越しの本命である。cron はこの要件の後半が書けない:
everyDay |> within(month) |> nth(25) |> roll(Preceding, on: bizDay) #=> 2026-09-25 2026-10-23 2026-11-25 2026-12-25
10 月だけ 23 日なのは、10/25 が日曜だから。roll(寄せる)と shift(数える)の使い分けは前々回の基礎回で書いた。
定番 4: 月末最終営業日
bizDay |> within(month) |> last #=> 2026-09-30 2026-10-30 2026-11-30 2026-12-31
10 月末(10/31 土)が金曜の 10/30 へ退いている。「営業日の列を作り、月ごとに最後の点を取る」——読んだままの構造である。
定番 5: 月末 3 営業日前
この連載の題になった式。祝日を挟んでも営業日の目盛りで正しく数える:
monthEnd |> roll(Preceding, on: bizDay) |> shift(-3, unit: bizDay)
運用の型——crontab に無かった三つの口
翻訳した定義は 1 ファイルに保存して、CLI で聞ける。
次はいつ動く?
$ kairos next -n 3 --from 2026-09-14 payday.kairos 2026-09-25 2026-10-23 2026-11-25
止まっていた間に何が飛んだ? サーバが 10/20〜10/31 に落ちていたとする:
$ kairos list --from 2026-10-20 --to 2026-11-01 payday.kairos 2026-10-23
10/23 の給料日振込バッチを撃ち漏らしていたことが、列挙で分かる(cron にこの口は無い——撃ち漏らしは列挙できる)。なお --from/--to は半開区間(--to の日は含まない)。この記法を選ぶ理由は第 13 弾で書いた。
じゃあ、どう動かす?——crontab は 1 行だけ残す
Kairos が答えるのは「いつ動くべきか」の集合までで、起動・リトライ・記録は実行系(systemd・ジョブランナー等)の仕事である——この分業は仕様に明文がある(spec §7.8)。ただ、それだけ言われても困るので、接続の型を 2 つ書いておく。どちらも今の CLI でそのまま動く(--json を付けると epochミリ秒込みの機械可読形で出る)。
型 1: cron を時計として残す。 crontab の行は 1 本だけ残し、判定を Kairos に移す。毎朝 9 時に「今日は点があるか」を聞いて、あれば起動する:
0 9 * * * cd /srv/batch && ./run-if-today.sh payday.kairos ./payday.sh
#!/bin/sh # run-if-today.sh <定義.kairos> <ジョブ> —— 今日 [今日, 明日) に点があればジョブを exec する today=$(date +%F); tomorrow=$(date -d "$today + 1 day" +%F) n=$(kairos list --from "$today" --to "$tomorrow" --json "$1" | jq '.results[0].dates | length') [ "$n" -gt 0 ] && exec "$2"
祝日も tz も判定は定義側にあるので、crontab には曜日も日付も書かない。人間向けの出力には被覆サマリの# 行が混ざるため、判定は --json で数える(上の payday.kairos なら 9/14 は 0 件・9/25 は 1 件)。
型 2: 次の 1 点を予約する。 時刻も定義側に置き(nth(25) |> roll(Preceding, on: bizDay) |> at(T09:00))、next --json で次の発火を取る。壁時計と epoch ミリ秒の両方で返るので、OS の一回限りのタイマーに渡し、ジョブの最後で次を登録し直す:
t=$(kairos next --json payday.kairos | jq -r '.results[0].dates[0]') # 2026-09-25T09:00
systemd-run --user --on-calendar="${t/T/ }:00" ./payday.sh
# Linux の at、Windows の schtasks /sc once でも同じ形
「点の列を定期的に取り直す」のが仕様の言う運用(§7.8)で、この 20〜40 行の型は別の回でまとめて書く。
データの話を最後に
例では祝日 6 個を定義に直書きした。実運用では祝日表は外部から供給する口(external)があり、式は静的・データは実行時という分業になる。どちらの場合もcovering:(この表はいつまで確認済みか)が必須で、表が尽きた先の結果には注記が付く——祝日テーブルは黙って腐るへの言語側の答えである。
まとめ——翻訳表
0 9 * * 1-5→bizDay |> at(T09:00)(tz と祝日が定義に入る)55 23 28-31 * *+判定 →monthEnd |> at(T23:55)- 給料日・前営業日(cron 不可) →
nth(25) |> roll(Preceding, on: bizDay) - 月末最終営業日(cron 不可) →
bizDay |> within(month) |> last - 月末 3 営業日前(cron 不可) →
monthEnd |> roll(…) |> shift(-3, unit: bizDay)
▶ Playground で 3 式を実行する——祝日を足したり、窓を動かしたりして試せる。ブラウザ内で完結し、何も送信しない。
Kairos はスケジュール定義言語です。仕様と参照実装はGitHub で公開しています。

コメント