Skip to content

Fix cron description treating "?" as a day-of-month/day-of-week conflict - #72292

Open
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description
Open

Fix cron description treating "?" as a day-of-month/day-of-week conflict#72292
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description

Conversation

@ethanstoner

Copy link
Copy Markdown

croniter accepts ? in the day-of-month and day-of-week fields and expands it to * (croniter.py: "currently just trade ? as *"), so 0 0 ? * MON schedules exactly like 0 0 * * MON. Airflow's docs explicitly point users at croniter's extended syntax in cron.rst.

CronMixin._describe_with_dom_dow_fix compares the DOM/DOW fields against "*" only, so a ? counts as a restriction and the description gets split into a bogus "or" clause that claims a run which never happens.

Reproduction (on main, before this change)

$ uv run --project airflow-core python -c '...'
croniter 6.2.4 | cron-descriptor 2.1.0
expression : 0 0 ? * MON
actual runs : ['2026-01-05', '2026-01-12', '2026-01-19'] # Mondays only
description : 'At 00:00 (or) At 00:00, only on Monday' # <- claims a daily midnight run
expression : 0 0 1 * ?
actual runs : ['2026-02-01', '2026-03-01', '2026-04-01'] # 1st of month only
description : 'At 00:00, on day 1 of the month (or) At 00:00'

The description is persisted as DagModel.timetable_description and surfaced in the UI, the REST API (DAGDetailsResponse.timetable_description) and airflow dags details, so the user is shown a schedule that contradicts what the scheduler actually does.

After

'0 0 ? * MON' -> 'At 00:00, only on Monday'
'0 0 1 * ?' -> 'At 00:00, on day 1 of the month'
'0 0 15 * 1' -> 'At 00:00, on day 15 of the month (or) At 00:00, only on Monday'

Genuine DOM/DOW conflicts keep the existing "or" explanation added in #54644.

Testing

Three parametrized cases added to airflow-core/tests/unit/timetables/test_cron_mixin.py, each asserting the ? form describes identically to its * equivalent. They fail on unpatched main:

3 failed, 3 passed
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-month]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-week]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-both]

and pass with the fix. Full suite and static checks:

  • uv run --project airflow-core pytest airflow-core/tests/unit/timetables/256 passed
  • prek run --files airflow-core/src/airflow/timetables/_cron.py airflow-core/tests/unit/timetables/test_cron_mixin.py — all hooks pass
  • prek run mypy-airflow-core --files airflow-core/src/airflow/timetables/_cron.py — passed

No newsfragment: this is a bugfix to a generated description string, not a significant user-facing change. Happy to add one if a maintainer prefers.


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Code following the guidelines

The defect was found by auditing airflow/timetables/, reproduced against unmodified main, and every command and output quoted above was actually run locally. I have reviewed and understand the change; it is two lines of logic plus a test.

croniter accepts "?" in the day-of-month and day-of-week fields and expands
it to "*", so "0 0 ? * MON" schedules exactly like "0 0 * * MON". The
DOM/DOW conflict check compared the two fields against "*" only, so a "?"
counted as a restriction and the generated description was split into a
bogus "or" clause:
0 0 ? * MON -> "At 00:00 (or) At 00:00, only on Monday"
0 0 1 * ? -> "At 00:00, on day 1 of the month (or) At 00:00"
The extra clause claims a daily midnight run that never happens. This
description is stored as DagModel.timetable_description and shown in the
UI, the REST API and "airflow dags details".
Treat "?" as unrestricted alongside "*". Genuine DOM/DOW conflicts such as
"0 0 15 * 1" keep the existing "or" explanation.
Signed-off-by: Ethan Stoner <ethanstoner08@gmail.com>
@boring-cyborg

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contributors' Guide
Here are some useful points:

  • Pay attention to the quality of your code (ruff, mypy and type annotations). Our prek-hooks will help you with that.
  • In case of a new feature add useful documentation (in docstrings or in docs/ directory). Adding a new operator? Check this short guide Consider adding an example Dag that shows how users should use it.
  • Consider using Breeze environment for testing locally, it's a heavy docker but it ships with a working Airflow and a lot of integrations.
  • Be patient and persistent. It might take some time to get a review or get the final approval from Committers.
  • Please follow ASF Code of Conduct for all communication including (but not limited to) comments on Pull Requests, Mailing list and Slack.
  • Be sure to read the Airflow Coding style.
  • Always keep your Pull Requests rebased, otherwise your build might fail due to changes not related to your commits.
    Apache Airflow is a community-driven project and together we are making it better 🚀.
    In case of doubts contact the developers at:
    Mailing List: dev@airflow.apache.org
    Slack: https://s.apache.org/airflow-slack

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ethanstoner
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Fix cron description treating "?" as a day-of-month/day-of-week conflict by ethanstoner · Pull Request #72292 · apache/airflow · GitHub
Skip to content

