You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
3.x (verified against main at HEAD on 2026-05-06; same mechanism present in 2.9.x)
What happened?
When a DAG with catchup=False is paused, the "Next Run" value displayed in the UI (and returned by the FastAPI /api/v2/dags/{dag_id}/detailsnext_dagrun_logical_date / next_dagrun_run_after) is recomputed each parse cycle against the current wall-clock time. The result is that the displayed value moves forward by one cron period each day, but stays in the past relative to "now". To a user, this looks like the scheduler is broken — the date keeps changing daily, but never advances to a future time.
Concrete example with cron 0 1 * * * (one daily run, catchup=False):
When (UTC)
DAG state
UI "Next Run" shows
User's reading
Day 0, 23:30
unpaused, prior run terminal
Day +1, 08:00 (future)
✓ "tomorrow"
Day 0, 23:39
user pauses
Day +1, 08:00 (still future at this moment)
✓ ok
Day 1, 22:40
parse runs while paused
Day 0, 08:00 (now in past)
⚠ "stuck?"
Day 2, 22:40
parse runs while paused
Day 1, 08:00 (still in past)
⚠ "drifting"
Day 3, 22:40
parse runs while paused
Day 2, 08:00 (still in past)
❌ "broken"
The displayed date strictly increases by one cron period per day, but the user's now increases by the same amount, so the gap between "now" and "Next Run" never closes — the value is permanently in the past while the DAG remains paused. This often gets escalated as a scheduling regression before the user discovers that the DAG is simply paused.
What you think should happen instead?
For paused DAGs, "Next Run" is not predictive of when the next run will fire — the scheduler will not materialize any run while is_paused=True. The current display is therefore misleading.
Three fix options, increasing in invasiveness:
(A) UI-only. When is_paused=True, render "Next Run" as — or Will run on unpause: <logical_date> (with a tooltip clarifying that the value is the interval that will fire on unpause, not a scheduled future time). Lowest-risk option, addresses the user-facing confusion entirely. Touches airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx:102-108 and the equivalent DAG-details view.
(B) FastAPI shape change. Set next_dagrun_logical_date / next_dagrun_run_after to null (or to a separate next_dagrun_on_unpause field) when is_paused=True. Cleaner contract but breaks any external API consumer that reads those fields on paused DAGs. Touches airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py.
(C) Scheduler short-circuit. Skip calculate_dagrun_date_fields in airflow-core/src/airflow/dag_processing/collection.py:638 when is_paused=True. Stops the rolling-forward computation at the source. Most invasive — would freeze next_dagrun_* at the value they had when the user paused; the existing "fires the missed interval immediately on unpause" behavior would need a recompute path on unpause to be preserved.
(A) is the recommended fix — minimum surface, no behavior change, just a UI guard with a clearer label.
How to reproduce
Define a DAG with catchup=False and a daily cron, e.g. 0 1 * * *. Start the scheduler.
Wait for one scheduled run to complete. Confirm the UI shows "Next Run" at the next scheduled time (e.g., tomorrow 1 AM).
Pause the DAG.
Wait at least one full DAG-parse cycle past the next scheduled time (default scheduler dag_dir_list_interval=300s; for a clearer effect wait until the next day).
Refresh the DAGs list page.
Observed: "Next Run" displays a date in the past. Each subsequent day the date moves forward by one cron period but remains in the past.
Expected: the UI does not display a misleading past-date "Next Run" while the DAG is paused.
Operating System
N/A — logic bug, not OS-specific
Versions of Apache Airflow Providers
N/A
Deployment
Other
Deployment details
Reproduces with airflow standalone on main HEAD as of 2026-05-06.
Anything else?
Source-code trace (verified against apache/airflowmain at 2026-05-06)
The mechanism is unchanged from 2.9.x → 3.x:
Parse path always runs calculate_dagrun_date_fields, regardless of pause state.airflow-core/src/airflow/dag_processing/collection.py:563-638 — update_dags() iterates all DAGs and calls dm.calculate_dagrun_date_fields(dag, last_automated_run=last_automated_run) at line 638. There is no is_paused short-circuit anywhere in this loop.
calculate_dagrun_date_fields calls next_dagrun_info which routes to the timetable's _skip_to_latest for catchup=False.airflow-core/src/airflow/models/dag.py:757-800 calls dag.next_dagrun_info(...). The TODO at dag.py:769 already references the existing meta-bug calculate_dagrun_date_fields doesn't understand how it is called #59618 about the helper's contract.
_skip_to_latest recomputes against utcnow() on every call.airflow-core/src/airflow/timetables/interval.py:156-178 (CronDataIntervalTimetable). Concrete trace at current_time = 2026-05-04T22:40:22+00:00 for cron 0 1 * * *:
current_time = 5-4 22:40 UTC
last_start = _get_prev(5-4 22:40) = 5-4 08:00 UTC
next_start = _get_next(5-4 08:00) = 5-5 08:00 UTC
# next_start > current_time, so:
new_start = _get_prev(last_start) = 5-3 08:00 UTC
Run the same code one day later at 5-5 22:40 UTC and new_start becomes 5-4 08:00 UTC — i.e., the value rolls forward by one cron period each day. The RoundedDataIntervalTimetable._skip_to_latest (interval.py:237-250) has the same utcnow()-based recomputation.
FastAPI returns these fields unchanged for paused DAGs.airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py:84-108 — is_paused: bool and next_dagrun_logical_date / next_dagrun_data_interval_start / next_dagrun_data_interval_end / next_dagrun_run_after are all serialized as independent fields with no conditional logic between them.
React UI renders "Next Run" with no is_paused check.airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx:102-108:
The only is_paused check in this file (line 95) is on the spinner for the Latest Run — Next Run has no equivalent guard.
Relationship to existing issues
calculate_dagrun_date_fields doesn't understand how it is called #59618 (calculate_dagrun_date_fields doesn't understand how it is called, OPEN, kind:bug area:Scheduler) is the in-tree TODO at dag.py:769, but it's about the helper's parameter contract (last_automated_run not always being the latest run, leading to backwards moves under concurrency) — different mechanism, different symptom. Multiple contributors are working on a rename + audit there.
This issue is not a duplicate of any of those — it's specifically about the UX of surfacing the rolling-forward-past next_dagrun_* value to a user who has paused the DAG.
Scope notes
Specific to catchup=False. With catchup=True, the timetable does not call _skip_to_latest, so the rolling-forward effect doesn't manifest the same way (paused-then-unpaused with catchup=True would attempt backfill of all missed intervals — different user-facing behavior).
The same display issue affects next_dagrun_data_interval_start and next_dagrun_data_interval_end.
The mechanism is correct for unpaused DAGs: it's how catchup=False knows to fire the most recent missed interval on schedule resumption. The problem is purely the UX of surfacing the rolling-forward value in a state where it's not predictive.
Adjacent: any DAG where run materialization is blocked while parsing continues (paused, max_active_runs exhausted, deactivated) will have the same display drift. Paused is the cleanest case to fix first; the others can follow.
Are you willing to submit PR?
Yes I am willing to submit a PR (Option A, UI-only)
Apache Airflow version
3.x (verified against
mainat HEAD on 2026-05-06; same mechanism present in 2.9.x)What happened?
When a DAG with
catchup=Falseis paused, the "Next Run" value displayed in the UI (and returned by the FastAPI/api/v2/dags/{dag_id}/detailsnext_dagrun_logical_date/next_dagrun_run_after) is recomputed each parse cycle against the current wall-clock time. The result is that the displayed value moves forward by one cron period each day, but stays in the past relative to "now". To a user, this looks like the scheduler is broken — the date keeps changing daily, but never advances to a future time.Concrete example with cron
0 1 * * *(one daily run,catchup=False):The displayed date strictly increases by one cron period per day, but the user's now increases by the same amount, so the gap between "now" and "Next Run" never closes — the value is permanently in the past while the DAG remains paused. This often gets escalated as a scheduling regression before the user discovers that the DAG is simply paused.
What you think should happen instead?
For paused DAGs, "Next Run" is not predictive of when the next run will fire — the scheduler will not materialize any run while
is_paused=True. The current display is therefore misleading.Three fix options, increasing in invasiveness:
(A) UI-only. When
is_paused=True, render "Next Run" as—orWill run on unpause: <logical_date>(with a tooltip clarifying that the value is the interval that will fire on unpause, not a scheduled future time). Lowest-risk option, addresses the user-facing confusion entirely. Touchesairflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx:102-108and the equivalent DAG-details view.(B) FastAPI shape change. Set
next_dagrun_logical_date/next_dagrun_run_aftertonull(or to a separatenext_dagrun_on_unpausefield) whenis_paused=True. Cleaner contract but breaks any external API consumer that reads those fields on paused DAGs. Touchesairflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py.(C) Scheduler short-circuit. Skip
calculate_dagrun_date_fieldsinairflow-core/src/airflow/dag_processing/collection.py:638whenis_paused=True. Stops the rolling-forward computation at the source. Most invasive — would freezenext_dagrun_*at the value they had when the user paused; the existing "fires the missed interval immediately on unpause" behavior would need a recompute path on unpause to be preserved.(A) is the recommended fix — minimum surface, no behavior change, just a UI guard with a clearer label.
How to reproduce
catchup=Falseand a daily cron, e.g.0 1 * * *. Start the scheduler.dag_dir_list_interval=300s; for a clearer effect wait until the next day).Observed: "Next Run" displays a date in the past. Each subsequent day the date moves forward by one cron period but remains in the past.
Expected: the UI does not display a misleading past-date "Next Run" while the DAG is paused.
Operating System
N/A — logic bug, not OS-specific
Versions of Apache Airflow Providers
N/A
Deployment
Other
Deployment details
Reproduces with
airflow standaloneonmainHEAD as of 2026-05-06.Anything else?
Source-code trace (verified against
apache/airflowmainat 2026-05-06)The mechanism is unchanged from 2.9.x → 3.x:
Parse path always runs
calculate_dagrun_date_fields, regardless of pause state.airflow-core/src/airflow/dag_processing/collection.py:563-638—update_dags()iterates all DAGs and callsdm.calculate_dagrun_date_fields(dag, last_automated_run=last_automated_run)at line 638. There is nois_pausedshort-circuit anywhere in this loop.calculate_dagrun_date_fieldscallsnext_dagrun_infowhich routes to the timetable's_skip_to_latestforcatchup=False.airflow-core/src/airflow/models/dag.py:757-800callsdag.next_dagrun_info(...). The TODO atdag.py:769already references the existing meta-bug calculate_dagrun_date_fields doesn't understand how it is called #59618 about the helper's contract._skip_to_latestrecomputes againstutcnow()on every call.airflow-core/src/airflow/timetables/interval.py:156-178(CronDataIntervalTimetable). Concrete trace atcurrent_time = 2026-05-04T22:40:22+00:00for cron0 1 * * *:Run the same code one day later at
5-5 22:40 UTCandnew_startbecomes5-4 08:00 UTC— i.e., the value rolls forward by one cron period each day. TheRoundedDataIntervalTimetable._skip_to_latest(interval.py:237-250) has the sameutcnow()-based recomputation.FastAPI returns these fields unchanged for paused DAGs.
airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py:84-108—is_paused: boolandnext_dagrun_logical_date / next_dagrun_data_interval_start / next_dagrun_data_interval_end / next_dagrun_run_afterare all serialized as independent fields with no conditional logic between them.React UI renders "Next Run" with no
is_pausedcheck.airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx:102-108:The only
is_pausedcheck in this file (line 95) is on the spinner for the Latest Run — Next Run has no equivalent guard.Relationship to existing issues
calculate_dagrun_date_fields doesn't understand how it is called, OPEN, kind:bug area:Scheduler) is the in-tree TODO atdag.py:769, but it's about the helper's parameter contract (last_automated_runnot always being the latest run, leading to backwards moves under concurrency) — different mechanism, different symptom. Multiple contributors are working on a rename + audit there.next_dagrun_*value to a user who has paused the DAG.Scope notes
catchup=False. Withcatchup=True, the timetable does not call_skip_to_latest, so the rolling-forward effect doesn't manifest the same way (paused-then-unpaused withcatchup=Truewould attempt backfill of all missed intervals — different user-facing behavior).next_dagrun_data_interval_startandnext_dagrun_data_interval_end.catchup=Falseknows to fire the most recent missed interval on schedule resumption. The problem is purely the UX of surfacing the rolling-forward value in a state where it's not predictive.max_active_runsexhausted, deactivated) will have the same display drift. Paused is the cleanest case to fix first; the others can follow.Are you willing to submit PR?
Code of Conduct