TL;DR
-
timezoneを指定しないscheduleのcronはUTCで評価されます。JSTの平日9時のつもりで0 9 * * 1-5と書くと、UTCの9時として評価され、評価器の結果ではJST 18時の発火時刻になります。 -
timezone: "Asia/Tokyo"を指定するか、時刻と曜日の両方をUTCに換算します。JST平日6時をUTCで表す場合は、曜日も1つ戻した0 21 * * 0-4です。 - 正時を避ける公式案内とYAMLの読み込みにも注意します。換算後のUTC cronと
Asia/Tokyo相当の評価は、2026年の288通りで発火時刻の集合が一致しました。
日次で動くAIエージェントを、GitHub Actionsの schedule で定期実行する場面を考えます。「平日の朝に動かす」と決めても、cronの時刻をJSTのつもりで書くと、指定した時刻とは異なる時刻で評価されます。早朝の実行では、UTCに換算したときの曜日も確認が必要です。
この記事では、JSTの時刻を指定するときに押さえたい5つの落とし穴を、再現、原因、直し方、確認ログの順に見ていきます。発火時刻の確認には、公式ドキュメントの構文(5フィールドと演算子 * , - /)に沿って自作したcron評価器を使います。この記事の発火時刻はすべて、この評価器で計算した結果です。
実行環境
Python 3.13.5、PyYAML 6.0.3、Linuxを使用しました。cronの構文はGitHubの公式ドキュメントに沿って、手元の評価器で発火時刻を計算しました。
以下は、公式のcron構文に沿って自作した評価器です。演算子は *、,、-、/ のみ、曜日は 0〜6 を扱います。夏時間の繰り下げは扱いません(日本は夏時間がないためです)。
cron_next.py(全文)
from datetime import datetime, time, timedelta, timezone
from zoneinfo import ZoneInfo
def _parse_field(field: str, minimum: int, maximum: int) -> set[int]:
values: set[int] = set()
def number(text: str) -> int:
if not text.isascii() or not text.isdigit():
raise ValueError(f"不正な数値: {text!r}")
value = int(text)
if not minimum <= value <= maximum:
raise ValueError(f"範囲外の値: {value}")
return value
for part in field.split(","):
if not part:
raise ValueError("空のフィールド要素です")
pieces = part.split("/")
if len(pieces) > 2:
raise ValueError(f"不正な間隔: {part!r}")
base = pieces[0]
step = 1
if len(pieces) == 2:
if not pieces[1].isascii() or not pieces[1].isdigit():
raise ValueError(f"不正な間隔: {part!r}")
step = int(pieces[1])
if step < 1:
raise ValueError("間隔は1以上で指定してください")
if base == "*":
first, last = minimum, maximum
elif "-" in base:
bounds = base.split("-")
if len(bounds) != 2:
raise ValueError(f"不正な範囲: {base!r}")
first, last = number(bounds[0]), number(bounds[1])
if first > last:
raise ValueError(f"逆順の範囲: {base!r}")
else:
first = number(base)
last = maximum if len(pieces) == 2 else first
values.update(range(first, last + 1, step))
return values
def fires(
cron: str, tz: str, start: datetime, end: datetime
) -> list[datetime]:
fields = cron.split()
if len(fields) != 5:
raise ValueError("cronは5フィールドで指定してください")
if fields[2] != "*" and fields[4] != "*":
raise ValueError("日と曜日の両方を制限するcronは扱えません")
if (
start.tzinfo is None
or start.utcoffset() is None
or end.tzinfo is None
or end.utcoffset() is None
):
raise ValueError("開始時刻と終了時刻にはタイムゾーンが必要です")
start_utc = start.astimezone(timezone.utc)
end_utc = end.astimezone(timezone.utc)
if start_utc > end_utc:
raise ValueError("開始時刻は終了時刻以前にしてください")
if start_utc == end_utc:
return []
minutes = _parse_field(fields[0], 0, 59)
hours = _parse_field(fields[1], 0, 23)
days = _parse_field(fields[2], 1, 31)
months = _parse_field(fields[3], 1, 12)
weekdays = _parse_field(fields[4], 0, 6)
zone = ZoneInfo(tz)
clock_times = [(hour, minute) for hour in sorted(hours) for minute in sorted(minutes)]
# 時差の変化で端の日付を取りこぼさないよう、前後の日も調べる。
day = start_utc.astimezone(zone).date() - timedelta(days=1)
last_day = end_utc.astimezone(zone).date() + timedelta(days=1)
instants: dict[datetime, datetime] = {}
while day <= last_day:
cron_weekday = (day.weekday() + 1) % 7
if day.month in months and day.day in days and cron_weekday in weekdays:
for hour, minute in clock_times:
wall = datetime.combine(day, time(hour, minute))
for fold in (0, 1):
local = wall.replace(tzinfo=zone, fold=fold)
instant = local.astimezone(timezone.utc)
# 往復して壁時計が変わる時刻は、夏時間の切り替えで存在しない。
restored = instant.astimezone(zone)
if restored.replace(tzinfo=None) != wall:
continue
if start_utc <= instant < end_utc:
instants[instant] = restored
day += timedelta(days=1)
return [instants[instant] for instant in sorted(instants)]
この評価器を使い、cronとタイムゾーンから発火時刻を計算します。後半では、Asia/Tokyo 相当の評価結果と、UTCに換算したcronの評価結果を比較します。
全体像:JSTの指定から発火時刻まで
on.schedule のcronは、分、時、日、月、曜日の5フィールド形式です。timezone を省略した場合はUTCで評価されます。2026年3月19日(GitHub Changelog)から、各scheduleエントリに timezone を指定でき、指定したタイムゾーンの壁時計の時刻でcronを評価できます。
曜日は 0〜6 または SUN〜SAT で指定し、日曜が 0 です。使える演算子は *、,、-、/ です。@daily のような省略記法は使えず、最短間隔は5分です。
落とし穴1:JSTのつもりで書いたcronが9時間ずれる
再現
平日9時に実行するつもりで、cronを 0 9 * * 1-5 と書きます。5フィールドのうち、分は 0、時は 9、曜日は月曜から金曜を表す 1-5 です。
JSTの予定として読むと「平日9時」ですが、timezone を指定しなければUTCの9時として評価されます。評価器が返す発火時刻をJSTで見ると18時です。朝に動かしたかったエージェントが、同じ日の夕方に動く指定になっています。
次のスクリプトは、JSTのつもりで書いたcronと、JST平日9時に対応するUTC cronの発火時刻を比較します。
from datetime import datetime
from zoneinfo import ZoneInfo
from cron_next import fires
def show(label: str, cron: str) -> None:
jst = ZoneInfo("Asia/Tokyo")
start = datetime(2026, 11, 2, tzinfo=jst)
end = datetime(2026, 11, 9, tzinfo=jst)
print(label)
for instant in fires(cron, "UTC", start, end)[:3]:
print(instant.astimezone(jst).strftime("%Y-%m-%d %H:%M JST (%a)"))
if __name__ == "__main__":
show("JSTのつもりで指定したcronをUTCで評価:", "0 9 * * 1-5")
print()
show("JST平日09:00に対応するUTCのcron:", "0 0 * * 1-5")
両方のcronをUTCで評価し、結果をJSTで表示することで、時刻のずれと換算後の時刻を確認します。
原因
原因は、cronの 9 がJSTの9時ではなく、UTCの9時として評価されることです。timezone を省略したときはUTCが既定なので、利用者が想定したJSTの時刻はcron文字列だけでは伝わりません。
曜日の 1-5 は平日を表していますが、時刻の評価に使われるタイムゾーンも合わせて確認する必要があります。この例では9時間の差がそのまま表示され、JSTの18時になります。
直し方
直し方は2つあります。
- scheduleエントリに
timezone: "Asia/Tokyo"を指定し、cronを0 9 * * 1-5のまま使います。Asia/Tokyoの壁時計では平日9時です。 -
timezoneを省略する場合は、JST平日9時をUTCに換算し、0 0 * * 1-5とします。
ワークフローの on は次のように書きます。
on:
schedule:
# 方法1: timezone を指定する(JSTの壁時計で平日9時)
- cron: '0 9 * * 1-5'
timezone: "Asia/Tokyo"
# 方法2: timezone を省略してUTCに換算する(JST平日9時 = UTC平日0時)
# - cron: '0 0 * * 1-5'
どちらの場合も、まず「何曜日の何時をJSTで期待するか」を決めてから指定を確認します。cron_next.py の fires("0 9 * * 1-5", "Asia/Tokyo", ...) による評価が、換算後のUTC cronによる評価と一致することは、落とし穴5の verify_roundtrip.py で確認します。
確認ログ
次のログでは、JSTのつもりで指定した 0 9 * * 1-5 が18時、換算後のUTC cronが9時として表示されます。
$ python3 case1_misread.py
JSTのつもりで指定したcronをUTCで評価:
2026-11-02 18:00 JST (Mon)
2026-11-03 18:00 JST (Tue)
2026-11-04 18:00 JST (Wed)
JST平日09:00に対応するUTCのcron:
2026-11-02 09:00 JST (Mon)
2026-11-03 09:00 JST (Tue)
2026-11-04 09:00 JST (Wed)
2026年11月2日から4日までの3件はいずれも、換算前の指定が18時、換算後の指定が9時です。同じ「9」という数字でも、UTCの時刻として評価されるか、JSTの壁時計の時刻として評価されるかで結果が変わります。
落とし穴2:時刻だけ直して曜日を直さない
再現
JSTの平日6時をUTCに換算すると、時刻は前日の21時です。ここで時刻だけを直し、曜日を平日の 1-5 のままにして 0 21 * * 1-5 とすると、評価器の結果では、JSTの火曜から土曜の6時が発火時刻になり、期待する月曜6時は含まれません。
月曜6時のJSTは、UTCでは前日の日曜21時に当たります。一方、UTCの金曜21時は、JSTでは土曜6時です。時刻を21時に変えるだけでは、平日の範囲が意図したJSTの日付に揃いません。
次のスクリプトは、時刻も曜日も換算していない例、時刻だけ換算した例、時刻と曜日を換算した例を比較します。
from datetime import datetime
from zoneinfo import ZoneInfo
from cron_next import fires
if __name__ == "__main__":
jst = ZoneInfo("Asia/Tokyo")
start = datetime(2026, 11, 2, tzinfo=jst)
end = datetime(2026, 11, 9, tzinfo=jst)
cases = (
("(a) 0 6 * * 1-5", "0 6 * * 1-5"),
("(b) 0 21 * * 1-5", "0 21 * * 1-5"),
("(c) 0 21 * * 0-4", "0 21 * * 0-4"),
)
weekdays = "月火水木金土日"
by_case = []
for _, cron in cases:
by_date: dict[object, list[str]] = {}
for instant in fires(cron, "UTC", start, end):
local = instant.astimezone(jst)
by_date.setdefault(local.date(), []).append(
f"{weekdays[local.weekday()]}曜 {local:%H:%M}"
)
by_case.append(by_date)
print("各cronはUTCで評価し、発火時刻をJSTで表示します。")
print("JSTの日付 | (a) 0 6 * * 1-5 | (b) 0 21 * * 1-5 | (c) 0 21 * * 0-4")
print("-" * 72)
day = start.date()
while day < end.date():
cells = [
"、".join(case.get(day, [])) or "なし"
for case in by_case
]
print(f"{day} | {cells[0]:<16} | {cells[1]:<16} | {cells[2]}")
day = day.fromordinal(day.toordinal() + 1)
3つのcronはいずれもUTCで評価し、発火時刻をJSTの日付ごとに表示します。月曜と土曜の行を見ると、曜日を戻す必要が分かります。
原因
JSTの時刻が9時より前なら、UTCでは前日になります。そのため、UTCに換算する際は時刻だけでなく曜日も1つ戻します。
JST平日6時の場合、JSTの月曜から金曜に対応するUTCの曜日は、日曜から木曜です。日曜が 0 なので、UTCの曜日フィールドは 0-4 になります。
直し方
JST平日6時に対応するUTC cronは 0 21 * * 0-4 です。曜日を1つ戻したことで、UTCの日曜21時から木曜21時が、JSTの月曜6時から金曜6時に対応します。
次のスクリプトは、JSTの平日時刻をUTCのcronに換算します。9時より前と、9時以降の例を並べて確認します。
def jst_to_utc_cron(minute: int, hour: int, dow: set[int]) -> str:
if type(minute) is not int or not 0 <= minute <= 59:
raise ValueError("分は0から59の整数で指定してください")
if type(hour) is not int or not 0 <= hour <= 23:
raise ValueError("時は0から23の整数で指定してください")
if not dow or any(type(day) is not int or not 0 <= day <= 6 for day in dow):
raise ValueError("曜日は0から6の整数の空でない集合で指定してください")
utc_hour = (hour - 9) % 24
utc_days = sorted((day - (hour < 9)) % 7 for day in dow)
if len(utc_days) > 1 and utc_days == list(range(utc_days[0], utc_days[-1] + 1)):
weekday_field = f"{utc_days[0]}-{utc_days[-1]}"
else:
weekday_field = ",".join(str(day) for day in utc_days)
return f"{minute} {utc_hour} * * {weekday_field}"
if __name__ == "__main__":
weekdays = {1, 2, 3, 4, 5}
for hour, minute in ((9, 0), (6, 0), (8, 30), (9, 7)):
print(
f"JST 平日 {hour:02d}:{minute:02d} → "
f"UTC {jst_to_utc_cron(minute, hour, weekdays)}"
)
9時より前の6時と8時30分ではUTCの日付が前日になるため、出力の曜日は 0-4 です。9時と9時7分では同じ日のUTC時刻になるため、曜日は 1-5 のままです。
確認ログ
まず、3つのcronを同じ週で比較したログです。
$ python3 case2_dow_shift.py
各cronはUTCで評価し、発火時刻をJSTで表示します。
JSTの日付 | (a) 0 6 * * 1-5 | (b) 0 21 * * 1-5 | (c) 0 21 * * 0-4
------------------------------------------------------------------------
2026-11-02 | 月曜 15:00 | なし | 月曜 06:00
2026-11-03 | 火曜 15:00 | 火曜 06:00 | 火曜 06:00
2026-11-04 | 水曜 15:00 | 水曜 06:00 | 水曜 06:00
2026-11-05 | 木曜 15:00 | 木曜 06:00 | 木曜 06:00
2026-11-06 | 金曜 15:00 | 金曜 06:00 | 金曜 06:00
2026-11-07 | なし | 土曜 06:00 | なし
2026-11-08 | なし | なし | なし
時刻だけ直した 0 21 * * 1-5 は、月曜が「なし」で土曜に6時が表示されています。時刻と曜日を直した 0 21 * * 0-4 は、月曜から金曜まで6時で、土曜と日曜は「なし」です。
続いて、JSTの平日時刻をUTC cronに換算した結果です。
$ python3 jst_to_utc.py
JST 平日 09:00 → UTC 0 0 * * 1-5
JST 平日 06:00 → UTC 0 21 * * 0-4
JST 平日 08:30 → UTC 30 23 * * 0-4
JST 平日 09:07 → UTC 7 0 * * 1-5
例えば、JST平日8時30分は 30 23 * * 0-4 です。時刻は前日の23時30分となり、曜日も日曜から木曜に戻っています。JST平日9時7分は 7 0 * * 1-5 となり、曜日はそのままです。
落とし穴3:正時に実行を集中させる
再現
平日9時にエージェントを動かしたい場合、UTCに換算すると 0 0 * * 1-5 です。この指定は毎回、分が 0 の正時に当たります。
一方、処理の要件が「9時ちょうど」でなくてもよければ、9時7分という指定も考えられます。落とし穴2の換算結果では、JST平日9時7分はUTCの 7 0 * * 1-5 です。
原因
公式ドキュメントは、負荷が高いとき、特に毎時0分付近にはscheduleの実行が遅延することがあり、負荷が十分高いと一部のキュー済みジョブが破棄されることもあると説明しています。そのため、毎時の別の時刻にずらすよう案内しています。
直し方
9時ちょうどである必要がない仕事なら、例えばJSTの9時7分にずらします。timezone を省略してUTCで指定する場合、落とし穴2の換算結果を使うと 7 0 * * 1-5 です。
timezone: "Asia/Tokyo" を指定する場合は、JSTの壁時計の時刻として分を 7、時を 9 にします。いずれも、正時を避けるという公式の案内に沿った指定です。
確認ログ
落とし穴2の jst_to_utc.py のログでは、JST平日9時7分が 7 0 * * 1-5 と出力されています。分フィールドが 0 から 7 に変わり、UTCの正時を避ける指定です。
落とし穴4:cronと on のYAML読み込みでつまずく
再現
5分ごとのcronとして */5 * * * * を書くとき、クォートを付けないと、YAMLの読み込みでエラーになります。先頭の * がエイリアスとして解釈されるためです。cron文字列をクォートすると読み込めます。
もう1つ、PyYAMLでワークフローを読むと、on: が文字列の "on" ではなく、真偽値 True のキーになる例があります。その場合、doc.get("on") だけではscheduleを取得できません。
次のスクリプトは、on のキー、クォートの有無、cron と timezone、そして list_schedules によるscheduleの取得結果を確認します。
import yaml
def list_schedules(text: str) -> list[dict[str, object]]:
doc = yaml.safe_load(text)
if not isinstance(doc, dict):
return []
result = []
for key in ("on", True):
events = doc.get(key)
if not isinstance(events, dict):
continue
schedules = events.get("schedule", [])
if not isinstance(schedules, list):
continue
for schedule in schedules:
if isinstance(schedule, dict):
result.append(
{
"cron": schedule.get("cron"),
"timezone": schedule.get("timezone"),
}
)
return result
if __name__ == "__main__":
example_a = "on:\n schedule:\n - cron: '0 0 * * 1-5'"
example_c = (
"on:\n"
" schedule:\n"
" - cron: '0 0 * * 1-5'\n"
' timezone: "Asia/Tokyo"'
)
doc = yaml.safe_load(example_a)
print("(a) トップレベルのキー:", list(doc.keys()))
print('(a) doc.get("on"):', doc.get("on"))
print("(a) doc.get(True):", doc.get(True))
print()
try:
yaml.safe_load("cron: */5 * * * *")
except yaml.YAMLError as error:
print("(b) クォートなしのエラー:", str(error).splitlines()[0])
print("(b) クォートあり:", yaml.safe_load("cron: '*/5 * * * *'"))
print()
print("(c) cronとtimezone:", yaml.safe_load(example_c)[True]["schedule"][0])
print()
print("(d) (a)のスケジュール:", list_schedules(example_a))
print("(d) (c)のスケジュール:", list_schedules(example_c))
list_schedules は、ワークフローをスクリプトで読む際に、文字列の "on" に加えて True のキーも見ます。これにより、このログの例ではscheduleの一覧を取り出せます。
原因
クォートなしの */5 * * * * は、先頭の * がYAMLのエイリアスとして扱われ、読み込みエラーになります。cronとして意味を確認する前に、YAMLとして読み込める形にする必要があります。
また、PyYAMLではYAML 1.1の扱いにより、on: が真偽値 True のキーになる場合があります。文字列キーだけを探す doc.get("on") は、この例では None を返します。
直し方
*/5 * * * * のようなcron文字列はクォートします。これで、ログの例では cron の値を文字列として読み込めます。
PyYAMLでワークフローを読むスクリプトでは、doc.get("on") だけでなく True のキーも確認します。case4_yaml.py の list_schedules は、scheduleの一覧を取り出すときにその両方を見ます。
確認ログ
次のログは、トップレベルのキーと、クォートの有無による読み込み結果を示しています。
$ python3 case4_yaml.py
(a) トップレベルのキー: [True]
(a) doc.get("on"): None
(a) doc.get(True): {'schedule': [{'cron': '0 0 * * 1-5'}]}
(b) クォートなしのエラー: while scanning an alias
(b) クォートあり: {'cron': '*/5 * * * *'}
(c) cronとtimezone: {'cron': '0 0 * * 1-5', 'timezone': 'Asia/Tokyo'}
(d) (a)のスケジュール: [{'cron': '0 0 * * 1-5', 'timezone': None}]
(d) (c)のスケジュール: [{'cron': '0 0 * * 1-5', 'timezone': 'Asia/Tokyo'}]
doc.get("on") は None ですが、doc.get(True) ではscheduleを含む値を取得しています。また、クォートなしの */5 * * * * はエラーとなり、クォートありではcron文字列として読み込めています。
list_schedules の結果では、timezone のない例が None、Asia/Tokyo を指定した例がその文字列として表示されています。ワークフローを読む側でも、cronとタイムゾーンの両方を確認できます。
落とし穴5:換算したcronを発火時刻で検証しない
再現
JSTの時刻と曜日をUTCに換算するとき、時刻の引き算だけを見ていると、落とし穴2のように曜日のずれを見落とします。換算後のcronが意図したJSTの時刻に対応するかは、発火時刻を比較して確認します。
次のスクリプトは、UTCに換算したcronの発火時刻の集合と、timezone: Asia/Tokyo 相当の評価による発火時刻の集合を比較します。
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
from cron_next import fires
from jst_to_utc import jst_to_utc_cron
def main() -> int:
jst = ZoneInfo("Asia/Tokyo")
start = datetime(2026, 1, 1, tzinfo=jst)
end = datetime(2027, 1, 1, tzinfo=jst)
weekday_sets = (
{1, 2, 3, 4, 5},
{0},
{6},
{0, 6},
{1},
{0, 1, 2, 3, 4, 5, 6},
)
combinations = 0
matches = 0
for hour in range(24):
for minute in (0, 7):
for weekdays in weekday_sets:
combinations += 1
weekday_field = ",".join(map(str, sorted(weekdays)))
local_cron = f"{minute} {hour} * * {weekday_field}"
utc_cron = jst_to_utc_cron(minute, hour, weekdays)
local_instants = {
instant.astimezone(timezone.utc)
for instant in fires(local_cron, "Asia/Tokyo", start, end)
}
utc_instants = {
instant.astimezone(timezone.utc)
for instant in fires(utc_cron, "UTC", start, end)
}
if local_instants == utc_instants:
matches += 1
else:
print(f"不一致: JST {local_cron}、UTC {utc_cron}")
print("JST側のみ:", sorted(local_instants - utc_instants))
print("UTC側のみ:", sorted(utc_instants - local_instants))
print(f"組み合わせ数: {combinations}")
print(f"一致数: {matches}")
return 0 if matches == combinations else 1
if __name__ == "__main__":
raise SystemExit(main())
対象は、時が 0〜23、分が 0 と 7、曜日が6パターンの組み合わせです。2026年の1年間について、各組み合わせの発火時刻の集合が一致するかを確認します。
原因
JSTとUTCでは時刻の差により日付が変わる場合があります。時刻の数字だけを換算しても、曜日フィールドがJSTの意図に対応しているとは限りません。
timezone: "Asia/Tokyo" を指定する場合は、その壁時計の時刻でcronが評価されます。一方、timezone を省略する場合はUTCで評価されるため、JSTで表したかった時刻と曜日をUTC側の指定に反映する必要があります。
直し方
換算後のUTC cronについて、期待するタイムゾーンで評価した発火時刻と比較します。時刻と曜日の両方を含めて発火時刻の集合を比べることで、表示上のcron文字列だけでは見落としやすい日付のずれを確認できます。
この検証では、落とし穴1の fires("0 9 * * 1-5", "Asia/Tokyo", ...) のような評価と、対応するUTC cronの評価を比較します。検証範囲は、指定した時刻と曜日の288通り、2026年の1年間です。
確認ログ
次のログは、比較した組み合わせ数と、発火時刻の集合が一致した数です。
$ python3 verify_roundtrip.py
組み合わせ数: 288
一致数: 288
288通りすべてで一致しました。この結果は、検証した2026年の範囲と288通りの組み合わせについて、換算後のUTC cronと Asia/Tokyo 相当の評価が同じ発火時刻の集合を返したことを示します。
なお、timezone に夏時間のあるタイムゾーンを指定した場合、夏時間によってスキップされる時刻の実行は、次の有効な時刻に繰り下がります。日本には夏時間がありません。
ここまでの5つのスクリプトは、次の run_all.sh でまとめて実行し、run.log に保存しました。
#!/bin/bash
# 各スクリプトを順に実行し、コマンドと出力を run.log に残す
cd "$(dirname "$0")" || exit 1
for f in jst_to_utc.py case1_misread.py case2_dow_shift.py verify_roundtrip.py case4_yaml.py; do
echo "\$ python3 $f"
python3 "$f" 2>&1
echo
done
運用メモ
公式ドキュメントには、発火時刻以外にもscheduleの運用に関わる事項があります。
- scheduleワークフローは、既定ブランチの最新コミットでのみ動きます。
- 公開リポジトリでは、60日間リポジトリの活動がないとscheduleワークフローが自動で無効化されます。
-
github.event.scheduleには、その回を起動したcron文字列が入ります。複数のcronを設定したとき、その値を分岐に使えます。
まとめ
JSTで定期実行したい場合は、timezone: "Asia/Tokyo" を指定する方法と、UTCのcronに換算する方法があります。UTCに換算する場合、JSTの時刻が9時より前なら前日になるため、曜日も1つ戻します。
また、正時を避ける公式案内を確認し、YAMLをスクリプトで読む場合はcronのクォートと on キーの扱いに注意します。今回の2026年・288通りの検証では、換算後のUTC cronと Asia/Tokyo 相当の評価で、発火時刻の集合がすべて一致しました。