Fix cron description treating "?" as a day-of-month/day-of-week conflict - #72292

Open
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description
Open

Fix cron description treating "?" as a day-of-month/day-of-week conflict#72292
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description

Conversation

@ethanstoner

Copy link
Copy Markdown

croniter accepts ? in the day-of-month and day-of-week fields and expands it to * (croniter.py: "currently just trade ? as *"), so 0 0 ? * MON schedules exactly like 0 0 * * MON. Airflow's docs explicitly point users at croniter's extended syntax in cron.rst.

CronMixin._describe_with_dom_dow_fix compares the DOM/DOW fields against "*" only, so a ? counts as a restriction and the description gets split into a bogus "or" clause that claims a run which never happens.

Reproduction (on main, before this change)

$ uv run --project airflow-core python -c '...'
croniter 6.2.4 | cron-descriptor 2.1.0
expression : 0 0 ? * MON
actual runs : ['2026-01-05', '2026-01-12', '2026-01-19'] # Mondays only
description : 'At 00:00 (or) At 00:00, only on Monday' # <- claims a daily midnight run
expression : 0 0 1 * ?
actual runs : ['2026-02-01', '2026-03-01', '2026-04-01'] # 1st of month only
description : 'At 00:00, on day 1 of the month (or) At 00:00'

The description is persisted as DagModel.timetable_description and surfaced in the UI, the REST API (DAGDetailsResponse.timetable_description) and airflow dags details, so the user is shown a schedule that contradicts what the scheduler actually does.

After

'0 0 ? * MON' -> 'At 00:00, only on Monday'
'0 0 1 * ?' -> 'At 00:00, on day 1 of the month'
'0 0 15 * 1' -> 'At 00:00, on day 15 of the month (or) At 00:00, only on Monday'

Genuine DOM/DOW conflicts keep the existing "or" explanation added in #54644.

Testing

Three parametrized cases added to airflow-core/tests/unit/timetables/test_cron_mixin.py, each asserting the ? form describes identically to its * equivalent. They fail on unpatched main:

3 failed, 3 passed
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-month]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-week]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-both]

and pass with the fix. Full suite and static checks:

  • uv run --project airflow-core pytest airflow-core/tests/unit/timetables/256 passed
  • prek run --files airflow-core/src/airflow/timetables/_cron.py airflow-core/tests/unit/timetables/test_cron_mixin.py — all hooks pass
  • prek run mypy-airflow-core --files airflow-core/src/airflow/timetables/_cron.py — passed

No newsfragment: this is a bugfix to a generated description string, not a significant user-facing change. Happy to add one if a maintainer prefers.


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Code following the guidelines

The defect was found by auditing airflow/timetables/, reproduced against unmodified main, and every command and output quoted above was actually run locally. I have reviewed and understand the change; it is two lines of logic plus a test.

croniter accepts "?" in the day-of-month and day-of-week fields and expands
it to "*", so "0 0 ? * MON" schedules exactly like "0 0 * * MON". The
DOM/DOW conflict check compared the two fields against "*" only, so a "?"
counted as a restriction and the generated description was split into a
bogus "or" clause:
0 0 ? * MON -> "At 00:00 (or) At 00:00, only on Monday"
0 0 1 * ? -> "At 00:00, on day 1 of the month (or) At 00:00"
The extra clause claims a daily midnight run that never happens. This
description is stored as DagModel.timetable_description and shown in the
UI, the REST API and "airflow dags details".
Treat "?" as unrestricted alongside "*". Genuine DOM/DOW conflicts such as
"0 0 15 * 1" keep the existing "or" explanation.
Signed-off-by: Ethan Stoner <ethanstoner08@gmail.com>
@boring-cyborg

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contributors' Guide
Here are some useful points:

  • Pay attention to the quality of your code (ruff, mypy and type annotations). Our prek-hooks will help you with that.
  • In case of a new feature add useful documentation (in docstrings or in docs/ directory). Adding a new operator? Check this short guide Consider adding an example Dag that shows how users should use it.
  • Consider using Breeze environment for testing locally, it's a heavy docker but it ships with a working Airflow and a lot of integrations.
  • Be patient and persistent. It might take some time to get a review or get the final approval from Committers.
  • Please follow ASF Code of Conduct for all communication including (but not limited to) comments on Pull Requests, Mailing list and Slack.
  • Be sure to read the Airflow Coding style.
  • Always keep your Pull Requests rebased, otherwise your build might fail due to changes not related to your commits.
    Apache Airflow is a community-driven project and together we are making it better 🚀.
    In case of doubts contact the developers at:
    Mailing List: dev@airflow.apache.org
    Slack: https://s.apache.org/airflow-slack

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ethanstoner
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix cron description treating "?" as a day-of-month/day-of-week conflict by ethanstoner · Pull Request #72292 · apache/airflow · GitHub
Skip to content

