This is an automated email from the ASF dual-hosted git repository.

henry3260 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 07f5458df87 Add airflowctl dags clear command (#68706)
07f5458df87 is described below

commit 07f5458df8760e9c53e66cbc0c5b8ddf29aea044
Author: Henry Chen <[email protected]>
AuthorDate: Thu Jul 30 13:02:03 2026 +0800

    Add airflowctl dags clear command (#68706)
    
    * Add airflowctl dags clear command
    
    * Fix airflowctl dags clear ignoring the partition date window
    
    The Dag run list endpoint declares the partition window as
    partition_date_gte / partition_date_lte. The names the command sent
    belong to the request body of a different route, and FastAPI drops
    query params it does not declare, so the window was silently discarded
    and every run of the Dag came back — and was cleared.
    
    partition_date_gt / partition_date_lt are not query params on that
    endpoint either, so they could never have filtered anything.
    
    * Drop unused Dag run logical date filters from airflowctl
    
    Nothing ever passed logical_date_gt or logical_date_lt, and documenting
    them broke the docs build: the spelling wordlist carries gte and lte but
    not gt and lt, so the generated API docs failed spell-checking.
    
    * Reuse the tasks clear operation for airflowctl dags clear
    
    DagRunOperations gained a private clear helper that duplicated the
    existing TasksOperations.clear. Because the CLI is generated from the
    operation classes, that helper also surfaced as a subcommand named
    "-clear-task-instances" with no help text.
    
    The Dag run list filters for run ID and partition key prefix had no
    callers either.
    
    * Group airflowctl dags clear arguments with the other Dag arguments
    
    The new arguments landed under the "# Task Commands Args" heading, so anyone
    looking for what "dags clear" accepts would not find them in the Dag section
    where they belong.
    
    * Consolidate airflowctl dags clear argument validation in one place
    
    The partition date ordering check sat in the function that fetches Dag runs
    while every other check lived in the validator, so a reader had to look in 
two
    places to learn what the command rejects. Parsing the window up front also
    removes a branch that could never be taken.
---
 airflow-ctl/docs/images/command_hashes.txt         |   2 +-
 airflow-ctl/docs/images/output_dags.svg            | 138 +++++-----
 airflow-ctl/src/airflowctl/api/operations.py       |  17 +-
 airflow-ctl/src/airflowctl/ctl/cli_config.py       |  61 +++++
 .../src/airflowctl/ctl/commands/dag_command.py     | 166 ++++++++++-
 airflow-ctl/src/airflowctl/ctl/help_texts.yaml     |   1 +
 .../tests/airflow_ctl/api/test_operations.py       |  37 +++
 .../airflow_ctl/ctl/commands/test_dag_command.py   | 305 ++++++++++++++++++++-
 8 files changed, 654 insertions(+), 73 deletions(-)

diff --git a/airflow-ctl/docs/images/command_hashes.txt 
b/airflow-ctl/docs/images/command_hashes.txt
index 74b5ede7ce3..40ddcd13a0b 100644
--- a/airflow-ctl/docs/images/command_hashes.txt
+++ b/airflow-ctl/docs/images/command_hashes.txt
@@ -4,7 +4,7 @@ auth:d79e9c7d00c432bdbcbc2a86e2e32053
 backfill:74c8737b0a62a86ed3605fa9e6165874
 config:a3d936cb15fe3b547bf6c82cf93d923f
 connections:942f9f88cb908c28bf5c19159fc5065b
-dags:5ad68174a1111563dff870a241914b30
+dags:ea477b70a1c3bcf8ed603cdbd11e5ab6
 dagrun:07035226eaaff0a557d3ad9aab4b41b5
 jobs:a5b644c5da8889443bb40ee10b599270
 pools:19efe105b9515ab1926ebcaf0e028d71
diff --git a/airflow-ctl/docs/images/output_dags.svg 
b/airflow-ctl/docs/images/output_dags.svg
index 70f2cda4cd0..78d61ab4506 100644
--- a/airflow-ctl/docs/images/output_dags.svg
+++ b/airflow-ctl/docs/images/output_dags.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 933 708.8" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 933 757.5999999999999" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -19,146 +19,154 @@
         font-weight: 700;
     }
 
-    .terminal-3245301439-matrix {
+    .terminal-4223325571-matrix {
         font-family: Fira Code, monospace;
         font-size: 20px;
         line-height: 24.4px;
         font-variant-east-asian: full-width;
     }
 
-    .terminal-3245301439-title {
+    .terminal-4223325571-title {
         font-size: 18px;
         font-weight: bold;
         font-family: arial;
     }
 
-    .terminal-3245301439-r1 { fill: #ff8700 }
-.terminal-3245301439-r2 { fill: #c5c8c6 }
-.terminal-3245301439-r3 { fill: #808080 }
-.terminal-3245301439-r4 { fill: #68a0b3 }
+    .terminal-4223325571-r1 { fill: #ff8700 }
+.terminal-4223325571-r2 { fill: #c5c8c6 }
+.terminal-4223325571-r3 { fill: #808080 }
+.terminal-4223325571-r4 { fill: #68a0b3 }
     </style>
 
     <defs>
-    <clipPath id="terminal-3245301439-clip-terminal">
-      <rect x="0" y="0" width="914.0" height="657.8" />
+    <clipPath id="terminal-4223325571-clip-terminal">
+      <rect x="0" y="0" width="914.0" height="706.5999999999999" />
     </clipPath>
-    <clipPath id="terminal-3245301439-line-0">
+    <clipPath id="terminal-4223325571-line-0">
     <rect x="0" y="1.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-1">
+<clipPath id="terminal-4223325571-line-1">
     <rect x="0" y="25.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-2">
+<clipPath id="terminal-4223325571-line-2">
     <rect x="0" y="50.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-3">
+<clipPath id="terminal-4223325571-line-3">
     <rect x="0" y="74.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-4">
+<clipPath id="terminal-4223325571-line-4">
     <rect x="0" y="99.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-5">
+<clipPath id="terminal-4223325571-line-5">
     <rect x="0" y="123.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-6">
+<clipPath id="terminal-4223325571-line-6">
     <rect x="0" y="147.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-7">
+<clipPath id="terminal-4223325571-line-7">
     <rect x="0" y="172.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-8">
+<clipPath id="terminal-4223325571-line-8">
     <rect x="0" y="196.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-9">
+<clipPath id="terminal-4223325571-line-9">
     <rect x="0" y="221.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-10">
+<clipPath id="terminal-4223325571-line-10">
     <rect x="0" y="245.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-11">
+<clipPath id="terminal-4223325571-line-11">
     <rect x="0" y="269.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-12">
+<clipPath id="terminal-4223325571-line-12">
     <rect x="0" y="294.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-13">
+<clipPath id="terminal-4223325571-line-13">
     <rect x="0" y="318.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-14">
+<clipPath id="terminal-4223325571-line-14">
     <rect x="0" y="343.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-15">
+<clipPath id="terminal-4223325571-line-15">
     <rect x="0" y="367.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-16">
+<clipPath id="terminal-4223325571-line-16">
     <rect x="0" y="391.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-17">
+<clipPath id="terminal-4223325571-line-17">
     <rect x="0" y="416.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-18">
+<clipPath id="terminal-4223325571-line-18">
     <rect x="0" y="440.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-19">
+<clipPath id="terminal-4223325571-line-19">
     <rect x="0" y="465.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-20">
+<clipPath id="terminal-4223325571-line-20">
     <rect x="0" y="489.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-21">
+<clipPath id="terminal-4223325571-line-21">
     <rect x="0" y="513.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-22">
+<clipPath id="terminal-4223325571-line-22">
     <rect x="0" y="538.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-23">
+<clipPath id="terminal-4223325571-line-23">
     <rect x="0" y="562.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-24">
+<clipPath id="terminal-4223325571-line-24">
     <rect x="0" y="587.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3245301439-line-25">
+<clipPath id="terminal-4223325571-line-25">
     <rect x="0" y="611.5" width="915" height="24.65"/>
             </clipPath>
+<clipPath id="terminal-4223325571-line-26">
+    <rect x="0" y="635.9" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-4223325571-line-27">
+    <rect x="0" y="660.3" width="915" height="24.65"/>
+            </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="931" height="706.8" rx="8"/>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="931" height="755.6" rx="8"/>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
             <circle cx="44" cy="0" r="7" fill="#28c840"/>
             </g>
         
-    <g transform="translate(9, 41)" 
clip-path="url(#terminal-3245301439-clip-terminal)">
+    <g transform="translate(9, 41)" 
clip-path="url(#terminal-4223325571-clip-terminal)">
     
-    <g class="terminal-3245301439-matrix">
-    <text class="terminal-3245301439-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-3245301439-line-0)">Usage:</text><text 
class="terminal-3245301439-r3" x="85.4" y="20" textLength="183" 
clip-path="url(#terminal-3245301439-line-0)">airflowctl&#160;dags</text><text 
class="terminal-3245301439-r2" x="268.4" y="20" textLength="24.4" 
clip-path="url(#terminal-3245301439-line-0)">&#160;[</text><text 
class="terminal-3245301439-r4" x="292.8" y="20" textLength="24.4" 
clip-path="url(#t [...]
-</text><text class="terminal-3245301439-r2" x="915" y="44.4" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-1)">
-</text><text class="terminal-3245301439-r2" x="0" y="68.8" textLength="280.6" 
clip-path="url(#terminal-3245301439-line-2)">Perform&#160;Dags&#160;operations</text><text
 class="terminal-3245301439-r2" x="915" y="68.8" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-2)">
-</text><text class="terminal-3245301439-r2" x="915" y="93.2" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-3)">
-</text><text class="terminal-3245301439-r1" x="0" y="117.6" textLength="256.2" 
clip-path="url(#terminal-3245301439-line-4)">Positional&#160;Arguments:</text><text
 class="terminal-3245301439-r2" x="915" y="117.6" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-4)">
-</text><text class="terminal-3245301439-r4" x="24.4" y="142" textLength="85.4" 
clip-path="url(#terminal-3245301439-line-5)">COMMAND</text><text 
class="terminal-3245301439-r2" x="915" y="142" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-5)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="166.4" 
textLength="73.2" 
clip-path="url(#terminal-3245301439-line-6)">delete</text><text 
class="terminal-3245301439-r2" x="268.4" y="166.4" textLength="268.4" 
clip-path="url(#terminal-3245301439-line-6)">Delete&#160;a&#160;Dag&#160;by&#160;its&#160;ID</text><text
 class="terminal-3245301439-r2" x="915" y="166.4" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-6)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="190.8" 
textLength="36.6" clip-path="url(#terminal-3245301439-line-7)">get</text><text 
class="terminal-3245301439-r2" x="268.4" y="190.8" textLength="292.8" 
clip-path="url(#terminal-3245301439-line-7)">Retrieve&#160;a&#160;Dag&#160;by&#160;its&#160;ID</text><text
 class="terminal-3245301439-r2" x="915" y="190.8" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-7)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="215.2" 
textLength="134.2" 
clip-path="url(#terminal-3245301439-line-8)">get-details</text><text 
class="terminal-3245301439-r2" x="268.4" y="215.2" textLength="475.8" 
clip-path="url(#terminal-3245301439-line-8)">Retrieve&#160;detailed&#160;information&#160;for&#160;a&#160;Dag</text><text
 class="terminal-3245301439-r2" x="915" y="215.2" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-8)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="239.6" 
textLength="195.2" 
clip-path="url(#terminal-3245301439-line-9)">get-import-error</text><text 
class="terminal-3245301439-r2" x="268.4" y="239.6" textLength="451.4" 
clip-path="url(#terminal-3245301439-line-9)">Retrieve&#160;a&#160;Dag&#160;import&#160;error&#160;by&#160;its&#160;ID</text><text
 class="terminal-3245301439-r2" x="915" y="239.6" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-9)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="264" 
textLength="109.8" 
clip-path="url(#terminal-3245301439-line-10)">get-stats</text><text 
class="terminal-3245301439-r2" x="268.4" y="264" textLength="536.8" 
clip-path="url(#terminal-3245301439-line-10)">Retrieve&#160;run&#160;statistics&#160;for&#160;one&#160;or&#160;more&#160;Dags</text><text
 class="terminal-3245301439-r2" x="915" y="264" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-10)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="288.4" 
textLength="97.6" 
clip-path="url(#terminal-3245301439-line-11)">get-tags</text><text 
class="terminal-3245301439-r2" x="268.4" y="288.4" textLength="366" 
clip-path="url(#terminal-3245301439-line-11)">List&#160;all&#160;tags&#160;used&#160;across&#160;Dags</text><text
 class="terminal-3245301439-r2" x="915" y="288.4" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-11)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="312.8" 
