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)

Reply via email to