Fix cron description treating "?" as a day-of-month/day-of-week conflict - #72292

Open
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description
Open

Fix cron description treating "?" as a day-of-month/day-of-week conflict#72292
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description

Conversation

@ethanstoner

Copy link
Copy Markdown

croniter accepts ? in the day-of-month and day-of-week fields and expands it to * (croniter.py: "currently just trade ? as *"), so 0 0 ? * MON schedules exactly like 0 0 * * MON. Airflow's docs explicitly point users at croniter's extended syntax in cron.rst.

CronMixin._describe_with_dom_dow_fix compares the DOM/DOW fields against "*" only, so a ? counts as a restriction and the description gets split into a bogus "or" clause that claims a run which never happens.

Reproduction (on main, before this change)

$ uv run --project airflow-core python -c '...'
croniter 6.2.4 | cron-descriptor 2.1.0
expression : 0 0 ? * MON
actual runs : ['2026-01-05', '2026-01-12', '2026-01-19'] # Mondays only
description : 'At 00:00 (or) At 00:00, only on Monday' # <- claims a daily midnight run
expression : 0 0 1 * ?
actual runs : ['2026-02-01', '2026-03-01', '2026-04-01'] # 1st of month only
description : 'At 00:00, on day 1 of the month (or) At 00:00'

The description is persisted as DagModel.timetable_description and surfaced in the UI, the REST API (DAGDetailsResponse.timetable_description) and airflow dags details, so the user is shown a schedule that contradicts what the scheduler actually does.

After

'0 0 ? * MON' -> 'At 00:00, only on Monday'
'0 0 1 * ?' -> 'At 00:00, on day 1 of the month'
'0 0 15 * 1' -> 'At 00:00, on day 15 of the month (or) At 00:00, only on Monday'

Genuine DOM/DOW conflicts keep the existing "or" explanation added in #54644.

Testing

Three parametrized cases added to airflow-core/tests/unit/timetables/test_cron_mixin.py, each asserting the ? form describes identically to its * equivalent. They fail on unpatched main:

3 failed, 3 passed
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-month]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-week]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-both]

and pass with the fix. Full suite and static checks:

  • uv run --project airflow-core pytest airflow-core/tests/unit/timetables/256 passed
  • prek run --files airflow-core/src/airflow/timetables/_cron.py airflow-core/tests/unit/timetables/test_cron_mixin.py — all hooks pass
  • prek run mypy-airflow-core --files airflow-core/src/airflow/timetables/_cron.py — passed

No newsfragment: this is a bugfix to a generated description string, not a significant user-facing change. Happy to add one if a maintainer prefers.


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Code following the guidelines

The defect was found by auditing airflow/timetables/, reproduced against unmodified main, and every command and output quoted above was actually run locally. I have reviewed and understand the change; it is two lines of logic plus a test.

croniter accepts "?" in the day-of-month and day-of-week fields and expands
it to "*", so "0 0 ? * MON" schedules exactly like "0 0 * * MON". The
DOM/DOW conflict check compared the two fields against "*" only, so a "?"
counted as a restriction and the generated description was split into a
bogus "or" clause:
0 0 ? * MON -> "At 00:00 (or) At 00:00, only on Monday"
0 0 1 * ? -> "At 00:00, on day 1 of the month (or) At 00:00"
The extra clause claims a daily midnight run that never happens. This
description is stored as DagModel.timetable_description and shown in the
UI, the REST API and "airflow dags details".
Treat "?" as unrestricted alongside "*". Genuine DOM/DOW conflicts such as
"0 0 15 * 1" keep the existing "or" explanation.
Signed-off-by: Ethan Stoner <ethanstoner08@gmail.com>
@boring-cyborg

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contributors' Guide
Here are some useful points:

  • Pay attention to the quality of your code (ruff, mypy and type annotations). Our prek-hooks will help you with that.
  • In case of a new feature add useful documentation (in docstrings or in docs/ directory). Adding a new operator? Check this short guide Consider adding an example Dag that shows how users should use it.
  • Consider using Breeze environment for testing locally, it's a heavy docker but it ships with a working Airflow and a lot of integrations.
  • Be patient and persistent. It might take some time to get a review or get the final approval from Committers.
  • Please follow ASF Code of Conduct for all communication including (but not limited to) comments on Pull Requests, Mailing list and Slack.
  • Be sure to read the Airflow Coding style.
  • Always keep your Pull Requests rebased, otherwise your build might fail due to changes not related to your commits.
    Apache Airflow is a community-driven project and together we are making it better 🚀.
    In case of doubts contact the developers at:
    Mailing List: dev@airflow.apache.org
    Slack: https://s.apache.org/airflow-slack

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ethanstoner
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix cron description treating "?" as a day-of-month/day-of-week conflict by ethanstoner · Pull Request #72292 · apache/airflow · GitHub
Skip to content

