Repository: apache/airflow

Issue:
Configuration setting to make CronTriggerTimetable default
### Description

Add a configuration setting under the scheduler section that changes the timetable used when passing a bare cron string to `schedule` from CronDataIntervalTimetable to CronTriggerTimetable.

### Use case/motivation

Airflow's CronDataIntervalTimetable behavior is unintuitive to many users of Airflow who do not really need a concept of data_interval and who expect behavior more like cron itself. Airflow provides the CronTriggerTimetable now, which would be a suitable choice for these users. However, most Airflow users are not accustomed to using alternative timetables, and I think many organizations would want to make this change across the board if they knew it existed. I posit that odd cron behavior contributes to a feeling that Airflow is unintuitive, and this simple change could help that.

On backwards compatibility, we'd need to make the current behavior the default setting. 

### Related issues

_No response_

### Are you willing to submit a PR?

- [X] Yes I am willing to submit a PR!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)



Discussion:
I like it and agree that this is unintuitive and not great behavior, particularly for learners.

If trying to come up with downsides, one might be that until airflow 3, it will make documentation more slightly more difficult because we'll have to keep the old docs with the confusing behavior and explanations, but then also explain this other option that you can only get to after changing the config. This would limit its ability to reduce the "airflow is unintuitive" perception, because it essentially adds more complexity to explain.  LIke wherever we explain what "execution_date" is we won't be able any longer to say "yes, it's confusing, but it's essentially the start of the prior interval" -- now we'll have to say well, what it means depends on this config setting.  I'm not sure how to mitigate that.

But I still think it's probably good to enable the more intuitive scheduling behavior.
We do already have a doc section discussing the difference between these two [here](https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/timetable.html#differences-between-the-two-cron-timetables). We could link to it in the description of this new setting.
Love this idea! If backwards compatibility is implemented as described can see no downside
I agree this could reduce confusion for new users. Even though having docs explaining both and having to change a config for now to get the simpler behavior is a bit complicated, I still think it's worth it to have the option. We can frame the docs to present this option "first" so that at least there's a "if you need this, change this config and stop reading" option for newer users.

Resolving patch:
diff --git a/airflow/models/dag.py b/airflow/models/dag.py
--- a/airflow/models/dag.py
+++ b/airflow/models/dag.py
@@ -116,6 +116,7 @@
     NullTimetable,
     OnceTimetable,
 )
+from airflow.timetables.trigger import CronTriggerTimetable
 from airflow.utils import timezone
 from airflow.utils.dag_cycle_tester import check_cycle
 from airflow.utils.dates import cron_presets, date_range as utils_date_range
@@ -226,8 +227,11 @@ def create_timetable(interval: ScheduleIntervalArg, timezone: Timezone) -> Timet
     if isinstance(interval, (timedelta, relativedelta)):
         return DeltaDataIntervalTimetable(interval)
     if isinstance(interval, str):
-        return CronDataIntervalTimetable(interval, timezone)
-    raise ValueError(f"{interval!r} is not a valid interval.")
+        if airflow_conf.getboolean("scheduler", "create_cron_data_intervals"):
+            return CronDataIntervalTimetable(interval, timezone)
+        else:
+            return CronTriggerTimetable(interval, timezone=timezone)
+    raise ValueError(f"{interval!r} is not a valid schedule_interval.")
 
 
 def get_last_dagrun(dag_id, session, include_externally_triggered=False):


Test patch: