Mehdi Lallouache
03/13/2026, 3:56 PMMarvin
03/13/2026, 3:56 PMMarvin
03/13/2026, 3:59 PMtimezone that applies to all of its timing logic. You can’t define a schedule window with a start in one timezone and an end in another. There are no `active_from`/`active_until` fields with per-window timezones either; any start/end you see are used as query filters when generating dates/runs, and they’re normalized into the schedule’s single timezone.
Workarounds:
- Create multiple schedules on the same deployment, each with its own timezone
- Or create multiple deployments, one per timezone
Example using one deployment with two schedules in different timezones:
from prefect import flow
from prefect.schedules import Cron
@flow
def my_flow():
print("Hello from a multi-timezone schedule")
if __name__ == "__main__":
my_flow.serve(
name="multi-tz-example",
schedules=[
Cron(cron="0 8 * * *", timezone="America/New_York", slug="ny-8am"),
Cron(cron="0 8 * * *", timezone="Europe/London", slug="ldn-8am"),
],
)
If you can share the behavior you’re aiming for (e.g., “run between 09:00 in TZ A and 17:00 in TZ B”), I can suggest the cleanest pattern to model it with multiple schedules or parameters.Mehdi Lallouache
03/13/2026, 4:01 PMMarvin
03/13/2026, 4:01 PMMarvin
03/13/2026, 4:06 PMfrom datetime import datetime, time, timedelta, timezone
from zoneinfo import ZoneInfo
from prefect import flow
from prefect.logging import get_run_logger
from prefect.schedules import Interval
PARIS_TZ = ZoneInfo("Europe/Paris")
NY_TZ = ZoneInfo("America/New_York")
START_TOD = time(8, 50) # 08:50 Paris
END_TOD = time(16, 0) # 16:00 New York
def current_window_bounds(now_utc: datetime | None = None):
if now_utc is None:
now_utc = datetime.now(timezone.utc)
# Find the last Paris 08:50 at or before 'now'
now_paris = now_utc.astimezone(PARIS_TZ)
start_paris_date = now_paris.date()
if now_paris.time() < START_TOD:
start_paris_date = start_paris_date.fromordinal(start_paris_date.toordinal() - 1)
start_local = datetime.combine(start_paris_date, START_TOD, tzinfo=PARIS_TZ)
start_utc = start_local.astimezone(timezone.utc)
# Find the first New York 16:00 strictly after 'start_utc'
start_in_ny = start_utc.astimezone(NY_TZ)
end_ny_date = start_in_ny.date()
if start_in_ny.time() >= END_TOD:
end_ny_date = end_ny_date.fromordinal(end_ny_date.toordinal() + 1)
end_local = datetime.combine(end_ny_date, END_TOD, tzinfo=NY_TZ)
end_utc = end_local.astimezone(timezone.utc)
return start_utc, end_utc
def in_window(now_utc: datetime | None = None) -> bool:
now_utc = now_utc or datetime.now(timezone.utc)
start_utc, end_utc = current_window_bounds(now_utc)
return start_utc <= now_utc < end_utc
@flow
def my_flow():
logger = get_run_logger()
if not in_window():
<http://logger.info|logger.info>("Outside window (08:50 Paris → 16:00 New York); exiting.")
return
<http://logger.info|logger.info>("Inside window; do work here.")
# your tasks go here
if __name__ == "__main__":
# Run every 10 minutes, but only do work inside the window
my_flow.serve(
name="windowed",
schedules=[Interval(interval=timedelta(minutes=10), timezone="UTC")],
)
If instead you only want runs at those two times (no window), you can add two schedules to the same deployment:
from prefect.schedules import Cron
my_flow.serve(
name="two-daily-triggers",
schedules=[
Cron("50 8 * * *", timezone="Europe/Paris", slug="paris-08-50"),
Cron("0 16 * * *", timezone="America/New_York", slug="ny-16-00"),
],
)
Tell me which pattern matches your intent, and I can tailor it further (e.g., cadence inside the window, handling weekends, etc.).Mehdi Lallouache
03/13/2026, 4:08 PMMarvin
03/13/2026, 4:08 PMMarvin
03/13/2026, 4:10 PMfrom datetime import datetime, time, timedelta, timezone
from zoneinfo import ZoneInfo
from prefect import flow
from prefect.logging import get_run_logger
PARIS_TZ = ZoneInfo("Europe/Paris")
NY_TZ = ZoneInfo("America/New_York")
START_TOD = time(8, 50) # 08:50 Paris
END_TOD = time(16, 0) # 16:00 New York
def current_window_bounds(now_utc: datetime | None = None):
now_utc = now_utc or datetime.now(timezone.utc)
# Start = most recent Paris 08:50 at or before now
now_paris = now_utc.astimezone(PARIS_TZ)
start_date_paris = now_paris.date()
start_today_paris = datetime.combine(start_date_paris, START_TOD, tzinfo=PARIS_TZ)
if now_paris < start_today_paris:
start_date_paris = start_date_paris - timedelta(days=1)
start_local = datetime.combine(start_date_paris, START_TOD, tzinfo=PARIS_TZ)
start_utc = start_local.astimezone(timezone.utc)
# End = first New York 16:00 strictly after start
start_in_ny = start_utc.astimezone(NY_TZ)
end_date_ny = start_in_ny.date()
end_today_ny = datetime.combine(end_date_ny, END_TOD, tzinfo=NY_TZ)
if start_in_ny >= end_today_ny:
end_date_ny = end_date_ny + timedelta(days=1)
end_local = datetime.combine(end_date_ny, END_TOD, tzinfo=NY_TZ)
end_utc = end_local.astimezone(timezone.utc)
return start_utc, end_utc
def in_window(now_utc: datetime | None = None) -> bool:
now_utc = now_utc or datetime.now(timezone.utc)
start_utc, end_utc = current_window_bounds(now_utc)
return start_utc <= now_utc < end_utc
@flow
def my_flow():
logger = get_run_logger()
if not in_window():
<http://logger.info|logger.info>("Outside window (08:50 Paris → 16:00 New York); exiting.")
return
<http://logger.info|logger.info>("Inside window; do work here.")
# Your work here
if __name__ == "__main__":
# One run per minute; the flow no-ops outside the window
from datetime import timedelta
my_flow.serve(
name="minute-window-paris-to-ny",
interval=timedelta(minutes=1),
# Optional: prevent overlaps if runs may exceed 1 minute
# global_limit=1,
)
Notes:
- This correctly handles DST for both Europe/Paris and America/New_York.
- It will still create a Prefect run every minute all day; runs outside the window just no-op quickly.
- If you need zero runs outside the window (not even no-ops), you’ll need an external scheduler or small helper flow to pause/resume the deployment at those local times via API. I can share that pattern if you prefer.