Fix cron description treating "?" as a day-of-month/day-of-week conflict - #72292

Open
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description
Open

Fix cron description treating "?" as a day-of-month/day-of-week conflict#72292
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description

Conversation

@ethanstoner

Copy link
Copy Markdown

croniter accepts ? in the day-of-month and day-of-week fields and expands it to * (croniter.py: "currently just trade ? as *"), so 0 0 ? * MON schedules exactly like 0 0 * * MON. Airflow's docs explicitly point users at croniter's extended syntax in cron.rst.

CronMixin._describe_with_dom_dow_fix compares the DOM/DOW fields against "*" only, so a ? counts as a restriction and the description gets split into a bogus "or" clause that claims a run which never happens.

Reproduction (on main, before this change)

$ uv run --project airflow-core python -c '...'
croniter 6.2.4 | cron-descriptor 2.1.0
expression : 0 0 ? * MON
actual runs : ['2026-01-05', '2026-01-12', '2026-01-19'] # Mondays only
description : 'At 00:00 (or) At 00:00, only on Monday' # <- claims a daily midnight run
expression : 0 0 1 * ?
actual runs : ['2026-02-01', '2026-03-01', '2026-04-01'] # 1st of month only
description : 'At 00:00, on day 1 of the month (or) At 00:00'

The description is persisted as DagModel.timetable_description and surfaced in the UI, the REST API (DAGDetailsResponse.timetable_description) and airflow dags details, so the user is shown a schedule that contradicts what the scheduler actually does.

After

'0 0 ? * MON' -> 'At 00:00, only on Monday'
'0 0 1 * ?' -> 'At 00:00, on day 1 of the month'
'0 0 15 * 1' -> 'At 00:00, on day 15 of the month (or) At 00:00, only on Monday'

Genuine DOM/DOW conflicts keep the existing "or" explanation added in #54644.

Testing

Three parametrized cases added to airflow-core/tests/unit/timetables/test_cron_mixin.py, each asserting the ? form describes identically to its * equivalent. They fail on unpatched main:

3 failed, 3 passed
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-month]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-week]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-both]

and pass with the fix. Full suite and static checks:

  • uv run --project airflow-core pytest airflow-core/tests/unit/timetables/256 passed
  • prek run --files airflow-core/src/airflow/timetables/_cron.py airflow-core/tests/unit/timetables/test_cron_mixin.py — all hooks pass
  • prek run mypy-airflow-core --files airflow-core/src/airflow/timetables/_cron.py — passed

No newsfragment: this is a bugfix to a generated description string, not a significant user-facing change. Happy to add one if a maintainer prefers.


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Code following the guidelines

The defect was found by auditing airflow/timetables/, reproduced against unmodified main, and every command and output quoted above was actually run locally. I have reviewed and understand the change; it is two lines of logic plus a test.

croniter accepts "?" in the day-of-month and day-of-week fields and expands
it to "*", so "0 0 ? * MON" schedules exactly like "0 0 * * MON". The
DOM/DOW conflict check compared the two fields against "*" only, so a "?"
counted as a restriction and the generated description was split into a
bogus "or" clause:
0 0 ? * MON -> "At 00:00 (or) At 00:00, only on Monday"
0 0 1 * ? -> "At 00:00, on day 1 of the month (or) At 00:00"
The extra clause claims a daily midnight run that never happens. This
description is stored as DagModel.timetable_description and shown in the
UI, the REST API and "airflow dags details".
Treat "?" as unrestricted alongside "*". Genuine DOM/DOW conflicts such as
"0 0 15 * 1" keep the existing "or" explanation.
Signed-off-by: Ethan Stoner <ethanstoner08@gmail.com>
@boring-cyborg

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contributors' Guide
Here are some useful points:

  • Pay attention to the quality of your code (ruff, mypy and type annotations). Our prek-hooks will help you with that.
  • In case of a new feature add useful documentation (in docstrings or in docs/ directory). Adding a new operator? Check this short guide Consider adding an example Dag that shows how users should use it.
  • Consider using Breeze environment for testing locally, it's a heavy docker but it ships with a working Airflow and a lot of integrations.
  • Be patient and persistent. It might take some time to get a review or get the final approval from Committers.
  • Please follow ASF Code of Conduct for all communication including (but not limited to) comments on Pull Requests, Mailing list and Slack.
  • Be sure to read the Airflow Coding style.
  • Always keep your Pull Requests rebased, otherwise your build might fail due to changes not related to your commits.
    Apache Airflow is a community-driven project and together we are making it better 🚀.
    In case of doubts contact the developers at:
    Mailing List: dev@airflow.apache.org
    Slack: https://s.apache.org/airflow-slack

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ethanstoner
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Fix cron description treating "?" as a day-of-month/day-of-week conflict by ethanstoner · Pull Request #72292 · apache/airflow · GitHub
Skip to content

Fix cron description treating "?" as a day-of-month/day-of-week conflict - #72292

Open
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description
Open

Fix cron description treating "?" as a day-of-month/day-of-week conflict#72292
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description

Conversation

@ethanstoner

Copy link
Copy Markdown

croniter accepts ? in the day-of-month and day-of-week fields and expands it to * (croniter.py: "currently just trade ? as *"), so 0 0 ? * MON schedules exactly like 0 0 * * MON. Airflow's docs explicitly point users at croniter's extended syntax in cron.rst.

CronMixin._describe_with_dom_dow_fix compares the DOM/DOW fields against "*" only, so a ? counts as a restriction and the description gets split into a bogus "or" clause that claims a run which never happens.

Reproduction (on main, before this change)

$ uv run --project airflow-core python -c '...'
croniter 6.2.4 | cron-descriptor 2.1.0
expression : 0 0 ? * MON
actual runs : ['2026-01-05', '2026-01-12', '2026-01-19'] # Mondays only
description : 'At 00:00 (or) At 00:00, only on Monday' # <- claims a daily midnight run
expression : 0 0 1 * ?
actual runs : ['2026-02-01', '2026-03-01', '2026-04-01'] # 1st of month only
description : 'At 00:00, on day 1 of the month (or) At 00:00'

The description is persisted as DagModel.timetable_description and surfaced in the UI, the REST API (DAGDetailsResponse.timetable_description) and airflow dags details, so the user is shown a schedule that contradicts what the scheduler actually does.

After

'0 0 ? * MON' -> 'At 00:00, only on Monday'
'0 0 1 * ?' -> 'At 00:00, on day 1 of the month'
'0 0 15 * 1' -> 'At 00:00, on day 15 of the month (or) At 00:00, only on Monday'

Genuine DOM/DOW conflicts keep the existing "or" explanation added in #54644.

Testing

Three parametrized cases added to airflow-core/tests/unit/timetables/test_cron_mixin.py, each asserting the ? form describes identically to its * equivalent. They fail on unpatched main:

3 failed, 3 passed
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-month]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-week]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-both]

and pass with the fix. Full suite and static checks:

  • uv run --project airflow-core pytest airflow-core/tests/unit/timetables/256 passed
  • prek run --files airflow-core/src/airflow/timetables/_cron.py airflow-core/tests/unit/timetables/test_cron_mixin.py — all hooks pass
  • prek run mypy-airflow-core --files airflow-core/src/airflow/timetables/_cron.py — passed

No newsfragment: this is a bugfix to a generated description string, not a significant user-facing change. Happy to add one if a maintainer prefers.


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Code following the guidelines

The defect was found by auditing airflow/timetables/, reproduced against unmodified main, and every command and output quoted above was actually run locally. I have reviewed and understand the change; it is two lines of logic plus a test.

croniter accepts "?" in the day-of-month and day-of-week fields and expands
it to "*", so "0 0 ? * MON" schedules exactly like "0 0 * * MON". The
DOM/DOW conflict check compared the two fields against "*" only, so a "?"
counted as a restriction and the generated description was split into a
bogus "or" clause:
0 0 ? * MON -> "At 00:00 (or) At 00:00, only on Monday"
0 0 1 * ? -> "At 00:00, on day 1 of the month (or) At 00:00"
The extra clause claims a daily midnight run that never happens. This
description is stored as DagModel.timetable_description and shown in the
UI, the REST API and "airflow dags details".
Treat "?" as unrestricted alongside "*". Genuine DOM/DOW conflicts such as
"0 0 15 * 1" keep the existing "or" explanation.
Signed-off-by: Ethan Stoner <ethanstoner08@gmail.com>
@boring-cyborg

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contributors' Guide
Here are some useful points:

  • Pay attention to the quality of your code (ruff, mypy and type annotations). Our prek-hooks will help you with that.
  • In case of a new feature add useful documentation (in docstrings or in docs/ directory). Adding a new operator? Check this short guide Consider adding an example Dag that shows how users should use it.
  • Consider using Breeze environment for testing locally, it's a heavy docker but it ships with a working Airflow and a lot of integrations.
  • Be patient and persistent. It might take some time to get a review or get the final approval from Committers.
  • Please follow ASF Code of Conduct for all communication including (but not limited to) comments on Pull Requests, Mailing list and Slack.
  • Be sure to read the Airflow Coding style.
  • Always keep your Pull Requests rebased, otherwise your build might fail due to changes not related to your commits.
    Apache Airflow is a community-driven project and together we are making it better 🚀.
    In case of doubts contact the developers at:
    Mailing List: dev@airflow.apache.org
    Slack: https://s.apache.org/airflow-slack

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ethanstoner
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix cron description treating "?" as a day-of-month/day-of-week conflict by ethanstoner · Pull Request #72292 · apache/airflow · GitHub
Skip to content

Fix cron description treating "?" as a day-of-month/day-of-week conflict - #72292

Open
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description
Open

Fix cron description treating "?" as a day-of-month/day-of-week conflict#72292
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description

Conversation

@ethanstoner

Copy link
Copy Markdown

croniter accepts ? in the day-of-month and day-of-week fields and expands it to * (croniter.py: "currently just trade ? as *"), so 0 0 ? * MON schedules exactly like 0 0 * * MON. Airflow's docs explicitly point users at croniter's extended syntax in cron.rst.

CronMixin._describe_with_dom_dow_fix compares the DOM/DOW fields against "*" only, so a ? counts as a restriction and the description gets split into a bogus "or" clause that claims a run which never happens.

Reproduction (on main, before this change)

$ uv run --project airflow-core python -c '...'
croniter 6.2.4 | cron-descriptor 2.1.0
expression : 0 0 ? * MON
actual runs : ['2026-01-05', '2026-01-12', '2026-01-19'] # Mondays only
description : 'At 00:00 (or) At 00:00, only on Monday' # <- claims a daily midnight run
expression : 0 0 1 * ?
actual runs : ['2026-02-01', '2026-03-01', '2026-04-01'] # 1st of month only
description : 'At 00:00, on day 1 of the month (or) At 00:00'

The description is persisted as DagModel.timetable_description and surfaced in the UI, the REST API (DAGDetailsResponse.timetable_description) and airflow dags details, so the user is shown a schedule that contradicts what the scheduler actually does.

After

'0 0 ? * MON' -> 'At 00:00, only on Monday'
'0 0 1 * ?' -> 'At 00:00, on day 1 of the month'
'0 0 15 * 1' -> 'At 00:00, on day 15 of the month (or) At 00:00, only on Monday'

Genuine DOM/DOW conflicts keep the existing "or" explanation added in #54644.

Testing

Three parametrized cases added to airflow-core/tests/unit/timetables/test_cron_mixin.py, each asserting the ? form describes identically to its * equivalent. They fail on unpatched main:

3 failed, 3 passed
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-month]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-week]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-both]

and pass with the fix. Full suite and static checks:

  • uv run --project airflow-core pytest airflow-core/tests/unit/timetables/256 passed
  • prek run --files airflow-core/src/airflow/timetables/_cron.py airflow-core/tests/unit/timetables/test_cron_mixin.py — all hooks pass
  • prek run mypy-airflow-core --files airflow-core/src/airflow/timetables/_cron.py — passed

No newsfragment: this is a bugfix to a generated description string, not a significant user-facing change. Happy to add one if a maintainer prefers.


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Code following the guidelines

The defect was found by auditing airflow/timetables/, reproduced against unmodified main, and every command and output quoted above was actually run locally. I have reviewed and understand the change; it is two lines of logic plus a test.

croniter accepts "?" in the day-of-month and day-of-week fields and expands
it to "*", so "0 0 ? * MON" schedules exactly like "0 0 * * MON". The
DOM/DOW conflict check compared the two fields against "*" only, so a "?"
counted as a restriction and the generated description was split into a
bogus "or" clause:
0 0 ? * MON -> "At 00:00 (or) At 00:00, only on Monday"
0 0 1 * ? -> "At 00:00, on day 1 of the month (or) At 00:00"
The extra clause claims a daily midnight run that never happens. This
description is stored as DagModel.timetable_description and shown in the
UI, the REST API and "airflow dags details".
Treat "?" as unrestricted alongside "*". Genuine DOM/DOW conflicts such as
"0 0 15 * 1" keep the existing "or" explanation.
Signed-off-by: Ethan Stoner <ethanstoner08@gmail.com>
@boring-cyborg

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contributors' Guide
Here are some useful points:

  • Pay attention to the quality of your code (ruff, mypy and type annotations). Our prek-hooks will help you with that.
  • In case of a new feature add useful documentation (in docstrings or in docs/ directory). Adding a new operator? Check this short guide Consider adding an example Dag that shows how users should use it.
  • Consider using Breeze environment for testing locally, it's a heavy docker but it ships with a working Airflow and a lot of integrations.
  • Be patient and persistent. It might take some time to get a review or get the final approval from Committers.
  • Please follow ASF Code of Conduct for all communication including (but not limited to) comments on Pull Requests, Mailing list and Slack.
  • Be sure to read the Airflow Coding style.
  • Always keep your Pull Requests rebased, otherwise your build might fail due to changes not related to your commits.
    Apache Airflow is a community-driven project and together we are making it better 🚀.
    In case of doubts contact the developers at:
    Mailing List: dev@airflow.apache.org
    Slack: https://s.apache.org/airflow-slack

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ethanstoner
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix cron description treating "?" as a day-of-month/day-of-week conflict by ethanstoner · Pull Request #72292 · apache/airflow · GitHub
Skip to content

Fix cron description treating "?" as a day-of-month/day-of-week conflict - #72292

Open
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description
Open

Fix cron description treating "?" as a day-of-month/day-of-week conflict#72292
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description

Conversation

@ethanstoner

Copy link
Copy Markdown

croniter accepts ? in the day-of-month and day-of-week fields and expands it to * (croniter.py: "currently just trade ? as *"), so 0 0 ? * MON schedules exactly like 0 0 * * MON. Airflow's docs explicitly point users at croniter's extended syntax in cron.rst.

CronMixin._describe_with_dom_dow_fix compares the DOM/DOW fields against "*" only, so a ? counts as a restriction and the description gets split into a bogus "or" clause that claims a run which never happens.

Reproduction (on main, before this change)

$ uv run --project airflow-core python -c '...'
croniter 6.2.4 | cron-descriptor 2.1.0
expression : 0 0 ? * MON
actual runs : ['2026-01-05', '2026-01-12', '2026-01-19'] # Mondays only
description : 'At 00:00 (or) At 00:00, only on Monday' # <- claims a daily midnight run
expression : 0 0 1 * ?
actual runs : ['2026-02-01', '2026-03-01', '2026-04-01'] # 1st of month only
description : 'At 00:00, on day 1 of the month (or) At 00:00'

The description is persisted as DagModel.timetable_description and surfaced in the UI, the REST API (DAGDetailsResponse.timetable_description) and airflow dags details, so the user is shown a schedule that contradicts what the scheduler actually does.

After

'0 0 ? * MON' -> 'At 00:00, only on Monday'
'0 0 1 * ?' -> 'At 00:00, on day 1 of the month'
'0 0 15 * 1' -> 'At 00:00, on day 15 of the month (or) At 00:00, only on Monday'

Genuine DOM/DOW conflicts keep the existing "or" explanation added in #54644.

Testing

Three parametrized cases added to airflow-core/tests/unit/timetables/test_cron_mixin.py, each asserting the ? form describes identically to its * equivalent. They fail on unpatched main:

3 failed, 3 passed
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-month]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-week]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-both]

and pass with the fix. Full suite and static checks:

  • uv run --project airflow-core pytest airflow-core/tests/unit/timetables/256 passed
  • prek run --files airflow-core/src/airflow/timetables/_cron.py airflow-core/tests/unit/timetables/test_cron_mixin.py — all hooks pass
  • prek run mypy-airflow-core --files airflow-core/src/airflow/timetables/_cron.py — passed

No newsfragment: this is a bugfix to a generated description string, not a significant user-facing change. Happy to add one if a maintainer prefers.


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Code following the guidelines

The defect was found by auditing airflow/timetables/, reproduced against unmodified main, and every command and output quoted above was actually run locally. I have reviewed and understand the change; it is two lines of logic plus a test.

croniter accepts "?" in the day-of-month and day-of-week fields and expands
it to "*", so "0 0 ? * MON" schedules exactly like "0 0 * * MON". The
DOM/DOW conflict check compared the two fields against "*" only, so a "?"
counted as a restriction and the generated description was split into a
bogus "or" clause:
0 0 ? * MON -> "At 00:00 (or) At 00:00, only on Monday"
0 0 1 * ? -> "At 00:00, on day 1 of the month (or) At 00:00"
The extra clause claims a daily midnight run that never happens. This
description is stored as DagModel.timetable_description and shown in the
UI, the REST API and "airflow dags details".
Treat "?" as unrestricted alongside "*". Genuine DOM/DOW conflicts such as
"0 0 15 * 1" keep the existing "or" explanation.
Signed-off-by: Ethan Stoner <ethanstoner08@gmail.com>
@boring-cyborg

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contributors' Guide
Here are some useful points:

  • Pay attention to the quality of your code (ruff, mypy and type annotations). Our prek-hooks will help you with that.
  • In case of a new feature add useful documentation (in docstrings or in docs/ directory). Adding a new operator? Check this short guide Consider adding an example Dag that shows how users should use it.
  • Consider using Breeze environment for testing locally, it's a heavy docker but it ships with a working Airflow and a lot of integrations.
  • Be patient and persistent. It might take some time to get a review or get the final approval from Committers.
  • Please follow ASF Code of Conduct for all communication including (but not limited to) comments on Pull Requests, Mailing list and Slack.
  • Be sure to read the Airflow Coding style.
  • Always keep your Pull Requests rebased, otherwise your build might fail due to changes not related to your commits.
    Apache Airflow is a community-driven project and together we are making it better 🚀.
    In case of doubts contact the developers at:
    Mailing List: dev@airflow.apache.org
    Slack: https://s.apache.org/airflow-slack

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ethanstoner
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Fix cron description treating "?" as a day-of-month/day-of-week conflict by ethanstoner · Pull Request #72292 · apache/airflow · GitHub
Skip to content

Fix cron description treating "?" as a day-of-month/day-of-week conflict - #72292

