What Airflow 3 cron schedules do and when teams need to review them
If your team runs scheduled DAGs on Airflow 2 and is planning the move to Airflow 3, the key change is that bare cron strings now use CronTriggerTimetable by default, instead of the data interval behavior most Airflow 2 teams know. That switch changes when runs fire and what label they carry in the UI and API, so the decision belongs before the upgrade, not after the first run times shift. Airflow is licensed under Apache-2.0 and has no paid edition from the project, so the cost of this review is your own engineering time or the effort of a partner you hire. There is no license fee to budget for, but there is a real need to plan the work.
The two timetables differ in how they interpret a schedule. CronTriggerTimetable fires at the cron time and labels the run with that moment, while CronDataIntervalTimetable runs once the data interval a cron expression describes has closed, which is what most Airflow 2 pipelines assume. The Airflow 3 defaults are catchup_by_default set to False and create_cron_data_intervals set to False. If you want to keep the Airflow 2 behavior, set create_cron_data_intervals to True before upgrading. Changing it after the upgrade skips one scheduled run, so do not treat it as a tuning step to make later.
Review every DAG that uses a cron schedule and confirm which timetable it resolves to, along with its start_date. Dynamic values such as datetime.now() are discouraged in the Airflow FAQ, and the first run is created after the first complete data interval following start_date, so fixed dates matter. Also check any downstream code that assumes a manual run's data interval equals the logical_date you supplied, since the upgrade guide advises using logical_date when a user-specified date is needed. Confirm each point against the Upgrading to Airflow 3 guide and your own test runs in staging.
Which timetable fits which pipeline
This table compares the bare cron string default, CronTriggerTimetable and CronDataIntervalTimetable on run timing, logical date meaning, catchup behavior and upgrade risk. Read each row as a question about your pipeline, then check the column for the timetable that answers it. The main takeaway is that the choice decides when runs fire and what the logical date means, so make it before the upgrade rather than after.
| Pipeline need | Timetable and behavior | Upgrade and catchup risk |
|---|---|---|
| Bare cron string (default in Airflow 3) | Uses CronTriggerTimetable when create_cron_data_intervals is False, the Airflow 3 default. Runs fire at the cron time with no data interval attached. | Changing create_cron_data_intervals after the upgrade skips one scheduled run. Confirm the setting in your config before the first Airflow 3 scheduler start. |
| CronTriggerTimetable | Fires at each cron tick. The logical date is the trigger time, so there is no closed window of data to process. Suits jobs that act on the current moment. | Catchup is off by default in Airflow 3, so missed ticks are not backfilled unless you enable it. Check that DAG code does not assume a previous window exists. |
| CronDataIntervalTimetable | Runs after each complete data interval closes, so the run processes a defined window. The logical date marks the start of that window. Suits jobs that load finished periods. | Keeping this timetable across the upgrade requires create_cron_data_intervals set to True before upgrading. Otherwise the timing shift skips a run. |
| Downstream loads that need complete data intervals | CronDataIntervalTimetable fits, because each run waits until its window has fully closed before it starts processing. | Confirm upstream systems finish late-arriving data inside the window. Missing data can still reach a closed window if sources land after the interval ends. |
| Event-like scheduling needs | CronTriggerTimetable fits best among cron options, because runs react to the clock rather than to a closed window. For message-driven starts, consider asset or event triggers instead. | Do not rely on cron ticks for work that should start when data arrives. A tick can run before the data exists, so verify readiness inside the task. |
| Manual runs | Manual runs use the logical_date you supply, which may not equal the data interval start. Use logical_date in code when a user-chosen date matters. | Do not assume a manual run's data interval matches the supplied date. Test manual triggers against both timetables before cutover. |
| Start_date handling | Set a fixed start_date. The first run follows the first complete data interval after start_date, so a dynamic value such as datetime.now() shifts scheduling on every parse. | Dynamic start_date values make run creation unpredictable. Review all DAGs for them before changing timetables. |
How catchup and start_date decide the first run
A DAG's first run depends on three settings working together: whether catchup is on, the start_date value, and the data interval the schedule defines. Read these cards before you deploy a new DAG or move an existing schedule onto Airflow 3, because each one changes which runs Airflow creates and when.
Catchup is off by default
In Airflow 3, catchup_by_default is False, so a DAG with a bare cron string does not create runs for every missed interval between its start_date and today. Airflow creates only the most recent eligible run. Teams moving from Airflow 2 often expect the old backfill-on-deploy behavior, so decide per DAG whether missed history should run. If it should, set catchup=True on that DAG explicitly and confirm the downstream load is acceptable before the first deploy.
start_date anchors the schedule
The start_date sets the beginning of the schedule. Per the Airflow FAQ, the first run is created after the first complete data interval following start_date, so a daily DAG with a start_date of January 1 does not produce a run for January 1 until that day's interval has closed. Review every start_date during code review and confirm with the data owner which interval the first run should represent.
Dynamic start dates cause drift
A start_date computed at parse time, such as datetime.now(), moves every time the DAG file is parsed. The scheduler can then compute different windows on different parses, which makes run creation hard to reason about and can skip or duplicate intervals. Use a fixed date in the DAG file, and make it a reviewed code change. Add a lint or CI check that rejects calls to the current time in DAG arguments.
Catchup changes skip one run
The upgrade guide notes that setting catchup to True before upgrading keeps CronDataIntervalTimetable for bare cron strings, while changing the setting after the upgrade skips one scheduled run. Plan the change during a maintenance window, record the last successful run for each DAG, and compare the first scheduled run after cutover against the expected logical date. Treat this as a timing change for downstream consumers, not only a configuration edit.
Why manual runs need logical_date, not the data interval assumption
The upgrade guide warns against assuming that a manual run's data interval equals the logical_date you supply when you trigger a DAG. Teams often pass a date to a manual run and expect the task to process exactly that day's data, but the interval Airflow computes for a manual run is not guaranteed to line up with that value. The guidance is to read logical_date inside the DAG whenever the business meaning of the date matters, and to stop deriving that date from data_interval_start or data_interval_end for manual runs. This applies to any DAG that uses a cron schedule, because those are the schedules where a reader is most likely to map timing to a calendar day.
The practical effect shows up in three places. Reruns of a cleared task keep the original run's dates, so a fix that depends on the wrong date will reprocess the wrong partition. Backfill-style manual triggers, where an operator starts a run for a past date to fill a gap, depend on whether the date parameter is read from logical_date or derived from the interval, and the two can produce different partitions. Downstream parameters, such as a file path, table partition or API query that a DAG passes to another system, inherit whichever date the DAG chose, so a mismatch can spread into systems outside Airflow.
In staging, trigger a manual run with an explicit logical date, then confirm the partition, file or query written by each task matches that date and not the interval boundaries. Repeat the test with a clear and rerun of one task, and with a run that starts during a timetable transition if you plan to change the setting for create_cron_data_intervals. Check the Airflow upgrade guide for the current wording before you finalize the DAG code, and record the results so your change board can see which dates each run used.
How to choose and verify a timetable before upgrading
This sequence turns an unplanned upgrade into a documented decision: every cron-scheduled DAG gets a recorded timetable, a deliberate catchup setting, and a verified next run before Airflow 3 changes how those runs are timed.
Inventory every cron-scheduled DAG
The platform team, with DAG owners, lists every DAG that uses a cron string, a preset, or a timetable object in its schedule argument, including DAGs generated from configuration or loops. Search the DAG folder for schedule arguments and compare the result with the metadata database so nothing is missed. The step is done when the list accounts for every active DAG and each entry names an owner. Confirm the count with the DAG owners, since dynamically generated DAGs often do not appear in a simple text search.
Record current catchup and interval behavior
For each DAG on the list, the engineer records the current catchup value, the start_date, the schedule expression, and whether the DAG relies on data intervals in its templates, such as data_interval_start or data_interval_end. Note any DAG that sets catchup explicitly and any that depends on the default. The output is a baseline table that shows what each DAG does today, so later differences can be traced to a specific setting. Check the baseline against the last several runs in the UI to confirm that the recorded behavior matches what actually happened.
Decide the target timetable for each DAG
The DAG owner and platform lead decide, per DAG, whether the schedule should keep the data interval model (CronDataIntervalTimetable) or move to the trigger-based model (CronTriggerTimetable). Keep the data interval model where downstream logic reads interval boundaries, and document the reason for each choice. Also decide whether catchup should stay off, which is the Airflow 3 default, or be enabled for a specific backfill need. The step closes with a signed-off decision record that names each DAG and its timetable, reviewed by the people who own the downstream consumers.
Set create_cron_data_intervals deliberately before upgrading
Before the upgrade, the platform team sets the create_cron_data_intervals option in the Airflow 2 configuration so that bare cron strings keep the timetable the DAGs were written against, and sets catchup explicitly on every DAG where its behavior matters. Per the upgrade guide, changing this setting after the upgrade skips one scheduled run, so the change belongs in the pre-upgrade configuration. The output is a reviewed configuration change tracked in version control, and a test run in Airflow 2 confirms that scheduling still behaves as the baseline recorded.
Run DAG code through Ruff AIR301 and AIR302 checks
The engineering team runs Ruff with the AIR301 and AIR302 rules across the DAG repository to find code that will break or change behavior under Airflow 3, such as removed imports and deprecated arguments. Fix the flagged lines in a branch and rerun the check until it reports no findings for those rules, then add the check to CI so that new DAGs are covered. The result is a clean lint report stored with the upgrade record. Also confirm that the environment is on Airflow 2.7 or later, since the upgrade guide requires that minimum, and that the latest 2.x release is installed first.
Verify next run times and logical dates in staging
Deploy the same DAG repository and configuration to a staging environment on Airflow 3.3.2 or the exact version you plan to run, and let the scheduler compute upcoming runs. Compare the next run time and first logical date for each DAG with the baseline and the decision record from earlier steps. Trigger a few manual runs and check that the logical date shown matches the value your code uses, because the upgrade guide warns against assuming a manual run's data interval equals the supplied logical_date. Also confirm that start_date values are fixed, since the FAQ advises against dynamic values such as datetime.now().
Record the approved timetable and promote
The platform team writes a short verification report that lists each DAG, its approved timetable, its catchup setting, the expected next run, and the staging result. Owners sign off on their rows, and the report is attached to the change request for the production upgrade. Promote the code and configuration only after the sign-off is recorded, and check the first production schedule against the report. The output is evidence that run timing was chosen, tested, and approved before the upgrade changed it.
Operational risks that change run timing
Changing timetable behavior during an upgrade or edit to a DAG can shift when runs happen, which runs exist, and what their dates mean. Each risk below has a specific control that a review, staging check or documented cutover can catch before production schedules move.
Skipped runs after changing catchup
Airflow 3 sets catchup_by_default to False. If a team sets it to True before upgrading, the existing CronDataIntervalTimetable behavior is kept, but changing it after the upgrade skips one scheduled run. Review every DAG that sets catchup for the upgrade window, and confirm in staging which run the first scheduler cycle creates before production is switched over.
Mislabeled runs from logical date assumptions
The upgrade guide warns against assuming that a manual run's data interval equals the logical_date supplied to it. Pipelines that compute partitions from the wrong value can write the wrong period. Require code review to show which variable each query and partition filter reads, and add a staging test that triggers a manual run with an explicit date and checks the output partition.
Unstable start_date shifting the first run
The Airflow FAQ advises against dynamic start_date values such as datetime.now(). The first run is created after the first complete data interval following start_date, so a value that changes on every parse moves the schedule each time the file loads. Enforce a fixed, dated start_date in code review and add a lint rule or CI check that rejects calls to the current time in DAG definitions.
Downstream jobs reading run timestamps
Consumers that read run timestamps, such as a reporting job that expects a run at a fixed clock time, break when a timetable change moves run creation or when a run keys on a different date than before. Inventory these consumers before cutover, record the timetable each depends on, and agree a documented cutover window with their owners so that a change is announced and tested before the schedule moves.
Questions teams ask before changing cron behavior
Do we need outside help to review cron schedules before upgrading to Airflow 3?
Not always. A small estate with one owner can often be reviewed in-house, while a larger one usually benefits from outside help. The effort drivers are the number of DAGs with cron schedules, whether any of them set catchup or timetable options explicitly, how many downstream jobs depend on run dates, and how well the current schedules are documented. Someone has to inventory every schedule, decide the timetable for each pipeline, and test the new timing in staging. A fixed-scope engagement from DeployOpen would define that work in writing before it starts. No price list is published, so ask for a quote tied to your DAG count and the systems that consume their output.
How long does a timetable review take, and what sets the duration?
No fixed duration applies, because the length depends on your estate. The main drivers are the number of DAGs and schedules, how many rely on catchup or on the older CronDataIntervalTimetable behavior, the number of teams who own them, and whether run history must be compared before and after the change. The upgrade path also adds steps. The upgrade guide requires Airflow 2.7 or later before the jump and recommends moving to the latest 2.x first. Ask any partner to split the estimate into inventory, timetable decisions, staging tests and cutover checks so you can see which part drives the total.
What do our engineers and data owners need to provide?
Engineers need to supply the raw material for each schedule decision, and data owners need to confirm what the outputs mean. Expect to provide:
- A list of DAGs with their schedule strings and any timetable arguments
- Current catchup and start_date settings, with the reason each was chosen
- The downstream consumers that expect a particular run date or data window
- Read access to run history from the current environment for comparison
- A named owner who approves the new run timing for each pipeline
Without the owner approvals, the technical change can be correct and still break a report or a reconciliation job.
Can our team handle the cron timetable change alone with the upgrade guide?
Yes, a team with Airflow experience can work from the upgrade guide alone. The guide documents the core behavior: in Airflow 3, catchup_by_default is False and create_cron_data_intervals is False, so bare cron strings use CronTriggerTimetable. Setting create_cron_data_intervals to True before upgrading keeps CronDataIntervalTimetable, while changing it afterward skips one scheduled run. The areas where teams usually want a second review are downstream consumers that read run dates and the staging tests that prove the new timing. If nobody on the team has owned those tests before, that is the point to ask for help.
Why does a manual run show a different date than I expect?
A manual trigger does not always set its data interval to the logical date you supplied. The upgrade guide warns against assuming those two values match and recommends using logical_date whenever a user-specified date is needed. Tasks that read interval start or end fields can therefore receive a window different from the date the operator typed. Find every task that reads interval fields, switch to logical_date where the user's date matters, and run a manual trigger with a known date in staging to confirm the outputs match what consumers expect.
How should start_date be set so the first run is predictable?
Use a fixed start_date, never a dynamic value such as datetime.now(). The Airflow FAQ recommends against dynamic start dates. Airflow creates the first run after the first complete data interval following start_date, so the value determines when the first run appears and which window it covers. Review each DAG's start_date during the timetable inventory, record the reason for each value, and confirm with the owner whether any historical windows should be processed at all.
What would a DeployOpen engagement scope for cron schedules?
A DeployOpen engagement is fixed-scope, so the work is defined in writing before it starts. For cron schedules, the scope would typically cover an inventory of DAGs with time-based schedules, a timetable decision for each pipeline, a standard for catchup and start_date values, a staging test plan that compares run timing before and after the change, a cutover checklist, and documentation for your team. Ongoing support and later reviews can be handled through a retainer. DeployOpen does not issue audit reports, so any compliance evidence from this work would go to your auditors for their own assessment.
How DeployOpen scopes timetable and upgrade work
DeployOpen scopes timetable work as a fixed-scope engagement, agreed in writing before any change starts. The first deliverable is a timetable inventory: every DAG in scope, its schedule expression, whether it uses a bare cron string or an explicit timetable, its start_date, its catchup setting, and which teams depend on the timing of its runs. The inventory also records the create_cron_data_intervals and catchup_by_default settings in each environment, because those two values decide which timetable a bare cron string receives after an upgrade. Confirm with your own team which DAGs feed downstream reports, approvals or external systems, since those are the runs whose timing matters most.
The second part covers the Airflow 3 upgrade checks and staging verification. The work typically includes running the Ruff AIR301 and AIR302 rules over DAG code, deciding whether to set the cron-related flags before the upgrade so existing schedules keep their behavior, and comparing next-run and data-interval values in a staging environment that mirrors production. It also includes checking that manual runs use logical_date where a user-supplied date matters. The scope should name who runs each staging comparison, which environment is the reference, and what sign-off looks like before production is changed. The effort depends on how many DAGs use cron strings, how many use custom timetables, and how much staging data is available to test against.
Optional retainer support can follow go-live for teams that want help monitoring next-run dates, reviewing new DAGs against the chosen timetable, or handling later Airflow patch and minor releases. A quote follows from the written scope, and the scope should state which items are one-time and which recur. All work is delivered inside the client's own infrastructure, including the Airflow environments, metadata database and repositories, so the evidence of each timetable decision stays under the client's control.