textLength="134.2" 
clip-path="url(#terminal-3245301439-line-12)">get-version</text><text 
class="terminal-3245301439-r2" x="268.4" y="312.8" textLength="439.2" 
clip-path="url(#terminal-3245301439-line-12)">Retrieve&#160;a&#160;specific&#160;version&#160;of&#160;a&#160;Dag</text><text
 class="terminal-3245301439-r2" x="915" y="312.8" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-12)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="337.2" 
textLength="48.8" 
clip-path="url(#terminal-3245301439-line-13)">list</text><text 
class="terminal-3245301439-r2" x="268.4" y="337.2" textLength="158.6" 
clip-path="url(#terminal-3245301439-line-13)">List&#160;all&#160;Dags</text><text
 class="terminal-3245301439-r2" x="915" y="337.2" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-13)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="361.6" 
textLength="219.6" 
clip-path="url(#terminal-3245301439-line-14)">list-import-errors</text><text 
class="terminal-3245301439-r2" x="915" y="361.6" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-14)">
-</text><text class="terminal-3245301439-r2" x="268.4" y="386" 
textLength="317.2" 
clip-path="url(#terminal-3245301439-line-15)">List&#160;all&#160;Dag&#160;import&#160;errors</text><text
 class="terminal-3245301439-r2" x="915" y="386" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-15)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="410.4" 
textLength="146.4" 
clip-path="url(#terminal-3245301439-line-16)">list-version</text><text 
class="terminal-3245301439-r2" x="268.4" y="410.4" textLength="317.2" 
clip-path="url(#terminal-3245301439-line-16)">List&#160;all&#160;versions&#160;of&#160;a&#160;Dag</text><text
 class="terminal-3245301439-r2" x="915" y="410.4" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-16)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="434.8" 