Open
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description
Open

Fix cron description treating "?" as a day-of-month/day-of-week conflict#72292
ethanstoner wants to merge 1 commit into
apache:mainfrom
ethanstoner:fix-cron-question-mark-description

Conversation

@ethanstoner

Copy link
Copy Markdown

croniter accepts ? in the day-of-month and day-of-week fields and expands it to * (croniter.py: "currently just trade ? as *"), so 0 0 ? * MON schedules exactly like 0 0 * * MON. Airflow's docs explicitly point users at croniter's extended syntax in cron.rst.

CronMixin._describe_with_dom_dow_fix compares the DOM/DOW fields against "*" only, so a ? counts as a restriction and the description gets split into a bogus "or" clause that claims a run which never happens.

Reproduction (on main, before this change)

$ uv run --project airflow-core python -c '...'
croniter 6.2.4 | cron-descriptor 2.1.0
expression : 0 0 ? * MON
actual runs : ['2026-01-05', '2026-01-12', '2026-01-19'] # Mondays only
description : 'At 00:00 (or) At 00:00, only on Monday' # <- claims a daily midnight run
expression : 0 0 1 * ?
actual runs : ['2026-02-01', '2026-03-01', '2026-04-01'] # 1st of month only
description : 'At 00:00, on day 1 of the month (or) At 00:00'

The description is persisted as DagModel.timetable_description and surfaced in the UI, the REST API (DAGDetailsResponse.timetable_description) and airflow dags details, so the user is shown a schedule that contradicts what the scheduler actually does.

After

'0 0 ? * MON' -> 'At 00:00, only on Monday'
'0 0 1 * ?' -> 'At 00:00, on day 1 of the month'
'0 0 15 * 1' -> 'At 00:00, on day 15 of the month (or) At 00:00, only on Monday'

Genuine DOM/DOW conflicts keep the existing "or" explanation added in #54644.

Testing

Three parametrized cases added to airflow-core/tests/unit/timetables/test_cron_mixin.py, each asserting the ? form describes identically to its * equivalent. They fail on unpatched main:

3 failed, 3 passed
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-month]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-day-of-week]
FAILED test_cron_mixin.py::test_question_mark_is_not_a_dom_dow_conflict[question-mark-both]

and pass with the fix. Full suite and static checks:

  • uv run --project airflow-core pytest airflow-core/tests/unit/timetables/256 passed
  • prek run --files airflow-core/src/airflow/timetables/_cron.py airflow-core/tests/unit/timetables/test_cron_mixin.py — all hooks pass
  • prek run mypy-airflow-core --files airflow-core/src/airflow/timetables/_cron.py — passed

No newsfragment: this is a bugfix to a generated description string, not a significant user-facing change. Happy to add one if a maintainer prefers.


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Code following the guidelines

The defect was found by auditing airflow/timetables/, reproduced against unmodified main, and every command and output quoted above was actually run locally. I have reviewed and understand the change; it is two lines of logic plus a test.

croniter accepts "?" in the day-of-month and day-of-week fields and expands
it to "*", so "0 0 ? * MON" schedules exactly like "0 0 * * MON". The
DOM/DOW conflict check compared the two fields against "*" only, so a "?"
counted as a restriction and the generated description was split into a
bogus "or" clause:
0 0 ? * MON -> "At 00:00 (or) At 00:00, only on Monday"
0 0 1 * ? -> "At 00:00, on day 1 of the month (or) At 00:00"
The extra clause claims a daily midnight run that never happens. This
description is stored as DagModel.timetable_description and shown in the
UI, the REST API and "airflow dags details".
Treat "?" as unrestricted alongside "*". Genuine DOM/DOW conflicts such as
"0 0 15 * 1" keep the existing "or" explanation.
Signed-off-by: Ethan Stoner <ethanstoner08@gmail.com>
@boring-cyborg

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contributors' Guide
Here are some useful points:

  • Pay attention to the quality of your code (ruff, mypy and type annotations). Our prek-hooks will help you with that.
  • In case of a new feature add useful documentation (in docstrings or in docs/ directory). Adding a new operator? Check this short guide Consider adding an example Dag that shows how users should use it.
  • Consider using Breeze environment for testing locally, it's a heavy docker but it ships with a working Airflow and a lot of integrations.
  • Be patient and persistent. It might take some time to get a review or get the final approval from Committers.
  • Please follow ASF Code of Conduct for all communication including (but not limited to) comments on Pull Requests, Mailing list and Slack.
  • Be sure to read the Airflow Coding style.
  • Always keep your Pull Requests rebased, otherwise your build might fail due to changes not related to your commits.
    Apache Airflow is a community-driven project and together we are making it better 🚀.
    In case of doubts contact the developers at:
    Mailing List: dev@airflow.apache.org
    Slack: https://s.apache.org/airflow-slack

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ethanstoner