This is an automated email from the ASF dual-hosted git repository.
ash pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 191eca02c13 Make start_date optional for @continuous schedule (#61405)
191eca02c13 is described below
commit 191eca02c136d0b1330ff2cf60789b0beb47dbfc
Author: Ash Berlin-Taylor <[email protected]>
AuthorDate: Tue Mar 10 16:30:34 2026 +0000
Make start_date optional for @continuous schedule (#61405)
When start_date is not specified, continuous DAGs will begin running
immediately when unpaused, using the current time as the starting point.
Previously if you didn't have a start date (the default in 3.0) it would do
nothing when it was unpaused.
This aligns with Airflow 3.0's philosophy of making start_date optional
for schedules that don't require historical backfilling.
---
.../continuous-optional-start-date.improvement.rst | 1 +
airflow-core/src/airflow/timetables/simple.py | 10 ++++------
.../unit/timetables/test_continuous_timetable.py | 20 ++++++++++++++++++--
3 files changed, 23 insertions(+), 8 deletions(-)
diff --git
a/airflow-core/newsfragments/continuous-optional-start-date.improvement.rst
b/airflow-core/newsfragments/continuous-optional-start-date.improvement.rst
new file mode 100644
index 00000000000..c001c620933
--- /dev/null
+++ b/airflow-core/newsfragments/continuous-optional-start-date.improvement.rst
@@ -0,0 +1 @@
+The ``schedule="@continuous"`` parameter now works without requiring a
``start_date``, and any DAGs with this schedule will begin running immediately
when unpaused.
diff --git a/airflow-core/src/airflow/timetables/simple.py
b/airflow-core/src/airflow/timetables/simple.py
index 082cf90e56d..01fb12f81dd 100644
--- a/airflow-core/src/airflow/timetables/simple.py
+++ b/airflow-core/src/airflow/timetables/simple.py
@@ -161,14 +161,12 @@ class ContinuousTimetable(_TrivialTimetable):
last_automated_data_interval: DataInterval | None,
restriction: TimeRestriction,
) -> DagRunInfo | None:
- if restriction.earliest is None: # No start date, won't run.
- return None
-
current_time = timezone.coerce_datetime(timezone.utcnow())
+ start_date = restriction.earliest or current_time
if last_automated_data_interval is not None: # has already run once
if last_automated_data_interval.end > current_time: # start date
is future
- start = restriction.earliest
+ start = start_date
elapsed = last_automated_data_interval.end -
last_automated_data_interval.start
end = start + elapsed.as_timedelta()
@@ -176,8 +174,8 @@ class ContinuousTimetable(_TrivialTimetable):
start = last_automated_data_interval.end
end = current_time
else: # first run
- start = restriction.earliest
- end = max(restriction.earliest, current_time)
+ start = start_date
+ end = max(start_date, current_time)
if restriction.latest is not None and end > restriction.latest:
return None
diff --git a/airflow-core/tests/unit/timetables/test_continuous_timetable.py
b/airflow-core/tests/unit/timetables/test_continuous_timetable.py
index 73a4be5ea48..03babcea638 100644
--- a/airflow-core/tests/unit/timetables/test_continuous_timetable.py
+++ b/airflow-core/tests/unit/timetables/test_continuous_timetable.py
@@ -41,12 +41,28 @@ def timetable():
return ContinuousTimetable()
-def test_no_runs_without_start_date(timetable):
+@time_machine.travel(DURING_DATE)
+def test_runs_without_start_date(timetable):
next_info = timetable.next_dagrun_info(
last_automated_data_interval=None,
restriction=TimeRestriction(earliest=None, latest=None, catchup=False),
)
- assert next_info is None
+ assert next_info is not None
+ assert next_info.run_after == DURING_DATE
+ assert next_info.data_interval.start == DURING_DATE
+ assert next_info.data_interval.end == DURING_DATE
+
+
+@time_machine.travel(AFTER_DATE)
+def test_subsequent_runs_without_start_date(timetable):
+ next_info = timetable.next_dagrun_info(
+ last_automated_data_interval=DataInterval(DURING_DATE, DURING_DATE),
+ restriction=TimeRestriction(earliest=None, latest=None, catchup=False),
+ )
+ assert next_info is not None
+ assert next_info.run_after == AFTER_DATE
+ assert next_info.data_interval.start == DURING_DATE
+ assert next_info.data_interval.end == AFTER_DATE
@time_machine.travel(DURING_DATE)