textLength="146.4" 
clip-path="url(#terminal-3245301439-line-17)">list-warning</text><text 
class="terminal-3245301439-r2" x="268.4" y="434.8" textLength="256.2" 
clip-path="url(#terminal-3245301439-line-17)">List&#160;all&#160;Dag&#160;warnings</text><text
 class="terminal-3245301439-r2" x="915" y="434.8" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-17)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="459.2" 
textLength="170.8" 
clip-path="url(#terminal-3245301439-line-18)">next-execution</text><text 
class="terminal-3245301439-r2" x="268.4" y="459.2" textLength="585.6" 
clip-path="url(#terminal-3245301439-line-18)">Show&#160;the&#160;next&#160;scheduled&#160;execution&#160;time&#160;for&#160;a&#160;Dag</text><text
 class="terminal-3245301439-r2" x="915" y="459.2" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-18)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="483.6" textLength="61" 
clip-path="url(#terminal-3245301439-line-19)">pause</text><text 
class="terminal-3245301439-r2" x="268.4" y="483.6" textLength="134.2" 
clip-path="url(#terminal-3245301439-line-19)">Pause&#160;a&#160;Dag</text><text 
class="terminal-3245301439-r2" x="915" y="483.6" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-19)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="508" textLength="61" 
clip-path="url(#terminal-3245301439-line-20)">state</text><text 
class="terminal-3245301439-r2" x="268.4" y="508" textLength="329.4" 
clip-path="url(#terminal-3245301439-line-20)">Get&#160;the&#160;status&#160;of&#160;a&#160;Dag&#160;run</text><text
 class="terminal-3245301439-r2" x="915" y="508" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-20)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="532.4" 
textLength="85.4" 
clip-path="url(#terminal-3245301439-line-21)">trigger</text><text 
class="terminal-3245301439-r2" x="268.4" y="532.4" textLength="256.2" 
clip-path="url(#terminal-3245301439-line-21)">Trigger&#160;a&#160;new&#160;Dag&#160;run</text><text
 class="terminal-3245301439-r2" x="915" y="532.4" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-21)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="556.8" 
textLength="85.4" 
clip-path="url(#terminal-3245301439-line-22)">unpause</text><text 
class="terminal-3245301439-r2" x="268.4" y="556.8" textLength="158.6" 
clip-path="url(#terminal-3245301439-line-22)">Unpause&#160;a&#160;Dag</text><text
 class="terminal-3245301439-r2" x="915" y="556.8" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-22)">
-</text><text class="terminal-3245301439-r4" x="48.8" y="581.2" 
textLength="73.2" 
clip-path="url(#terminal-3245301439-line-23)">update</text><text 
class="terminal-3245301439-r2" x="268.4" y="581.2" textLength="317.2" 
clip-path="url(#terminal-3245301439-line-23)">Update&#160;properties&#160;of&#160;a&#160;Dag</text><text
 class="terminal-3245301439-r2" x="915" y="581.2" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-23)">
-</text><text class="terminal-3245301439-r2" x="915" y="605.6" 
textLength="12.2" clip-path="url(#terminal-3245301439-line-24)">
-</text><text class="terminal-3245301439-r1" x="0" y="630" textLength="97.6" 
clip-path="url(#terminal-3245301439-line-25)">Options:</text><text 
class="terminal-3245301439-r2" x="915" y="630" textLength="12.2" 
clip-path="url(#terminal-3245301439-line-25)">
-</text><text class="terminal-3245301439-r4" x="24.4" y="654.4" 
textLength="24.4" clip-path="url(#terminal-3245301439-line-26)">-h</text><text 
class="terminal-3245301439-r2" x="48.8" y="654.4" textLength="24.4" 
clip-path="url(#terminal-3245301439-line-26)">,&#160;</text><text 
class="terminal-3245301439-r4" x="73.2" y="654.4" textLength="73.2" 
clip-path="url(#terminal-3245301439-line-26)">--help</text><text 
class="terminal-3245301439-r2" x="268.4" y="654.4" textLength="378.2" 
clip-path="ur [...]
+    <g class="terminal-4223325571-matrix">
+    <text class="terminal-4223325571-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-4223325571-line-0)">Usage:</text><text 
class="terminal-4223325571-r3" x="85.4" y="20" textLength="183" 
clip-path="url(#terminal-4223325571-line-0)">airflowctl&#160;dags</text><text 
class="terminal-4223325571-r2" x="268.4" y="20" textLength="24.4" 
clip-path="url(#terminal-4223325571-line-0)">&#160;[</text><text 
class="terminal-4223325571-r4" x="292.8" y="20" textLength="24.4" 
clip-path="url(#t [...]
+</text><text class="terminal-4223325571-r2" x="915" y="44.4" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-1)">
+</text><text class="terminal-4223325571-r2" x="0" y="68.8" textLength="280.6" 
clip-path="url(#terminal-4223325571-line-2)">Perform&#160;Dags&#160;operations</text><text
 class="terminal-4223325571-r2" x="915" y="68.8" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-2)">
+</text><text class="terminal-4223325571-r2" x="915" y="93.2" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-3)">
+</text><text class="terminal-4223325571-r1" x="0" y="117.6" textLength="256.2" 
clip-path="url(#terminal-4223325571-line-4)">Positional&#160;Arguments:</text><text
 class="terminal-4223325571-r2" x="915" y="117.6" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-4)">
+</text><text class="terminal-4223325571-r4" x="24.4" y="142" textLength="85.4" 
clip-path="url(#terminal-4223325571-line-5)">COMMAND</text><text 
class="terminal-4223325571-r2" x="915" y="142" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-5)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="166.4" textLength="61" 
clip-path="url(#terminal-4223325571-line-6)">clear</text><text 
class="terminal-4223325571-r2" x="268.4" y="166.4" textLength="646.6" 
clip-path="url(#terminal-4223325571-line-6)">Clear&#160;task&#160;instances&#160;for&#160;Dag&#160;runs&#160;selected&#160;by&#160;run&#160;ID,</text><text
 class="terminal-4223325571-r2" x="915" y="166.4" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-6)">
+</text><text class="terminal-4223325571-r2" x="0" y="190.8" textLength="390.4" 
clip-path="url(#terminal-4223325571-line-7)">partition&#160;key,&#160;or&#160;partition&#160;date</text><text
 class="terminal-4223325571-r2" x="915" y="190.8" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-7)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="215.2" 
textLength="73.2" 
clip-path="url(#terminal-4223325571-line-8)">delete</text><text 
class="terminal-4223325571-r2" x="268.4" y="215.2" textLength="268.4" 
clip-path="url(#terminal-4223325571-line-8)">Delete&#160;a&#160;Dag&#160;by&#160;its&#160;ID</text><text
 class="terminal-4223325571-r2" x="915" y="215.2" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-8)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="239.6" 
textLength="36.6" clip-path="url(#terminal-4223325571-line-9)">get</text><text 
class="terminal-4223325571-r2" x="268.4" y="239.6" textLength="292.8" 
clip-path="url(#terminal-4223325571-line-9)">Retrieve&#160;a&#160;Dag&#160;by&#160;its&#160;ID</text><text
 class="terminal-4223325571-r2" x="915" y="239.6" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-9)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="264" 
textLength="134.2" 
clip-path="url(#terminal-4223325571-line-10)">get-details</text><text 
class="terminal-4223325571-r2" x="268.4" y="264" textLength="475.8" 
clip-path="url(#terminal-4223325571-line-10)">Retrieve&#160;detailed&#160;information&#160;for&#160;a&#160;Dag</text><text
 class="terminal-4223325571-r2" x="915" y="264" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-10)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="288.4" 
textLength="195.2" 
clip-path="url(#terminal-4223325571-line-11)">get-import-error</text><text 
class="terminal-4223325571-r2" x="268.4" y="288.4" textLength="451.4" 
clip-path="url(#terminal-4223325571-line-11)">Retrieve&#160;a&#160;Dag&#160;import&#160;error&#160;by&#160;its&#160;ID</text><text
 class="terminal-4223325571-r2" x="915" y="288.4" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-11)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="312.8" 
textLength="109.8" 
clip-path="url(#terminal-4223325571-line-12)">get-stats</text><text 
class="terminal-4223325571-r2" x="268.4" y="312.8" textLength="536.8" 
clip-path="url(#terminal-4223325571-line-12)">Retrieve&#160;run&#160;statistics&#160;for&#160;one&#160;or&#160;more&#160;Dags</text><text
 class="terminal-4223325571-r2" x="915" y="312.8" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-12)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="337.2" 
textLength="97.6" 
clip-path="url(#terminal-4223325571-line-13)">get-tags</text><text 
class="terminal-4223325571-r2" x="268.4" y="337.2" textLength="366" 
clip-path="url(#terminal-4223325571-line-13)">List&#160;all&#160;tags&#160;used&#160;across&#160;Dags</text><text
 class="terminal-4223325571-r2" x="915" y="337.2" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-13)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="361.6" 
textLength="134.2" 
clip-path="url(#terminal-4223325571-line-14)">get-version</text><text 
class="terminal-4223325571-r2" x="268.4" y="361.6" textLength="439.2" 
clip-path="url(#terminal-4223325571-line-14)">Retrieve&#160;a&#160;specific&#160;version&#160;of&#160;a&#160;Dag</text><text
 class="terminal-4223325571-r2" x="915" y="361.6" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-14)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="386" textLength="48.8" 
clip-path="url(#terminal-4223325571-line-15)">list</text><text 
class="terminal-4223325571-r2" x="268.4" y="386" textLength="158.6" 
clip-path="url(#terminal-4223325571-line-15)">List&#160;all&#160;Dags</text><text
 class="terminal-4223325571-r2" x="915" y="386" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-15)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="410.4" 
textLength="219.6" 
clip-path="url(#terminal-4223325571-line-16)">list-import-errors</text><text 
class="terminal-4223325571-r2" x="915" y="410.4" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-16)">
+</text><text class="terminal-4223325571-r2" x="268.4" y="434.8" 
textLength="317.2" 
clip-path="url(#terminal-4223325571-line-17)">List&#160;all&#160;Dag&#160;import&#160;errors</text><text
 class="terminal-4223325571-r2" x="915" y="434.8" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-17)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="459.2" 
textLength="146.4" 
clip-path="url(#terminal-4223325571-line-18)">list-version</text><text 
class="terminal-4223325571-r2" x="268.4" y="459.2" textLength="317.2" 
clip-path="url(#terminal-4223325571-line-18)">List&#160;all&#160;versions&#160;of&#160;a&#160;Dag</text><text
 class="terminal-4223325571-r2" x="915" y="459.2" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-18)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="483.6" 
textLength="146.4" 
clip-path="url(#terminal-4223325571-line-19)">list-warning</text><text 
class="terminal-4223325571-r2" x="268.4" y="483.6" textLength="256.2" 
clip-path="url(#terminal-4223325571-line-19)">List&#160;all&#160;Dag&#160;warnings</text><text
 class="terminal-4223325571-r2" x="915" y="483.6" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-19)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="508" 
textLength="170.8" 
clip-path="url(#terminal-4223325571-line-20)">next-execution</text><text 
class="terminal-4223325571-r2" x="268.4" y="508" textLength="585.6" 
clip-path="url(#terminal-4223325571-line-20)">Show&#160;the&#160;next&#160;scheduled&#160;execution&#160;time&#160;for&#160;a&#160;Dag</text><text
 class="terminal-4223325571-r2" x="915" y="508" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-20)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="532.4" textLength="61" 
clip-path="url(#terminal-4223325571-line-21)">pause</text><text 
class="terminal-4223325571-r2" x="268.4" y="532.4" textLength="134.2" 
clip-path="url(#terminal-4223325571-line-21)">Pause&#160;a&#160;Dag</text><text 
class="terminal-4223325571-r2" x="915" y="532.4" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-21)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="556.8" textLength="61" 
clip-path="url(#terminal-4223325571-line-22)">state</text><text 
class="terminal-4223325571-r2" x="268.4" y="556.8" textLength="329.4" 
clip-path="url(#terminal-4223325571-line-22)">Get&#160;the&#160;status&#160;of&#160;a&#160;Dag&#160;run</text><text
 class="terminal-4223325571-r2" x="915" y="556.8" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-22)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="581.2" 
textLength="85.4" 
clip-path="url(#terminal-4223325571-line-23)">trigger</text><text 
class="terminal-4223325571-r2" x="268.4" y="581.2" textLength="256.2" 
clip-path="url(#terminal-4223325571-line-23)">Trigger&#160;a&#160;new&#160;Dag&#160;run</text><text
 class="terminal-4223325571-r2" x="915" y="581.2" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-23)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="605.6" 
textLength="85.4" 
clip-path="url(#terminal-4223325571-line-24)">unpause</text><text 
class="terminal-4223325571-r2" x="268.4" y="605.6" textLength="158.6" 
clip-path="url(#terminal-4223325571-line-24)">Unpause&#160;a&#160;Dag</text><text
 class="terminal-4223325571-r2" x="915" y="605.6" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-24)">
+</text><text class="terminal-4223325571-r4" x="48.8" y="630" textLength="73.2" 
clip-path="url(#terminal-4223325571-line-25)">update</text><text 
class="terminal-4223325571-r2" x="268.4" y="630" textLength="317.2" 
clip-path="url(#terminal-4223325571-line-25)">Update&#160;properties&#160;of&#160;a&#160;Dag</text><text
 class="terminal-4223325571-r2" x="915" y="630" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-25)">
+</text><text class="terminal-4223325571-r2" x="915" y="654.4" 
textLength="12.2" clip-path="url(#terminal-4223325571-line-26)">
+</text><text class="terminal-4223325571-r1" x="0" y="678.8" textLength="97.6" 
clip-path="url(#terminal-4223325571-line-27)">Options:</text><text 
class="terminal-4223325571-r2" x="915" y="678.8" textLength="12.2" 
clip-path="url(#terminal-4223325571-line-27)">
+</text><text class="terminal-4223325571-r4" x="24.4" y="703.2" 
textLength="24.4" clip-path="url(#terminal-4223325571-line-28)">-h</text><text 
class="terminal-4223325571-r2" x="48.8" y="703.2" textLength="24.4" 
clip-path="url(#terminal-4223325571-line-28)">,&#160;</text><text 
class="terminal-4223325571-r4" x="73.2" y="703.2" textLength="73.2" 
clip-path="url(#terminal-4223325571-line-28)">--help</text><text 
class="terminal-4223325571-r2" x="268.4" y="703.2" textLength="378.2" 
clip-path="ur [...]
 </text>
     </g>
     </g>
diff --git a/airflow-ctl/src/airflowctl/api/operations.py 
b/airflow-ctl/src/airflowctl/api/operations.py
index b2e431afd88..5a5556cbe45 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -91,7 +91,8 @@ T = TypeVar("T", bound=BaseModel)
 
 
 def _serialize_query_param(value: Any) -> Any:
-    if isinstance(value, datetime.datetime):
+    # datetime.datetime subclasses datetime.date, so this covers both.
+    if isinstance(value, datetime.date):
         return value.isoformat()
     return value
 
@@ -525,12 +526,16 @@ class DagRunOperations(BaseOperations):
         self,
         state: str | None = None,
         limit: int = 100,
+        offset: int | None = None,
         start_date: datetime.datetime | None = None,
         end_date: datetime.datetime | None = None,
         dag_id: str | None = None,
         logical_date_gte: datetime.datetime | None = None,
         logical_date_lte: datetime.datetime | None = None,
+        partition_date_gte: datetime.date | None = None,
+        partition_date_lte: datetime.date | None = None,
         order_by: str | None = None,
+        partition_key_pattern: str | None = None,
         *,
         suppress_error_log: bool = False,
     ) -> DAGRunCollectionResponse | ServerResponseError:
@@ -542,10 +547,16 @@ class DagRunOperations(BaseOperations):
             start_date: Filter Dag runs by start date (optional)
             end_date: Filter Dag runs by end date (optional)
             limit: Limit the number of results returned
+            offset: Offset to start returning results from
             dag_id: The Dag ID to filter by. If None, retrieves Dag runs for 
all Dags (using "~").
             logical_date_gte: Filter Dag runs with a logical date greater than 
or equal to this value.
             logical_date_lte: Filter Dag runs with a logical date less than or 
equal to this value.
+            partition_date_gte: Inclusive lower bound of the partition_date 
window, as a local
+                calendar day in the Dag's timetable timezone.
+            partition_date_lte: Inclusive upper bound of the partition_date 
window, as a local
+                calendar day in the Dag's timetable timezone.
             order_by: Order the results by the specified field.
+            partition_key_pattern: Filter Dag runs by partition key pattern.
             suppress_error_log: Skip client-side error logging, for callers 
handling the error themselves.
         """
         # Use "~" for all Dags if dag_id is not specified
@@ -554,12 +565,16 @@ class DagRunOperations(BaseOperations):
 
         params = _build_query_params(
             limit=limit,
+            offset=offset,
             state=str(state) if state is not None else None,
             start_date=start_date,
             end_date=end_date,
             logical_date_gte=logical_date_gte,
             logical_date_lte=logical_date_lte,
+            partition_date_gte=partition_date_gte,
+            partition_date_lte=partition_date_lte,
             order_by=order_by,
+            partition_key_pattern=partition_key_pattern,
         )
 
         self.response = self.client.get(
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py 
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index 899067d3410..258b565ce1b 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -292,6 +292,50 @@ ARG_LOGICAL_DATE = Arg(
     type=str,
     help="The logical date of the Dag run with a timezone offset (pass this or 
run_id, not both)",
 )
+ARG_DAG_RUN_ID = Arg(
+    flags=("--run-id",),
+    type=str,
+    help="The Dag run ID to clear",
+)
+ARG_DAG_PARTITION_KEY = Arg(
+    flags=("--partition-key",),
+    type=str,
+    help="The Dag run partition key to clear",
+)
+ARG_DAG_PARTITION_DATE_START = Arg(
+    flags=("--partition-date-start",),
+    type=str,
+    help=(
+        "Inclusive lower bound of the partition_date window, interpreted as a 
local calendar "
+        "day in the Dag's timetable timezone. Any time-of-day component is 
ignored."
+    ),
+)
+ARG_DAG_PARTITION_DATE_END = Arg(
+    flags=("--partition-date-end",),
+    type=str,
+    help=(
+        "Inclusive upper bound of the partition_date window, interpreted as a 
local calendar "
+        "day in the Dag's timetable timezone. Any time-of-day component is 
ignored."
+    ),
+)
+ARG_DAG_CLEAR_ONLY_FAILED = Arg(
+    flags=("-f", "--only-failed"),
+    default=False,
+    action="store_true",
+    help="Only clear failed task instances",
+)
+ARG_DAG_CLEAR_ONLY_RUNNING = Arg(
+    flags=("-r", "--only-running"),
+    default=False,
+    action="store_true",
+    help="Only clear running task instances",
+)
+ARG_DAG_CLEAR_YES = Arg(
+    flags=("-y", "--yes"),
+    default=False,
+    action="store_true",
+    help="Do not prompt to confirm clearing task instances",
+)
 
 # Task Commands Args
 ARG_RUN_ID = Arg(
@@ -536,6 +580,7 @@ class CommandFactory:
             "dict",
             "tuple",
             "set",
+            "datetime.date",
             "datetime.datetime",
         }
         # Handle Optional types (e.g., "datetime.datetime | None", "str | 
None")
@@ -566,6 +611,7 @@ class CommandFactory:
             "dict": json_dict_type,
             "tuple": tuple,
             "set": set,
+            "datetime.date": datetime.date,
             "datetime.datetime": datetime.datetime,
             "dict[str, typing.Any]": json_dict_type,
         }
@@ -1027,6 +1073,21 @@ CONNECTION_COMMANDS = (
 )
 
 DAG_COMMANDS = (
+    ActionCommand(
+        name="clear",
+        help="Clear task instances for Dag runs selected by run ID, partition 
key, or partition date",
+        func=lazy_load_command("airflowctl.ctl.commands.dag_command.clear"),
+        args=(
+            ARG_DAG_ID,
+            ARG_DAG_RUN_ID,
+            ARG_DAG_PARTITION_KEY,
+            ARG_DAG_PARTITION_DATE_START,
+            ARG_DAG_PARTITION_DATE_END,
+            ARG_DAG_CLEAR_ONLY_FAILED,
+            ARG_DAG_CLEAR_ONLY_RUNNING,
+            ARG_DAG_CLEAR_YES,
+        ),
+    ),
     ActionCommand(
         name="next-execution",
         help="Show the next scheduled execution time for a Dag",
diff --git a/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py 
b/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py
index d2acee3d24a..15307085f36 100644
--- a/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py
@@ -32,7 +32,11 @@ from airflowctl.api.client import (
     ServerResponseError,
     provide_api_client,
 )
-from airflowctl.api.datamodels.generated import DAGPatchBody, DAGRunResponse
+from airflowctl.api.datamodels.generated import (
+    ClearTaskInstancesBody,
+    DAGPatchBody,
+    DAGRunResponse,
+)
 from airflowctl.ctl.console_formatting import AirflowConsole
 
 
@@ -93,17 +97,17 @@ _NEXT_EXECUTION_FIELDS = (
 
 @provide_api_client(kind=ClientKind.CLI)
 def next_execution(args, api_client=NEW_API_CLIENT) -> dict | None:
-    """Show next scheduled execution time for a DAG."""
+    """Show next scheduled execution time for a Dag."""
     try:
         response = api_client.dags.get(dag_id=args.dag_id)
     except ServerResponseError as e:
-        rich.print(f"[red]Error retrieving DAG {args.dag_id}: {e}[/red]")
+        rich.print(f"[red]Error retrieving Dag {args.dag_id}: {e}[/red]")
         sys.exit(1)
 
     next_exec_data = {field: getattr(response, field) for field in 
_NEXT_EXECUTION_FIELDS}
 
     if all(value is None for value in next_exec_data.values()):
-        rich.print(f"[yellow]No upcoming run scheduled for DAG 
{args.dag_id}.[/yellow]")
+        rich.print(f"[yellow]No upcoming run scheduled for Dag 
{args.dag_id}.[/yellow]")
         return None
 
     result = next_exec_data
@@ -172,3 +176,157 @@ def state(args, api_client=NEW_API_CLIENT) -> None:
         rich.print(Text(f"{state_value}, {json.dumps(dag_run.conf)}"))
     else:
         rich.print(Text(state_value))
+
+
+def _parse_partition_date(value: str | None, *, option: str) -> datetime.date 
| None:
+    if value is None:
+        return None
+
+    try:
+        if "T" not in value and " " not in value:
+            return datetime.date.fromisoformat(value)
+
+        return datetime.datetime.fromisoformat(value.replace("Z", 
"+00:00")).date()
+    except ValueError:
+        raise SystemExit(
+            f"Invalid {option}: {value!r}. Use YYYY-MM-DD or ISO 8601 
datetime; only the date is used."
+        ) from None
+
+
+def _parse_and_validate_clear_args(args) -> tuple[datetime.date | None, 
datetime.date | None]:
+    """Reject invalid selector combinations and return the parsed 
partition_date window."""
+    has_run_id = args.run_id is not None
+    has_partition_key = args.partition_key is not None
+    has_partition_date = args.partition_date_start is not None or 
args.partition_date_end is not None
+
+    if sum([has_run_id, has_partition_key, has_partition_date]) != 1:
+        raise SystemExit(
+            "Exactly one selector is required: --run-id, --partition-key, "
+            "or --partition-date-start with --partition-date-end."
+        )
+    if has_partition_date and (args.partition_date_start is None or 
args.partition_date_end is None):
+        raise SystemExit("--partition-date-start and --partition-date-end must 
be provided together.")
+    if args.only_failed and args.only_running:
+        raise SystemExit("--only-failed and --only-running are mutually 
exclusive.")
+
+    partition_date_start = _parse_partition_date(args.partition_date_start, 
option="--partition-date-start")
+    partition_date_end = _parse_partition_date(args.partition_date_end, 
option="--partition-date-end")
+    if (
+        partition_date_start is not None
+        and partition_date_end is not None
+        and partition_date_start > partition_date_end
+    ):
+        raise SystemExit("--partition-date-start must be before or equal to 
--partition-date-end.")
+    return partition_date_start, partition_date_end
+
+
+def _list_dag_runs(api_client, dag_id: str, *, order_by: str = "logical_date", 
**filters) -> list:
+    dag_runs = []
+    offset = 0
+    while True:
+        response = api_client.dag_runs.list(
+            dag_id=dag_id,
+            offset=offset,
+            order_by=order_by,
+            **filters,
+        )
+        page_dag_runs = response.dag_runs
+        dag_runs.extend(page_dag_runs)
+
+        offset += len(page_dag_runs)
+        if not page_dag_runs or response.total_entries is None or offset >= 
response.total_entries:
+            return dag_runs
+
+
+def _get_dag_runs_to_clear(
+    args,
+    api_client,
+    partition_date_start: datetime.date | None,
+    partition_date_end: datetime.date | None,
+) -> list:
+    if args.run_id is not None:
+        return [api_client.dag_runs.get(dag_id=args.dag_id, 
dag_run_id=args.run_id)]
+
+    if args.partition_key is not None:
+        return [
+            dag_run
+            for dag_run in _list_dag_runs(
+                api_client,
+                args.dag_id,
+                order_by="partition_date",
+                partition_key_pattern=args.partition_key,
+            )
+            if dag_run.partition_key == args.partition_key
+        ]
+
+    return _list_dag_runs(
+        api_client,
+        args.dag_id,
+        order_by="partition_date",
+        partition_date_gte=partition_date_start,
+        partition_date_lte=partition_date_end,
+    )
+
+
+def _print_dag_runs_to_clear(dag_id: str, dag_runs: list) -> None:
+    rich.print(f"[yellow]Dag:[/yellow] {dag_id}")
+    rich.print(f"[yellow]Dag runs to clear:[/yellow] {len(dag_runs)}")
+    for dag_run in dag_runs:
+        logical_date = dag_run.logical_date.isoformat() if 
dag_run.logical_date is not None else "-"
+        partition_date = getattr(dag_run, "partition_date", None)
+        partition_date_display = partition_date.isoformat() if partition_date 
is not None else "-"
+        rich.print(
+            f"  - {dag_run.dag_run_id} (logical date: {logical_date}, 
partition date: {partition_date_display})"
+        )
+
+
+def _confirm_clear(dag_id: str, dag_runs: list) -> bool:
+    _print_dag_runs_to_clear(dag_id, dag_runs)
+    answer = input("Clear task instances for these Dag runs? [y/N] ")
+    return answer.strip().lower() in {"y", "yes"}
+
+
+def _get_dag_run_sort_key(dag_run) -> tuple[str, str, str]:
+    partition_date: datetime.datetime | None = getattr(dag_run, 
"partition_date", None)
+    logical_date: datetime.datetime | None = dag_run.logical_date
+    return (
+        partition_date.isoformat() if partition_date is not None else "",
+        logical_date.isoformat() if logical_date is not None else "",
+        dag_run.dag_run_id,
+    )
+
+
+@provide_api_client(kind=ClientKind.CLI)
+def clear(args, api_client=NEW_API_CLIENT) -> dict[str, int | bool]:
+    """Clear task instances for selected Dag runs."""
+    partition_date_start, partition_date_end = 
_parse_and_validate_clear_args(args)
+
+    dag_runs = _get_dag_runs_to_clear(args, api_client, partition_date_start, 
partition_date_end)
+    if not dag_runs:
+        rich.print(f"[yellow]No matching Dag runs found for 
{args.dag_id}.[/yellow]")
+        return {"dag_run_count": 0, "cleared_task_instances": 0}
+
+    dag_runs = sorted(dag_runs, key=_get_dag_run_sort_key)
+
+    if not args.yes and not _confirm_clear(args.dag_id, dag_runs):
+        rich.print("[yellow]Cancelled.[/yellow]")
+        return {"dag_run_count": len(dag_runs), "cleared_task_instances": 0, 
"cancelled": True}
+
+    cleared_task_instances = 0
+    for dag_run in dag_runs:
+        response = api_client.tasks.clear(
+            dag_id=args.dag_id,
+            clear_task_instances=ClearTaskInstancesBody(
+                dag_run_id=dag_run.dag_run_id,
+                dry_run=False,
+                only_failed=args.only_failed,
+                only_running=args.only_running,
+                reset_dag_runs=True,
+            ),
+        )
+        cleared_task_instances += response.total_entries or 0
+
+    rich.print(
+        f"[green]Cleared {cleared_task_instances} task instance(s) across 
{len(dag_runs)} Dag run(s).[/green]"
+    )
+    return {"dag_run_count": len(dag_runs), "cleared_task_instances": 
cleared_task_instances}
diff --git a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml 
b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
index 9c2c591417b..395b8cb3e13 100644
--- a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
+++ b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
@@ -52,6 +52,7 @@ connections:
   test: "Test connectivity for a given connection"
 
 dags:
+  clear: "Clear task instances for Dag runs selected by run ID, partition key, 
or partition date"
   get: "Retrieve a Dag by its ID"
   get-details: "Retrieve detailed information for a Dag"
   get-tags: "List all tags used across Dags"
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py 
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index b18beddd677..d20f08e77f4 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -1301,6 +1301,43 @@ class TestDagRunOperations:
         )
         assert response == self.dag_run_collection_response
 
+    def test_list_with_clear_filters(self):
+        logical_date_start = datetime.datetime(2025, 1, 1, 0, 0, 0, 
tzinfo=datetime.timezone.utc)
+        logical_date_end = datetime.datetime(2025, 1, 2, 23, 59, 59, 
tzinfo=datetime.timezone.utc)
+        partition_day_start = datetime.date(2025, 2, 1)
+        partition_day_end = datetime.date(2025, 2, 4)
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == f"/api/v2/dags/{self.dag_id}/dagRuns"
+            # Asserted exactly: a name the endpoint does not declare is 
silently ignored by
+            # FastAPI, so an unfiltered list would come back and the caller 
would act on it.
+            assert dict(request.url.params) == {
+                "limit": "50",
+                "offset": "100",
+                "logical_date_gte": logical_date_start.isoformat(),
+                "logical_date_lte": logical_date_end.isoformat(),
+                "partition_date_gte": partition_day_start.isoformat(),
+                "partition_date_lte": partition_day_end.isoformat(),
+                "order_by": "logical_date",
+                "partition_key_pattern": "2025-01-01",
+            }
+            return httpx.Response(200, 
json=json.loads(self.dag_run_collection_response.model_dump_json()))
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.dag_runs.list(
+            dag_id=self.dag_id,
+            limit=50,
+            offset=100,
+            logical_date_gte=logical_date_start,
+            logical_date_lte=logical_date_end,
+            partition_date_gte=partition_day_start,
+            partition_date_lte=partition_day_end,
+            order_by="logical_date",
+            partition_key_pattern="2025-01-01",
+        )
+
+        assert response == self.dag_run_collection_response
+
     @pytest.mark.parametrize(
         (
             "dag_id_input",
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py 
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py
index 62c402afb6c..1425868e282 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py
@@ -17,14 +17,16 @@
 from __future__ import annotations
 
 import datetime
+from types import SimpleNamespace
 from unittest import mock
+from unittest.mock import Mock, call, create_autospec
 
 import httpx
 import pytest
 
 from airflowctl.api.client import ClientKind
-from airflowctl.api.datamodels.generated import DAGResponse
-from airflowctl.api.operations import ServerResponseError
+from airflowctl.api.datamodels.generated import ClearTaskInstancesBody, 
DAGResponse
+from airflowctl.api.operations import DagRunOperations, ServerResponseError, 
TasksOperations
 from airflowctl.ctl import cli_parser
 from airflowctl.ctl.commands import dag_command
 
@@ -133,6 +135,28 @@ class TestDagCommands:
         is_stale=False,
     )
 
+    @staticmethod
+    def _dag_run(
+        dag_run_id: str,
+        *,
+        logical_date: datetime.datetime | None = datetime.datetime(2025, 1, 1, 
0, 0, 0),
+        partition_key: str | None = None,
+        partition_date: datetime.datetime | None = datetime.datetime(2025, 1, 
1, 0, 0, 0),
+    ):
+        return SimpleNamespace(
+            dag_run_id=dag_run_id,
+            logical_date=logical_date,
+            partition_key=partition_key,
+            partition_date=partition_date,
+        )
+
+    @staticmethod
+    def _api_client_mock():
+        api_client = Mock(spec_set=["dag_runs", "tasks"])
+        api_client.dag_runs = create_autospec(DagRunOperations, instance=True, 
spec_set=True)
+        api_client.tasks = create_autospec(TasksOperations, instance=True, 
spec_set=True)
+        return api_client
+
     def test_pause_dag(self, api_client_maker, monkeypatch):
         api_client = api_client_maker(
             path=f"/api/v2/dags/{self.dag_id}",
@@ -380,3 +404,280 @@ class TestDagCommands:
 
         assert ctx.value is error
         api_client.dag_runs.get.assert_not_called()
+
+    def test_clear_by_run_id(self):
+        api_client = self._api_client_mock()
+        api_client.dag_runs.get.return_value = 
self._dag_run("scheduled__2025-01-01")
+        api_client.tasks.clear.return_value = SimpleNamespace(total_entries=2)
+
+        result = dag_command.clear(
+            self.parser.parse_args(
+                ["dags", "clear", self.dag_id, "--run-id", 
"scheduled__2025-01-01", "--yes"]
+            ),
+            api_client=api_client,
+        )
+
+        assert result == {"dag_run_count": 1, "cleared_task_instances": 2}
+        api_client.dag_runs.get.assert_called_once_with(
+            dag_id=self.dag_id, dag_run_id="scheduled__2025-01-01"
+        )
+        api_client.tasks.clear.assert_called_once_with(
+            dag_id=self.dag_id,
+            clear_task_instances=ClearTaskInstancesBody(
+                dag_run_id="scheduled__2025-01-01",
+                dry_run=False,
+                only_failed=False,
+                only_running=False,
+                reset_dag_runs=True,
+            ),
+        )
+
+    def test_clear_by_partition_key_filters_exact_match_and_paginates(self):
+        api_client = self._api_client_mock()
+        api_client.dag_runs.list.side_effect = [
+            SimpleNamespace(
+                dag_runs=[
+                    self._dag_run(
+                        "scheduled__2025-01-01",
+                        logical_date=datetime.datetime(2025, 1, 1, 0, 0, 0),
+                        partition_key="customer-a",
+                    ),
+                    self._dag_run(
+                        "scheduled__2025-01-02",
+                        logical_date=datetime.datetime(2025, 1, 2, 0, 0, 0),
+                        partition_key="customer-a-suffix",
+                    ),
+                ],
+                total_entries=3,
+            ),
+            SimpleNamespace(
+                dag_runs=[
+                    self._dag_run(
+                        "scheduled__2025-01-03",
+                        logical_date=datetime.datetime(2025, 1, 3, 0, 0, 0),
+                        partition_key="customer-a",
+                    )
+                ],
+                total_entries=3,
+            ),
+        ]
+        api_client.tasks.clear.side_effect = [
+            SimpleNamespace(total_entries=1),
+            SimpleNamespace(total_entries=2),
+        ]
+
+        result = dag_command.clear(
+            self.parser.parse_args(["dags", "clear", self.dag_id, 
"--partition-key", "customer-a", "--yes"]),
+            api_client=api_client,
+        )
+
+        assert result == {"dag_run_count": 2, "cleared_task_instances": 3}
+        assert api_client.dag_runs.list.call_args_list == [
+            call(
+                dag_id=self.dag_id,
+                offset=0,
+                order_by="partition_date",
+                partition_key_pattern="customer-a",
+            ),
+            call(
+                dag_id=self.dag_id,
+                offset=2,
+                order_by="partition_date",
+                partition_key_pattern="customer-a",
+            ),
+        ]
+        assert api_client.tasks.clear.call_args_list == [
+            call(
+                dag_id=self.dag_id,
+                clear_task_instances=ClearTaskInstancesBody(
+                    dag_run_id="scheduled__2025-01-01",
+                    dry_run=False,
+                    only_failed=False,
+                    only_running=False,
+                    reset_dag_runs=True,
+                ),
+            ),
+            call(
+                dag_id=self.dag_id,
+                clear_task_instances=ClearTaskInstancesBody(
+                    dag_run_id="scheduled__2025-01-03",
+                    dry_run=False,
+                    only_failed=False,
+                    only_running=False,
+                    reset_dag_runs=True,
+                ),
+            ),
+        ]
+
+    def test_clear_by_partition_date_uses_partition_date_filters(self):
+        api_client = self._api_client_mock()
+        api_client.dag_runs.list.return_value = SimpleNamespace(
+            dag_runs=[self._dag_run("scheduled__2025-01-01")],
+            total_entries=1,
+        )
+        api_client.tasks.clear.return_value = SimpleNamespace(total_entries=1)
+
+        result = dag_command.clear(
+            self.parser.parse_args(
+                [
+                    "dags",
+                    "clear",
+                    self.dag_id,
+                    "--partition-date-start",
+                    "2025-01-01",
+                    "--partition-date-end",
+                    "2025-01-02",
+                    "--only-running",
+                    "--yes",
+                ]
+            ),
+            api_client=api_client,
+        )
+
+        assert result == {"dag_run_count": 1, "cleared_task_instances": 1}
+        api_client.dag_runs.list.assert_called_once_with(
+            dag_id=self.dag_id,
+            offset=0,
+            order_by="partition_date",
+            partition_date_gte=datetime.date(2025, 1, 1),
+            partition_date_lte=datetime.date(2025, 1, 2),
+        )
+        api_client.tasks.clear.assert_called_once_with(
+            dag_id=self.dag_id,
+            clear_task_instances=ClearTaskInstancesBody(
+                dag_run_id="scheduled__2025-01-01",
+                dry_run=False,
+                only_failed=False,
+                only_running=True,
+                reset_dag_runs=True,
+            ),
+        )
+
+    def test_clear_by_partition_date_uses_calendar_dates_from_datetimes(self):
+        api_client = self._api_client_mock()
+        api_client.dag_runs.list.return_value = SimpleNamespace(
+            dag_runs=[self._dag_run("scheduled__2025-01-01")],
+            total_entries=1,
+        )
+        api_client.tasks.clear.return_value = SimpleNamespace(total_entries=1)
+
+        result = dag_command.clear(
+            self.parser.parse_args(
+                [
+                    "dags",
+                    "clear",
+                    self.dag_id,
+                    "--partition-date-start",
+                    "2025-01-01T08:00:00+08:00",
+                    "--partition-date-end",
+                    "2025-01-02T17:00:00+08:00",
+                    "--yes",
+                ]
+            ),
+            api_client=api_client,
+        )
+
+        assert result == {"dag_run_count": 1, "cleared_task_instances": 1}
+        api_client.dag_runs.list.assert_called_once_with(
+            dag_id=self.dag_id,
+            offset=0,
+            order_by="partition_date",
+            partition_date_gte=datetime.date(2025, 1, 1),
+            partition_date_lte=datetime.date(2025, 1, 2),
+        )
+
+    def 
test_clear_by_partition_date_accepts_naive_datetime_as_calendar_date(self):
+        api_client = self._api_client_mock()
+        api_client.dag_runs.list.return_value = SimpleNamespace(
+            dag_runs=[self._dag_run("scheduled__2025-01-01")],
+            total_entries=1,
+        )
+        api_client.tasks.clear.return_value = SimpleNamespace(total_entries=1)
+
+        result = dag_command.clear(
+            self.parser.parse_args(
+                [
+                    "dags",
+                    "clear",
+                    self.dag_id,
+                    "--partition-date-start",
+                    "2025-01-01T00:00:00",
+                    "--partition-date-end",
+                    "2025-01-02T00:00:00",
+                    "--yes",
+                ]
+            ),
+            api_client=api_client,
+        )
+
+        assert result == {"dag_run_count": 1, "cleared_task_instances": 1}
+        api_client.dag_runs.list.assert_called_once_with(
+            dag_id=self.dag_id,
+            offset=0,
+            order_by="partition_date",
+            partition_date_gte=datetime.date(2025, 1, 1),
+            partition_date_lte=datetime.date(2025, 1, 2),
+        )
+
+    def test_clear_prompts_before_clearing(self, monkeypatch):
+        api_client = self._api_client_mock()
+        api_client.dag_runs.get.return_value = 
self._dag_run("scheduled__2025-01-01")
+        monkeypatch.setattr("builtins.input", lambda _: "n")
+
+        result = dag_command.clear(
+            self.parser.parse_args(["dags", "clear", self.dag_id, "--run-id", 
"scheduled__2025-01-01"]),
+            api_client=api_client,
+        )
+
+        assert result == {"dag_run_count": 1, "cleared_task_instances": 0, 
"cancelled": True}
+        api_client.tasks.clear.assert_not_called()
+
+    @pytest.mark.parametrize(
+        "command",
+        [
+            ["dags", "clear", dag_id],
+            ["dags", "clear", dag_id, "--run-id", "run", "--partition-key", 
"key"],
+            [
+                "dags",
+                "clear",
+                dag_id,
+                "--partition-date-start",
+                "2025-01-01",
+            ],
+            [
+                "dags",
+                "clear",
+                dag_id,
+                "--run-id",
+                "run",
+                "--only-failed",
+                "--only-running",
+            ],
+            [
+                "dags",
+                "clear",
+                dag_id,
+                "--partition-date-start",
+                "2025-01-02",
+                "--partition-date-end",
+                "2025-01-01",
+            ],
+            [
+                "dags",
+                "clear",
+                dag_id,
+                "--partition-date-start",
+                "not-a-date",
+                "--partition-date-end",
+                "2025-01-01",
+            ],
+        ],
+    )
+    def test_clear_validates_selectors(self, command):
+        api_client = self._api_client_mock()
+
+        with pytest.raises(SystemExit):
+            dag_command.clear(self.parser.parse_args(command), 
api_client=api_client)
+
+        api_client.dag_runs.list.assert_not_called()
+        api_client.tasks.clear.assert_not_called()

Reply via email to