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

Lee-W pushed a commit to branch airflow-ctl/v0-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/airflow-ctl/v0-1-test by this 
push:
     new cb4d11f4a43 Add airflowctl tasks states-for-dag-run command (#69366) 
(#70276)
cb4d11f4a43 is described below

commit cb4d11f4a43372cb6a2cdd529cd3671841c0f93e
Author: Wei Lee <[email protected]>
AuthorDate: Thu Oct 29 16:08:02 2026 +0900

    Add airflowctl tasks states-for-dag-run command (#69366) (#70276)
    
    Co-authored-by: PoAn Yang <[email protected]>
    Signed-off-by: PoAn Yang <[email protected]>
---
 .../airflowctl_tests/test_airflowctl_commands.py   |   6 +
 airflow-ctl/docs/conf.py                           |   5 +-
 airflow-ctl/docs/howto/index.rst                   |  14 +
 airflow-ctl/docs/images/command_hashes.txt         |   4 +-
 airflow-ctl/docs/images/output_main.svg            | 142 ++++++-----
 airflow-ctl/docs/images/output_taskinstances.svg   |  97 +++++++
 airflow-ctl/docs/images/output_tasks.svg           | 105 ++++++++
 airflow-ctl/docs/spelling_wordlist.txt             |   1 +
 airflow-ctl/src/airflowctl/api/client.py           |  14 +
 airflow-ctl/src/airflowctl/api/operations.py       |  30 +++
 airflow-ctl/src/airflowctl/ctl/cli_config.py       |  92 +++++--
 .../src/airflowctl/ctl/commands/task_command.py    |  99 +++++++
 airflow-ctl/src/airflowctl/ctl/help_texts.yaml     |   6 +
 .../tests/airflow_ctl/api/test_operations.py       |  81 ++++++
 .../airflow_ctl/ctl/commands/test_task_command.py  | 284 +++++++++++++++++++++
 .../tests/airflow_ctl/ctl/test_cli_config.py       |  51 +++-
 .../in_container/run_capture_airflowctl_help.py    |   2 +
 17 files changed, 947 insertions(+), 86 deletions(-)

diff --git 
a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py 
b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
index 7862b163e59..39ed27cb25d 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -97,6 +97,12 @@ TEST_COMMANDS = [
     "dags update example_bash_operator --no-is-paused",
     # Dag Run commands
     "dagrun list --dag-id example_bash_operator --state success --limit=1",
+    # Tasks commands
+    'tasks states-for-dag-run example_bash_operator "manual__{date_param}"',
+    'tasks states-for-dag-run example_bash_operator --logical-date 
"{date_param}"',
+    'tasks clear example_bash_operator --dag-run-id "manual__{date_param}" 
--task-ids runme_0 -o json',
+    # Task Instances commands
+    'taskinstances list example_bash_operator "manual__{date_param}"',
     # XCom commands - need a Dag run with completed tasks
     'xcom add example_bash_operator "manual__{date_param}" runme_0 {xcom_key} 
\'{{"test": "value"}}\'',
     'xcom get example_bash_operator "manual__{date_param}" runme_0 {xcom_key}',
diff --git a/airflow-ctl/docs/conf.py b/airflow-ctl/docs/conf.py
index 5a19b2adea3..a40d685c5ca 100644
--- a/airflow-ctl/docs/conf.py
+++ b/airflow-ctl/docs/conf.py
@@ -290,7 +290,10 @@ exampleinclude_sourceroot = os.path.abspath("..")
 redirects_file = "redirects.txt"
 
 # -- Options for sphinxcontrib-spelling 
----------------------------------------
-spelling_word_list_filename = [SPELLING_WORDLIST_PATH.as_posix()]
+spelling_word_list_filename = [
+    SPELLING_WORDLIST_PATH.as_posix(),
+    (pathlib.Path(__file__).parent / "spelling_wordlist.txt").as_posix(),
+]
 spelling_exclude_patterns = ["project.rst", "changelog.rst"]
 
 spelling_ignore_contributor_names = False
diff --git a/airflow-ctl/docs/howto/index.rst b/airflow-ctl/docs/howto/index.rst
index fdb239f32e2..91274e0c1e0 100644
--- a/airflow-ctl/docs/howto/index.rst
+++ b/airflow-ctl/docs/howto/index.rst
@@ -213,6 +213,20 @@ These visual references show the full command syntax, 
options, and parameters fo
   :width: 60%
   :alt: airflowctl Providers Command
 
+**Task Instances**
+''''''''''''''''''
+.. image:: ../images/output_taskinstances.svg
+  :target: 
https://raw.githubusercontent.com/apache/airflow/main/airflow-ctl/docs/images/output_taskinstances.svg
+  :width: 60%
+  :alt: airflowctl Task Instances Command
+
+**Tasks**
+'''''''''
+.. image:: ../images/output_tasks.svg
+  :target: 
https://raw.githubusercontent.com/apache/airflow/main/airflow-ctl/docs/images/output_tasks.svg
+  :width: 60%
+  :alt: airflowctl Tasks Command
+
 **Variables**
 '''''''''''''
 .. image:: ../images/output_variables.svg
diff --git a/airflow-ctl/docs/images/command_hashes.txt 
b/airflow-ctl/docs/images/command_hashes.txt
index 87d05b1991b..ae6829e440f 100644
--- a/airflow-ctl/docs/images/command_hashes.txt
+++ b/airflow-ctl/docs/images/command_hashes.txt
@@ -1,4 +1,4 @@
-main:27a22c00dcf32e7a1a4f06672dc8e3c8
+main:2c358f5fc894541cab11854dcd67658a
 assets:4ec546634ac98b220969450db85b98d1
 auth:5aeefc79ee662adbd7cbfb51e1c1de4b
 backfill:74c8737b0a62a86ed3605fa9e6165874
@@ -9,6 +9,8 @@ dagrun:07035226eaaff0a557d3ad9aab4b41b5
 jobs:a5b644c5da8889443bb40ee10b599270
 pools:19efe105b9515ab1926ebcaf0e028d71
 providers:34502fe09dc0b8b0a13e7e46efdffda6
+taskinstances:7e323968c0b585287c2a4ab4339aee1f
+tasks:e2ed0ab7f67ebacf4be87b5f396ff782
 variables:f8fc76d3d398b2780f4e97f7cd816646
 version:31f4efdf8de0dbaaa4fac71ff7efecc3
 plugins:4864fd8f356704bd2b3cd1aec3567e35
diff --git a/airflow-ctl/docs/images/output_main.svg 
b/airflow-ctl/docs/images/output_main.svg
index f586877bce8..2a39e7e0ce2 100644
--- a/airflow-ctl/docs/images/output_main.svg
+++ b/airflow-ctl/docs/images/output_main.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 782.0" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -19,146 +19,158 @@
         font-weight: 700;
     }
 
-    .terminal-2670787131-matrix {
+    .terminal-151424782-matrix {
         font-family: Fira Code, monospace;
         font-size: 20px;
         line-height: 24.4px;
         font-variant-east-asian: full-width;
     }
 
-    .terminal-2670787131-title {
+    .terminal-151424782-title {
         font-size: 18px;
         font-weight: bold;
         font-family: arial;
     }
 
-    .terminal-2670787131-r1 { fill: #ff8700 }
-.terminal-2670787131-r2 { fill: #c5c8c6 }
-.terminal-2670787131-r3 { fill: #808080 }
-.terminal-2670787131-r4 { fill: #68a0b3 }
+    .terminal-151424782-r1 { fill: #ff8700 }
+.terminal-151424782-r2 { fill: #c5c8c6 }
+.terminal-151424782-r3 { fill: #808080 }
+.terminal-151424782-r4 { fill: #68a0b3 }
     </style>
 
     <defs>
-    <clipPath id="terminal-2670787131-clip-terminal">
-      <rect x="0" y="0" width="914.0" height="657.8" />
+    <clipPath id="terminal-151424782-clip-terminal">
+      <rect x="0" y="0" width="914.0" height="731.0" />
     </clipPath>
-    <clipPath id="terminal-2670787131-line-0">
+    <clipPath id="terminal-151424782-line-0">
     <rect x="0" y="1.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-1">
+<clipPath id="terminal-151424782-line-1">
     <rect x="0" y="25.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-2">
+<clipPath id="terminal-151424782-line-2">
     <rect x="0" y="50.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-3">
+<clipPath id="terminal-151424782-line-3">
     <rect x="0" y="74.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-4">
+<clipPath id="terminal-151424782-line-4">
     <rect x="0" y="99.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-5">
+<clipPath id="terminal-151424782-line-5">
     <rect x="0" y="123.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-6">
+<clipPath id="terminal-151424782-line-6">
     <rect x="0" y="147.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-7">
+<clipPath id="terminal-151424782-line-7">
     <rect x="0" y="172.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-8">
+<clipPath id="terminal-151424782-line-8">
     <rect x="0" y="196.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-9">
+<clipPath id="terminal-151424782-line-9">
     <rect x="0" y="221.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-10">
+<clipPath id="terminal-151424782-line-10">
     <rect x="0" y="245.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-11">
+<clipPath id="terminal-151424782-line-11">
     <rect x="0" y="269.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-12">
+<clipPath id="terminal-151424782-line-12">
     <rect x="0" y="294.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-13">
+<clipPath id="terminal-151424782-line-13">
     <rect x="0" y="318.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-14">
+<clipPath id="terminal-151424782-line-14">
     <rect x="0" y="343.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-15">
+<clipPath id="terminal-151424782-line-15">
     <rect x="0" y="367.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-16">
+<clipPath id="terminal-151424782-line-16">
     <rect x="0" y="391.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-17">
+<clipPath id="terminal-151424782-line-17">
     <rect x="0" y="416.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-18">
+<clipPath id="terminal-151424782-line-18">
     <rect x="0" y="440.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-19">
+<clipPath id="terminal-151424782-line-19">
     <rect x="0" y="465.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-20">
+<clipPath id="terminal-151424782-line-20">
     <rect x="0" y="489.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-21">
+<clipPath id="terminal-151424782-line-21">
     <rect x="0" y="513.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-22">
+<clipPath id="terminal-151424782-line-22">
     <rect x="0" y="538.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-23">
+<clipPath id="terminal-151424782-line-23">
     <rect x="0" y="562.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-24">
+<clipPath id="terminal-151424782-line-24">
     <rect x="0" y="587.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-2670787131-line-25">
+<clipPath id="terminal-151424782-line-25">
     <rect x="0" y="611.5" width="915" height="24.65"/>
             </clipPath>
+<clipPath id="terminal-151424782-line-26">
+    <rect x="0" y="635.9" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-151424782-line-27">
+    <rect x="0" y="660.3" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-151424782-line-28">
+    <rect x="0" y="684.7" 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="780" 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-2670787131-clip-terminal)">
+    <g transform="translate(9, 41)" 
clip-path="url(#terminal-151424782-clip-terminal)">
     
-    <g class="terminal-2670787131-matrix">
-    <text class="terminal-2670787131-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-2670787131-line-0)">Usage:</text><text 
class="terminal-2670787131-r3" x="85.4" y="20" textLength="122" 
clip-path="url(#terminal-2670787131-line-0)">airflowctl</text><text 
class="terminal-2670787131-r2" x="207.4" y="20" textLength="24.4" 
clip-path="url(#terminal-2670787131-line-0)">&#160;[</text><text 
class="terminal-2670787131-r4" x="231.8" y="20" textLength="24.4" 
clip-path="url(#terminal-26 [...]
-</text><text class="terminal-2670787131-r2" x="915" y="44.4" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-1)">
-</text><text class="terminal-2670787131-r1" x="0" y="68.8" textLength="256.2" 
clip-path="url(#terminal-2670787131-line-2)">Positional&#160;Arguments:</text><text
 class="terminal-2670787131-r2" x="915" y="68.8" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-2)">
-</text><text class="terminal-2670787131-r4" x="24.4" y="93.2" 
textLength="195.2" 
clip-path="url(#terminal-2670787131-line-3)">GROUP_OR_COMMAND</text><text 
class="terminal-2670787131-r2" x="915" y="93.2" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-3)">
-</text><text class="terminal-2670787131-r2" x="915" y="117.6" 
textLength="12.2" clip-path="url(#terminal-2670787131-line-4)">
-</text><text class="terminal-2670787131-r4" x="0" y="142" textLength="122" 
clip-path="url(#terminal-2670787131-line-5)">&#160;&#160;&#160;&#160;Groups</text><text
 class="terminal-2670787131-r2" x="915" y="142" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-5)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="166.4" 
textLength="73.2" 
clip-path="url(#terminal-2670787131-line-6)">assets</text><text 
class="terminal-2670787131-r2" x="244" y="166.4" textLength="305" 
clip-path="url(#terminal-2670787131-line-6)">Perform&#160;Assets&#160;operations</text><text
 class="terminal-2670787131-r2" x="915" y="166.4" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-6)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="190.8" 
textLength="48.8" clip-path="url(#terminal-2670787131-line-7)">auth</text><text 
class="terminal-2670787131-r2" x="244" y="190.8" textLength="646.6" 
clip-path="url(#terminal-2670787131-line-7)">Manage&#160;authentication&#160;for&#160;CLI.&#160;Either&#160;pass&#160;token&#160;from</text><text
 class="terminal-2670787131-r2" x="915" y="190.8" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-7)">
-</text><text class="terminal-2670787131-r2" x="244" y="215.2" 
textLength="622.2" 
clip-path="url(#terminal-2670787131-line-8)">environment&#160;variable/parameter&#160;or&#160;pass&#160;username&#160;and</text><text
 class="terminal-2670787131-r2" x="915" y="215.2" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-8)">
-</text><text class="terminal-2670787131-r2" x="244" y="239.6" 
textLength="109.8" 
clip-path="url(#terminal-2670787131-line-9)">password.</text><text 
class="terminal-2670787131-r2" x="915" y="239.6" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-9)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="264" textLength="97.6" 
clip-path="url(#terminal-2670787131-line-10)">backfill</text><text 
class="terminal-2670787131-r2" x="244" y="264" textLength="329.4" 
clip-path="url(#terminal-2670787131-line-10)">Perform&#160;Backfill&#160;operations</text><text
 class="terminal-2670787131-r2" x="915" y="264" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-10)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="288.4" 
textLength="73.2" 
clip-path="url(#terminal-2670787131-line-11)">config</text><text 
class="terminal-2670787131-r2" x="244" y="288.4" textLength="305" 
clip-path="url(#terminal-2670787131-line-11)">Perform&#160;Config&#160;operations</text><text
 class="terminal-2670787131-r2" x="915" y="288.4" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-11)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="312.8" 
textLength="134.2" 
clip-path="url(#terminal-2670787131-line-12)">connections</text><text 
class="terminal-2670787131-r2" x="244" y="312.8" textLength="366" 
clip-path="url(#terminal-2670787131-line-12)">Perform&#160;Connections&#160;operations</text><text
 class="terminal-2670787131-r2" x="915" y="312.8" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-12)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="337.2" 
textLength="73.2" 
clip-path="url(#terminal-2670787131-line-13)">dagrun</text><text 
class="terminal-2670787131-r2" x="244" y="337.2" textLength="305" 
clip-path="url(#terminal-2670787131-line-13)">Perform&#160;DagRun&#160;operations</text><text
 class="terminal-2670787131-r2" x="915" y="337.2" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-13)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="361.6" 
textLength="48.8" 
clip-path="url(#terminal-2670787131-line-14)">dags</text><text 
class="terminal-2670787131-r2" x="244" y="361.6" textLength="280.6" 
clip-path="url(#terminal-2670787131-line-14)">Perform&#160;Dags&#160;operations</text><text
 class="terminal-2670787131-r2" x="915" y="361.6" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-14)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="386" textLength="48.8" 
clip-path="url(#terminal-2670787131-line-15)">jobs</text><text 
class="terminal-2670787131-r2" x="244" y="386" textLength="280.6" 
clip-path="url(#terminal-2670787131-line-15)">Perform&#160;Jobs&#160;operations</text><text
 class="terminal-2670787131-r2" x="915" y="386" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-15)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="410.4" 
textLength="85.4" 
clip-path="url(#terminal-2670787131-line-16)">plugins</text><text 
class="terminal-2670787131-r2" x="244" y="410.4" textLength="317.2" 
clip-path="url(#terminal-2670787131-line-16)">Perform&#160;Plugins&#160;operations</text><text
 class="terminal-2670787131-r2" x="915" y="410.4" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-16)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="434.8" textLength="61" 
clip-path="url(#terminal-2670787131-line-17)">pools</text><text 
class="terminal-2670787131-r2" x="244" y="434.8" textLength="292.8" 
clip-path="url(#terminal-2670787131-line-17)">Perform&#160;Pools&#160;operations</text><text
 class="terminal-2670787131-r2" x="915" y="434.8" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-17)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="459.2" 
textLength="109.8" 
clip-path="url(#terminal-2670787131-line-18)">providers</text><text 
class="terminal-2670787131-r2" x="244" y="459.2" textLength="341.6" 
clip-path="url(#terminal-2670787131-line-18)">Perform&#160;Providers&#160;operations</text><text
 class="terminal-2670787131-r2" x="915" y="459.2" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-18)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="483.6" 
textLength="109.8" 
clip-path="url(#terminal-2670787131-line-19)">variables</text><text 
class="terminal-2670787131-r2" x="244" y="483.6" textLength="341.6" 
clip-path="url(#terminal-2670787131-line-19)">Perform&#160;Variables&#160;operations</text><text
 class="terminal-2670787131-r2" x="915" y="483.6" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-19)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="508" textLength="48.8" 
clip-path="url(#terminal-2670787131-line-20)">xcom</text><text 
class="terminal-2670787131-r2" x="244" y="508" textLength="280.6" 
clip-path="url(#terminal-2670787131-line-20)">Perform&#160;XCom&#160;operations</text><text
 class="terminal-2670787131-r2" x="915" y="508" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-20)">
-</text><text class="terminal-2670787131-r2" x="915" y="532.4" 
textLength="12.2" clip-path="url(#terminal-2670787131-line-21)">
-</text><text class="terminal-2670787131-r4" x="0" y="556.8" textLength="158.6" 
clip-path="url(#terminal-2670787131-line-22)">&#160;&#160;&#160;&#160;Commands:</text><text
 class="terminal-2670787131-r2" x="915" y="556.8" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-22)">
-</text><text class="terminal-2670787131-r4" x="73.2" y="581.2" 
textLength="85.4" 
clip-path="url(#terminal-2670787131-line-23)">version</text><text 
class="terminal-2670787131-r2" x="244" y="581.2" textLength="292.8" 
clip-path="url(#terminal-2670787131-line-23)">Show&#160;version&#160;information</text><text
 class="terminal-2670787131-r2" x="915" y="581.2" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-23)">
-</text><text class="terminal-2670787131-r2" x="915" y="605.6" 
textLength="12.2" clip-path="url(#terminal-2670787131-line-24)">
-</text><text class="terminal-2670787131-r1" x="0" y="630" textLength="97.6" 
clip-path="url(#terminal-2670787131-line-25)">Options:</text><text 
class="terminal-2670787131-r2" x="915" y="630" textLength="12.2" 
clip-path="url(#terminal-2670787131-line-25)">
-</text><text class="terminal-2670787131-r4" x="24.4" y="654.4" 
textLength="24.4" clip-path="url(#terminal-2670787131-line-26)">-h</text><text 
class="terminal-2670787131-r2" x="48.8" y="654.4" textLength="24.4" 
clip-path="url(#terminal-2670787131-line-26)">,&#160;</text><text 
class="terminal-2670787131-r4" x="73.2" y="654.4" textLength="73.2" 
clip-path="url(#terminal-2670787131-line-26)">--help</text><text 
class="terminal-2670787131-r2" x="244" y="654.4" textLength="378.2" 
clip-path="url( [...]
+    <g class="terminal-151424782-matrix">
+    <text class="terminal-151424782-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-151424782-line-0)">Usage:</text><text 
class="terminal-151424782-r3" x="85.4" y="20" textLength="122" 
clip-path="url(#terminal-151424782-line-0)">airflowctl</text><text 
class="terminal-151424782-r2" x="207.4" y="20" textLength="24.4" 
clip-path="url(#terminal-151424782-line-0)">&#160;[</text><text 
class="terminal-151424782-r4" x="231.8" y="20" textLength="24.4" 
clip-path="url(#terminal-151424782 [...]
+</text><text class="terminal-151424782-r2" x="915" y="44.4" textLength="12.2" 
clip-path="url(#terminal-151424782-line-1)">
+</text><text class="terminal-151424782-r1" x="0" y="68.8" textLength="256.2" 
clip-path="url(#terminal-151424782-line-2)">Positional&#160;Arguments:</text><text
 class="terminal-151424782-r2" x="915" y="68.8" textLength="12.2" 
clip-path="url(#terminal-151424782-line-2)">
+</text><text class="terminal-151424782-r4" x="24.4" y="93.2" 
textLength="195.2" 
clip-path="url(#terminal-151424782-line-3)">GROUP_OR_COMMAND</text><text 
class="terminal-151424782-r2" x="915" y="93.2" textLength="12.2" 
clip-path="url(#terminal-151424782-line-3)">
+</text><text class="terminal-151424782-r2" x="915" y="117.6" textLength="12.2" 
clip-path="url(#terminal-151424782-line-4)">
+</text><text class="terminal-151424782-r4" x="0" y="142" textLength="122" 
clip-path="url(#terminal-151424782-line-5)">&#160;&#160;&#160;&#160;Groups</text><text
 class="terminal-151424782-r2" x="915" y="142" textLength="12.2" 
clip-path="url(#terminal-151424782-line-5)">
+</text><text class="terminal-151424782-r4" x="73.2" y="166.4" 
textLength="73.2" 
clip-path="url(#terminal-151424782-line-6)">assets</text><text 
class="terminal-151424782-r2" x="244" y="166.4" textLength="305" 
clip-path="url(#terminal-151424782-line-6)">Perform&#160;Assets&#160;operations</text><text
 class="terminal-151424782-r2" x="915" y="166.4" textLength="12.2" 
clip-path="url(#terminal-151424782-line-6)">
+</text><text class="terminal-151424782-r4" x="73.2" y="190.8" 
textLength="48.8" clip-path="url(#terminal-151424782-line-7)">auth</text><text 
class="terminal-151424782-r2" x="244" y="190.8" textLength="646.6" 
clip-path="url(#terminal-151424782-line-7)">Manage&#160;authentication&#160;for&#160;CLI.&#160;Either&#160;pass&#160;token&#160;from</text><text
 class="terminal-151424782-r2" x="915" y="190.8" textLength="12.2" 
clip-path="url(#terminal-151424782-line-7)">
+</text><text class="terminal-151424782-r2" x="244" y="215.2" 
textLength="622.2" 
clip-path="url(#terminal-151424782-line-8)">environment&#160;variable/parameter&#160;or&#160;pass&#160;username&#160;and</text><text
 class="terminal-151424782-r2" x="915" y="215.2" textLength="12.2" 
clip-path="url(#terminal-151424782-line-8)">
+</text><text class="terminal-151424782-r2" x="244" y="239.6" 
textLength="109.8" 
clip-path="url(#terminal-151424782-line-9)">password.</text><text 
class="terminal-151424782-r2" x="915" y="239.6" textLength="12.2" 
clip-path="url(#terminal-151424782-line-9)">
+</text><text class="terminal-151424782-r4" x="73.2" y="264" textLength="97.6" 
clip-path="url(#terminal-151424782-line-10)">backfill</text><text 
class="terminal-151424782-r2" x="244" y="264" textLength="329.4" 
clip-path="url(#terminal-151424782-line-10)">Perform&#160;Backfill&#160;operations</text><text
 class="terminal-151424782-r2" x="915" y="264" textLength="12.2" 
clip-path="url(#terminal-151424782-line-10)">
+</text><text class="terminal-151424782-r4" x="73.2" y="288.4" 
textLength="73.2" 
clip-path="url(#terminal-151424782-line-11)">config</text><text 
class="terminal-151424782-r2" x="244" y="288.4" textLength="305" 
clip-path="url(#terminal-151424782-line-11)">Perform&#160;Config&#160;operations</text><text
 class="terminal-151424782-r2" x="915" y="288.4" textLength="12.2" 
clip-path="url(#terminal-151424782-line-11)">
+</text><text class="terminal-151424782-r4" x="73.2" y="312.8" 
textLength="134.2" 
clip-path="url(#terminal-151424782-line-12)">connections</text><text 
class="terminal-151424782-r2" x="244" y="312.8" textLength="366" 
clip-path="url(#terminal-151424782-line-12)">Perform&#160;Connections&#160;operations</text><text
 class="terminal-151424782-r2" x="915" y="312.8" textLength="12.2" 
clip-path="url(#terminal-151424782-line-12)">
+</text><text class="terminal-151424782-r4" x="73.2" y="337.2" 
textLength="73.2" 
clip-path="url(#terminal-151424782-line-13)">dagrun</text><text 
class="terminal-151424782-r2" x="244" y="337.2" textLength="305" 
clip-path="url(#terminal-151424782-line-13)">Perform&#160;DagRun&#160;operations</text><text
 class="terminal-151424782-r2" x="915" y="337.2" textLength="12.2" 
clip-path="url(#terminal-151424782-line-13)">
+</text><text class="terminal-151424782-r4" x="73.2" y="361.6" 
textLength="48.8" clip-path="url(#terminal-151424782-line-14)">dags</text><text 
class="terminal-151424782-r2" x="244" y="361.6" textLength="280.6" 
clip-path="url(#terminal-151424782-line-14)">Perform&#160;Dags&#160;operations</text><text
 class="terminal-151424782-r2" x="915" y="361.6" textLength="12.2" 
clip-path="url(#terminal-151424782-line-14)">
+</text><text class="terminal-151424782-r4" x="73.2" y="386" textLength="48.8" 
clip-path="url(#terminal-151424782-line-15)">jobs</text><text 
class="terminal-151424782-r2" x="244" y="386" textLength="280.6" 
clip-path="url(#terminal-151424782-line-15)">Perform&#160;Jobs&#160;operations</text><text
 class="terminal-151424782-r2" x="915" y="386" textLength="12.2" 
clip-path="url(#terminal-151424782-line-15)">
+</text><text class="terminal-151424782-r4" x="73.2" y="410.4" 
textLength="85.4" 
clip-path="url(#terminal-151424782-line-16)">plugins</text><text 
class="terminal-151424782-r2" x="244" y="410.4" textLength="317.2" 
clip-path="url(#terminal-151424782-line-16)">Perform&#160;Plugins&#160;operations</text><text
 class="terminal-151424782-r2" x="915" y="410.4" textLength="12.2" 
clip-path="url(#terminal-151424782-line-16)">
+</text><text class="terminal-151424782-r4" x="73.2" y="434.8" textLength="61" 
clip-path="url(#terminal-151424782-line-17)">pools</text><text 
class="terminal-151424782-r2" x="244" y="434.8" textLength="292.8" 
clip-path="url(#terminal-151424782-line-17)">Perform&#160;Pools&#160;operations</text><text
 class="terminal-151424782-r2" x="915" y="434.8" textLength="12.2" 
clip-path="url(#terminal-151424782-line-17)">
+</text><text class="terminal-151424782-r4" x="73.2" y="459.2" 
textLength="109.8" 
clip-path="url(#terminal-151424782-line-18)">providers</text><text 
class="terminal-151424782-r2" x="244" y="459.2" textLength="341.6" 
clip-path="url(#terminal-151424782-line-18)">Perform&#160;Providers&#160;operations</text><text
 class="terminal-151424782-r2" x="915" y="459.2" textLength="12.2" 
clip-path="url(#terminal-151424782-line-18)">
+</text><text class="terminal-151424782-r4" x="73.2" y="483.6" 
textLength="158.6" 
clip-path="url(#terminal-151424782-line-19)">taskinstances</text><text 
class="terminal-151424782-r2" x="915" y="483.6" textLength="12.2" 
clip-path="url(#terminal-151424782-line-19)">
+</text><text class="terminal-151424782-r2" x="244" y="508" textLength="390.4" 
clip-path="url(#terminal-151424782-line-20)">Perform&#160;TaskInstances&#160;operations</text><text
 class="terminal-151424782-r2" x="915" y="508" textLength="12.2" 
clip-path="url(#terminal-151424782-line-20)">
+</text><text class="terminal-151424782-r4" x="73.2" y="532.4" textLength="61" 
clip-path="url(#terminal-151424782-line-21)">tasks</text><text 
class="terminal-151424782-r2" x="244" y="532.4" textLength="292.8" 
clip-path="url(#terminal-151424782-line-21)">Perform&#160;Tasks&#160;operations</text><text
 class="terminal-151424782-r2" x="915" y="532.4" textLength="12.2" 
clip-path="url(#terminal-151424782-line-21)">
+</text><text class="terminal-151424782-r4" x="73.2" y="556.8" 
textLength="109.8" 
clip-path="url(#terminal-151424782-line-22)">variables</text><text 
class="terminal-151424782-r2" x="244" y="556.8" textLength="341.6" 
clip-path="url(#terminal-151424782-line-22)">Perform&#160;Variables&#160;operations</text><text
 class="terminal-151424782-r2" x="915" y="556.8" textLength="12.2" 
clip-path="url(#terminal-151424782-line-22)">
+</text><text class="terminal-151424782-r4" x="73.2" y="581.2" 
textLength="48.8" clip-path="url(#terminal-151424782-line-23)">xcom</text><text 
class="terminal-151424782-r2" x="244" y="581.2" textLength="280.6" 
clip-path="url(#terminal-151424782-line-23)">Perform&#160;XCom&#160;operations</text><text
 class="terminal-151424782-r2" x="915" y="581.2" textLength="12.2" 
clip-path="url(#terminal-151424782-line-23)">
+</text><text class="terminal-151424782-r2" x="915" y="605.6" textLength="12.2" 
clip-path="url(#terminal-151424782-line-24)">
+</text><text class="terminal-151424782-r4" x="0" y="630" textLength="158.6" 
clip-path="url(#terminal-151424782-line-25)">&#160;&#160;&#160;&#160;Commands:</text><text
 class="terminal-151424782-r2" x="915" y="630" textLength="12.2" 
clip-path="url(#terminal-151424782-line-25)">
+</text><text class="terminal-151424782-r4" x="73.2" y="654.4" 
textLength="85.4" 
clip-path="url(#terminal-151424782-line-26)">version</text><text 
class="terminal-151424782-r2" x="244" y="654.4" textLength="292.8" 
clip-path="url(#terminal-151424782-line-26)">Show&#160;version&#160;information</text><text
 class="terminal-151424782-r2" x="915" y="654.4" textLength="12.2" 
clip-path="url(#terminal-151424782-line-26)">
+</text><text class="terminal-151424782-r2" x="915" y="678.8" textLength="12.2" 
clip-path="url(#terminal-151424782-line-27)">
+</text><text class="terminal-151424782-r1" x="0" y="703.2" textLength="97.6" 
clip-path="url(#terminal-151424782-line-28)">Options:</text><text 
class="terminal-151424782-r2" x="915" y="703.2" textLength="12.2" 
clip-path="url(#terminal-151424782-line-28)">
+</text><text class="terminal-151424782-r4" x="24.4" y="727.6" 
textLength="24.4" clip-path="url(#terminal-151424782-line-29)">-h</text><text 
class="terminal-151424782-r2" x="48.8" y="727.6" textLength="24.4" 
clip-path="url(#terminal-151424782-line-29)">,&#160;</text><text 
class="terminal-151424782-r4" x="73.2" y="727.6" textLength="73.2" 
clip-path="url(#terminal-151424782-line-29)">--help</text><text 
class="terminal-151424782-r2" x="244" y="727.6" textLength="378.2" 
clip-path="url(#termin [...]
 </text>
     </g>
     </g>
diff --git a/airflow-ctl/docs/images/output_taskinstances.svg 
b/airflow-ctl/docs/images/output_taskinstances.svg
new file mode 100644
index 00000000000..9d9e86568e1
--- /dev/null
+++ b/airflow-ctl/docs/images/output_taskinstances.svg
@@ -0,0 +1,97 @@
+<svg class="rich-terminal" viewBox="0 0 933 294.0" 
xmlns="http://www.w3.org/2000/svg";>
+    <!-- Generated with Rich https://www.textualize.io -->
+    <style>
+
+    @font-face {
+        font-family: "Fira Code";
+        src: local("FiraCode-Regular"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2";)
 format("woff2"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff";)
 format("woff");
+        font-style: normal;
+        font-weight: 400;
+    }
+    @font-face {
+        font-family: "Fira Code";
+        src: local("FiraCode-Bold"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2";)
 format("woff2"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff";)
 format("woff");
+        font-style: bold;
+        font-weight: 700;
+    }
+
+    .terminal-1659955685-matrix {
+        font-family: Fira Code, monospace;
+        font-size: 20px;
+        line-height: 24.4px;
+        font-variant-east-asian: full-width;
+    }
+
+    .terminal-1659955685-title {
+        font-size: 18px;
+        font-weight: bold;
+        font-family: arial;
+    }
+
+    .terminal-1659955685-r1 { fill: #ff8700 }
+.terminal-1659955685-r2 { fill: #c5c8c6 }
+.terminal-1659955685-r3 { fill: #808080 }
+.terminal-1659955685-r4 { fill: #68a0b3 }
+    </style>
+
+    <defs>
+    <clipPath id="terminal-1659955685-clip-terminal">
+      <rect x="0" y="0" width="914.0" height="243.0" />
+    </clipPath>
+    <clipPath id="terminal-1659955685-line-0">
+    <rect x="0" y="1.5" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-1659955685-line-1">
+    <rect x="0" y="25.9" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-1659955685-line-2">
+    <rect x="0" y="50.3" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-1659955685-line-3">
+    <rect x="0" y="74.7" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-1659955685-line-4">
+    <rect x="0" y="99.1" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-1659955685-line-5">
+    <rect x="0" y="123.5" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-1659955685-line-6">
+    <rect x="0" y="147.9" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-1659955685-line-7">
+    <rect x="0" y="172.3" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-1659955685-line-8">
+    <rect x="0" y="196.7" 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="292" 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-1659955685-clip-terminal)">
+    
+    <g class="terminal-1659955685-matrix">
+    <text class="terminal-1659955685-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-1659955685-line-0)">Usage:</text><text 
class="terminal-1659955685-r3" x="85.4" y="20" textLength="292.8" 
clip-path="url(#terminal-1659955685-line-0)">airflowctl&#160;taskinstances</text><text
 class="terminal-1659955685-r2" x="378.2" y="20" textLength="24.4" 
clip-path="url(#terminal-1659955685-line-0)">&#160;[</text><text 
class="terminal-1659955685-r4" x="402.6" y="20" textLength="24.4" clip-p [...]
+</text><text class="terminal-1659955685-r2" x="915" y="44.4" textLength="12.2" 
clip-path="url(#terminal-1659955685-line-1)">
+</text><text class="terminal-1659955685-r2" x="0" y="68.8" textLength="390.4" 
clip-path="url(#terminal-1659955685-line-2)">Perform&#160;TaskInstances&#160;operations</text><text
 class="terminal-1659955685-r2" x="915" y="68.8" textLength="12.2" 
clip-path="url(#terminal-1659955685-line-2)">
+</text><text class="terminal-1659955685-r2" x="915" y="93.2" textLength="12.2" 
clip-path="url(#terminal-1659955685-line-3)">
+</text><text class="terminal-1659955685-r1" x="0" y="117.6" textLength="256.2" 
clip-path="url(#terminal-1659955685-line-4)">Positional&#160;Arguments:</text><text
 class="terminal-1659955685-r2" x="915" y="117.6" textLength="12.2" 
clip-path="url(#terminal-1659955685-line-4)">
+</text><text class="terminal-1659955685-r4" x="24.4" y="142" textLength="85.4" 
clip-path="url(#terminal-1659955685-line-5)">COMMAND</text><text 
class="terminal-1659955685-r2" x="915" y="142" textLength="12.2" 
clip-path="url(#terminal-1659955685-line-5)">
+</text><text class="terminal-1659955685-r4" x="48.8" y="166.4" 
textLength="48.8" clip-path="url(#terminal-1659955685-line-6)">list</text><text 
class="terminal-1659955685-r2" x="170.8" y="166.4" textLength="524.6" 
clip-path="url(#terminal-1659955685-line-6)">List&#160;all&#160;task&#160;instances&#160;for&#160;a&#160;given&#160;Dag&#160;run</text><text
 class="terminal-1659955685-r2" x="915" y="166.4" textLength="12.2" 
clip-path="url(#terminal-1659955685-line-6)">
+</text><text class="terminal-1659955685-r2" x="915" y="190.8" 
textLength="12.2" clip-path="url(#terminal-1659955685-line-7)">
+</text><text class="terminal-1659955685-r1" x="0" y="215.2" textLength="97.6" 
clip-path="url(#terminal-1659955685-line-8)">Options:</text><text 
class="terminal-1659955685-r2" x="915" y="215.2" textLength="12.2" 
clip-path="url(#terminal-1659955685-line-8)">
+</text><text class="terminal-1659955685-r4" x="24.4" y="239.6" 
textLength="24.4" clip-path="url(#terminal-1659955685-line-9)">-h</text><text 
class="terminal-1659955685-r2" x="48.8" y="239.6" textLength="24.4" 
clip-path="url(#terminal-1659955685-line-9)">,&#160;</text><text 
class="terminal-1659955685-r4" x="73.2" y="239.6" textLength="73.2" 
clip-path="url(#terminal-1659955685-line-9)">--help</text><text 
class="terminal-1659955685-r2" x="170.8" y="239.6" textLength="378.2" 
clip-path="url(# [...]
+</text>
+    </g>
+    </g>
+</svg>
diff --git a/airflow-ctl/docs/images/output_tasks.svg 
b/airflow-ctl/docs/images/output_tasks.svg
new file mode 100644
index 00000000000..f3fa3c4d945
--- /dev/null
+++ b/airflow-ctl/docs/images/output_tasks.svg
@@ -0,0 +1,105 @@
+<svg class="rich-terminal" viewBox="0 0 933 342.79999999999995" 
xmlns="http://www.w3.org/2000/svg";>
+    <!-- Generated with Rich https://www.textualize.io -->
+    <style>
+
+    @font-face {
+        font-family: "Fira Code";
+        src: local("FiraCode-Regular"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2";)
 format("woff2"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff";)
 format("woff");
+        font-style: normal;
+        font-weight: 400;
+    }
+    @font-face {
+        font-family: "Fira Code";
+        src: local("FiraCode-Bold"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2";)
 format("woff2"),
+                
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff";)
 format("woff");
+        font-style: bold;
+        font-weight: 700;
+    }
+
+    .terminal-177420400-matrix {
+        font-family: Fira Code, monospace;
+        font-size: 20px;
+        line-height: 24.4px;
+        font-variant-east-asian: full-width;
+    }
+
+    .terminal-177420400-title {
+        font-size: 18px;
+        font-weight: bold;
+        font-family: arial;
+    }
+
+    .terminal-177420400-r1 { fill: #ff8700 }
+.terminal-177420400-r2 { fill: #c5c8c6 }
+.terminal-177420400-r3 { fill: #808080 }
+.terminal-177420400-r4 { fill: #68a0b3 }
+    </style>
+
+    <defs>
+    <clipPath id="terminal-177420400-clip-terminal">
+      <rect x="0" y="0" width="914.0" height="291.79999999999995" />
+    </clipPath>
+    <clipPath id="terminal-177420400-line-0">
+    <rect x="0" y="1.5" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-177420400-line-1">
+    <rect x="0" y="25.9" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-177420400-line-2">
+    <rect x="0" y="50.3" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-177420400-line-3">
+    <rect x="0" y="74.7" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-177420400-line-4">
+    <rect x="0" y="99.1" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-177420400-line-5">
+    <rect x="0" y="123.5" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-177420400-line-6">
+    <rect x="0" y="147.9" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-177420400-line-7">
+    <rect x="0" y="172.3" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-177420400-line-8">
+    <rect x="0" y="196.7" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-177420400-line-9">
+    <rect x="0" y="221.1" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-177420400-line-10">
+    <rect x="0" y="245.5" 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="340.8" 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-177420400-clip-terminal)">
+    
+    <g class="terminal-177420400-matrix">
+    <text class="terminal-177420400-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-177420400-line-0)">Usage:</text><text 
class="terminal-177420400-r3" x="85.4" y="20" textLength="195.2" 
clip-path="url(#terminal-177420400-line-0)">airflowctl&#160;tasks</text><text 
class="terminal-177420400-r2" x="280.6" y="20" textLength="24.4" 
clip-path="url(#terminal-177420400-line-0)">&#160;[</text><text 
class="terminal-177420400-r4" x="305" y="20" textLength="24.4" 
clip-path="url(#termina [...]
+</text><text class="terminal-177420400-r2" x="915" y="44.4" textLength="12.2" 
clip-path="url(#terminal-177420400-line-1)">
+</text><text class="terminal-177420400-r2" x="0" y="68.8" textLength="292.8" 
clip-path="url(#terminal-177420400-line-2)">Perform&#160;Tasks&#160;operations</text><text
 class="terminal-177420400-r2" x="915" y="68.8" textLength="12.2" 
clip-path="url(#terminal-177420400-line-2)">
+</text><text class="terminal-177420400-r2" x="915" y="93.2" textLength="12.2" 
clip-path="url(#terminal-177420400-line-3)">
+</text><text class="terminal-177420400-r1" x="0" y="117.6" textLength="256.2" 
clip-path="url(#terminal-177420400-line-4)">Positional&#160;Arguments:</text><text
 class="terminal-177420400-r2" x="915" y="117.6" textLength="12.2" 
clip-path="url(#terminal-177420400-line-4)">
+</text><text class="terminal-177420400-r4" x="24.4" y="142" textLength="85.4" 
clip-path="url(#terminal-177420400-line-5)">COMMAND</text><text 
class="terminal-177420400-r2" x="915" y="142" textLength="12.2" 
clip-path="url(#terminal-177420400-line-5)">
+</text><text class="terminal-177420400-r4" x="48.8" y="166.4" textLength="61" 
clip-path="url(#terminal-177420400-line-6)">clear</text><text 
class="terminal-177420400-r2" x="268.4" y="166.4" textLength="475.8" 
clip-path="url(#terminal-177420400-line-6)">Clear&#160;task&#160;instances&#160;of&#160;a&#160;Dag&#160;by&#160;its&#160;ID</text><text
 class="terminal-177420400-r2" x="915" y="166.4" textLength="12.2" 
clip-path="url(#terminal-177420400-line-6)">
+</text><text class="terminal-177420400-r4" x="48.8" y="190.8" 
textLength="219.6" 
clip-path="url(#terminal-177420400-line-7)">states-for-dag-run</text><text 
class="terminal-177420400-r2" x="915" y="190.8" textLength="12.2" 
clip-path="url(#terminal-177420400-line-7)">
+</text><text class="terminal-177420400-r2" x="268.4" y="215.2" 
textLength="597.8" 
clip-path="url(#terminal-177420400-line-8)">Get&#160;the&#160;status&#160;of&#160;all&#160;task&#160;instances&#160;in&#160;a&#160;Dag&#160;run</text><text
 class="terminal-177420400-r2" x="915" y="215.2" textLength="12.2" 
clip-path="url(#terminal-177420400-line-8)">
+</text><text class="terminal-177420400-r2" x="915" y="239.6" textLength="12.2" 
clip-path="url(#terminal-177420400-line-9)">
+</text><text class="terminal-177420400-r1" x="0" y="264" textLength="97.6" 
clip-path="url(#terminal-177420400-line-10)">Options:</text><text 
class="terminal-177420400-r2" x="915" y="264" textLength="12.2" 
clip-path="url(#terminal-177420400-line-10)">
+</text><text class="terminal-177420400-r4" x="24.4" y="288.4" 
textLength="24.4" clip-path="url(#terminal-177420400-line-11)">-h</text><text 
class="terminal-177420400-r2" x="48.8" y="288.4" textLength="24.4" 
clip-path="url(#terminal-177420400-line-11)">,&#160;</text><text 
class="terminal-177420400-r4" x="73.2" y="288.4" textLength="73.2" 
clip-path="url(#terminal-177420400-line-11)">--help</text><text 
class="terminal-177420400-r2" x="268.4" y="288.4" textLength="378.2" 
clip-path="url(#term [...]
+</text>
+    </g>
+    </g>
+</svg>
diff --git a/airflow-ctl/docs/spelling_wordlist.txt 
b/airflow-ctl/docs/spelling_wordlist.txt
new file mode 100644
index 00000000000..04c8494c855
--- /dev/null
+++ b/airflow-ctl/docs/spelling_wordlist.txt
@@ -0,0 +1 @@
+taskinstances
diff --git a/airflow-ctl/src/airflowctl/api/client.py 
b/airflow-ctl/src/airflowctl/api/client.py
index 208e1800f79..dfd961aee0f 100644
--- a/airflow-ctl/src/airflowctl/api/client.py
+++ b/airflow-ctl/src/airflowctl/api/client.py
@@ -57,6 +57,8 @@ from airflowctl.api.operations import (
     PoolsOperations,
     ProvidersOperations,
     ServerResponseError,
+    TaskInstancesOperations,
+    TasksOperations,
     VariablesOperations,
     VersionOperations,
     XComOperations,
@@ -474,6 +476,18 @@ class Client(httpx.Client):
         """Operations related to providers."""
         return ProvidersOperations(self)
 
+    @lru_cache()  # type: ignore[prop-decorator]
+    @property
+    def task_instances(self):
+        """Operations related to task instances."""
+        return TaskInstancesOperations(self)
+
+    @lru_cache()  # type: ignore[prop-decorator]
+    @property
+    def tasks(self):
+        """Operations related to tasks."""
+        return TasksOperations(self)
+
     @lru_cache()  # type: ignore[prop-decorator]
     @property
     def variables(self):
diff --git a/airflow-ctl/src/airflowctl/api/operations.py 
b/airflow-ctl/src/airflowctl/api/operations.py
index 266ad7783b3..bd7d3ec58ff 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -44,6 +44,7 @@ from airflowctl.api.datamodels.generated import (
     BulkBodyPoolBody,
     BulkBodyVariableBody,
     BulkResponse,
+    ClearTaskInstancesBody,
     Config,
     ConnectionBody,
     ConnectionCollectionResponse,
@@ -73,6 +74,7 @@ from airflowctl.api.datamodels.generated import (
     ProviderCollectionResponse,
     QueuedEventCollectionResponse,
     QueuedEventResponse,
+    TaskInstanceCollectionResponse,
     TriggerDAGRunPostBody,
     VariableBody,
     VariableCollectionResponse,
@@ -807,6 +809,34 @@ class ProvidersOperations(BaseOperations):
         return super().execute_list(path="providers", 
data_model=ProviderCollectionResponse)
 
 
+class TaskInstancesOperations(BaseOperations):
+    """Task instance operations."""
+
+    def list(self, dag_id: str, dag_run_id: str) -> 
TaskInstanceCollectionResponse | ServerResponseError:
+        """List task instances for a Dag run."""
+        return super().execute_list(
+            path=f"dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances",
+            data_model=TaskInstanceCollectionResponse,
+        )
+
+
+class TasksOperations(BaseOperations):
+    """Tasks operations."""
+
+    def clear(
+        self, dag_id: str, clear_task_instances: ClearTaskInstancesBody
+    ) -> TaskInstanceCollectionResponse | ServerResponseError:
+        """Clear task instances of a Dag; with dry_run (the default) only 
previews the affected task instances."""
+        try:
+            self.response = self.client.post(
+                f"dags/{dag_id}/clearTaskInstances",
+                json=clear_task_instances.model_dump(mode="json", 
exclude_none=True),
+            )
+            return 
TaskInstanceCollectionResponse.model_validate_json(self.response.content)
+        except ServerResponseError as e:
+            raise e
+
+
 class VariablesOperations(BaseOperations):
     """Variable operations."""
 
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py 
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index 11d6e7a15f5..e42f4ebd02c 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -306,7 +306,7 @@ ARG_AUTH_PASSWORD = Arg(
 ARG_DAG_ID = Arg(
     flags=("dag_id",),
     type=str,
-    help="The Dag ID of the Dag to pause or unpause",
+    help="The Dag ID",
 )
 ARG_RUN_ID = Arg(
     flags=("run_id",),
@@ -320,6 +320,19 @@ ARG_LOGICAL_DATE = Arg(
     help="The logical date of the Dag run with a timezone offset (pass this or 
run_id, not both)",
 )
 
+# Task Commands Args
+ARG_RUN_ID = Arg(
+    flags=("run_id",),
+    type=str,
+    nargs="?",
+    help="The run ID of the Dag run (pass this or --logical-date, not both)",
+)
+ARG_LOGICAL_DATE = Arg(
+    flags=("--logical-date",),
+    type=str,
+    help="The logical date of the Dag run with a timezone offset (pass this or 
run_id, not both)",
+)
+
 ARG_ACTION_ON_EXISTING_KEY = Arg(
     flags=("-a", "--action-on-existing-key"),
     type=str,
@@ -457,6 +470,10 @@ class CommandFactory:
             "set",
             "clear",
         ]
+        # Datamodels whose generated bool flags follow the datamodel field 
defaults instead of
+        # defaulting to False, so the CLI keeps the API semantics (e.g. a bare 
``tasks clear``
+        # must keep ``dry_run=True`` and preview instead of clearing).
+        self.field_bool_default_datamodels = ["ClearTaskInstancesBody"]
         self.exclude_operation_names = ["LoginOperations", 
"VersionOperations", "BaseOperations"]
         self.exclude_method_names = [
             "error",
@@ -624,6 +641,14 @@ class CommandFactory:
             help=arg_help,
         )
 
+    def _get_bool_arg_default(self, parameter_type: str, field_default: Any) 
-> bool | None:
+        """Get default for a generated bool flag: the datamodel field default 
for datamodels in ``field_bool_default_datamodels``, otherwise False."""
+        if parameter_type in self.field_bool_default_datamodels and (
+            field_default is None or isinstance(field_default, bool)
+        ):
+            return field_default
+        return False
+
     def _create_arg_for_non_primitive_type(
         self,
         parameter_type: str,
@@ -639,30 +664,24 @@ class CommandFactory:
                 continue
             self.datamodels_extended_map[parameter_type].append(field)
             if type(field_type.annotation) is type:
-                commands.append(
-                    self._create_arg(
-                        arg_flags=("--" + 
self._sanitize_arg_parameter_key(field),),
-                        
arg_type=self._python_type_from_string(field_type.annotation),
-                        arg_action=argparse.BooleanOptionalAction if 
field_type.annotation is bool else None,  # type: ignore
-                        arg_help=f"{field} for {parameter_key} operation",
-                        arg_default=False if field_type.annotation is bool 
else None,
-                    )
-                )
+                annotation = field_type.annotation
             else:
                 try:
                     annotation = field_type.annotation.__args__[0]
                 except AttributeError:
                     annotation = field_type.annotation
 
-                commands.append(
-                    self._create_arg(
-                        arg_flags=("--" + 
self._sanitize_arg_parameter_key(field),),
-                        arg_type=self._python_type_from_string(annotation),
-                        arg_action=argparse.BooleanOptionalAction if 
annotation is bool else None,  # type: ignore
-                        arg_help=f"{field} for {parameter_key} operation",
-                        arg_default=False if annotation is bool else None,
-                    )
+            commands.append(
+                self._create_arg(
+                    
arg_flags=(f"--{self._sanitize_arg_parameter_key(field)}",),
+                    arg_type=self._python_type_from_string(annotation),
+                    arg_action=argparse.BooleanOptionalAction if annotation is 
bool else None,  # type: ignore
+                    arg_help=f"{field} for {parameter_key} operation",
+                    arg_default=self._get_bool_arg_default(parameter_type, 
field_type.default)
+                    if annotation is bool
+                    else None,
                 )
+            )
         return commands
 
     def _create_args_map_from_operation(self):
@@ -743,6 +762,20 @@ class CommandFactory:
         ):
             params["logical_date"] = 
datetime.datetime.now(datetime.timezone.utc)
 
+        # Handle ClearTaskInstancesBody: --task-ids arrives as a single string 
but the API expects
+        # a list of task_id or [task_id, map_index]; accept comma-separated 
ids or a JSON list
+        if datamodel.__name__ == "ClearTaskInstancesBody" and 
isinstance(params.get("task_ids"), str):
+            raw_task_ids = params["task_ids"]
+            if raw_task_ids.lstrip().startswith("["):
+                try:
+                    params["task_ids"] = json.loads(raw_task_ids)
+                except json.JSONDecodeError as e:
+                    raise SystemExit(f"Invalid JSON list for --task-ids 
{raw_task_ids!r}: {e}")
+            else:
+                params["task_ids"] = [
+                    task_id.strip() for task_id in raw_task_ids.split(",") if 
task_id.strip()
+                ]
+
         return params
 
     def _create_func_map_from_operation(self):
@@ -1088,6 +1121,24 @@ POOL_COMMANDS = (
     ),
 )
 
+TASK_COMMANDS = (
+    ActionCommand(
+        name="states-for-dag-run",
+        help="Get the status of all task instances in a Dag run",
+        description=(
+            "Get the status of all task instances in a Dag run. "
+            "Select the run with either run_id or --logical-date (pass exactly 
one)."
+        ),
+        
func=lazy_load_command("airflowctl.ctl.commands.task_command.states_for_dag_run"),
+        args=(
+            ARG_DAG_ID,
+            ARG_RUN_ID,
+            ARG_LOGICAL_DATE,
+            ARG_OUTPUT,
+        ),
+    ),
+)
+
 VARIABLE_COMMANDS = (
     ActionCommand(
         name="import",
@@ -1124,6 +1175,11 @@ core_commands: list[CLICommand] = [
         help="Manage Airflow pools",
         subcommands=POOL_COMMANDS,
     ),
+    GroupCommand(
+        name="tasks",
+        help="Manage Airflow tasks",
+        subcommands=TASK_COMMANDS,
+    ),
     ActionCommand(
         name="version",
         help="Show version information",
diff --git a/airflow-ctl/src/airflowctl/ctl/commands/task_command.py 
b/airflow-ctl/src/airflowctl/ctl/commands/task_command.py
new file mode 100644
index 00000000000..5881a1bd556
--- /dev/null
+++ b/airflow-ctl/src/airflowctl/ctl/commands/task_command.py
@@ -0,0 +1,99 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+import datetime
+import sys
+from typing import TYPE_CHECKING
+
+import rich
+
+from airflowctl.api.client import NEW_API_CLIENT, ClientKind, 
ServerResponseError, provide_api_client
+from airflowctl.ctl.console_formatting import AirflowConsole
+
+if TYPE_CHECKING:
+    from airflowctl.api.datamodels.generated import TaskInstanceResponse
+
+
+def _find_run_id_by_logical_date(api_client, dag_id: str, value: str) -> str:
+    """Find the run ID of the Dag run with an exact logical date match."""
+    try:
+        logical_date = datetime.datetime.fromisoformat(value.replace("Z", 
"+00:00"))
+    except ValueError:
+        rich.print(f"[red]Invalid --logical-date: {value!r}[/red]")
+        sys.exit(1)
+    if logical_date.tzinfo is None:
+        rich.print("[red]--logical-date must include a timezone offset[/red]")
+        sys.exit(1)
+
+    dag_runs = []
+    try:
+        dag_runs = api_client.dag_runs.list(
+            dag_id=dag_id,
+            logical_date_gte=logical_date,
+            logical_date_lte=logical_date,
+            order_by="-id",
+            limit=1,
+            suppress_error_log=True,
+        ).dag_runs
+    except ServerResponseError as e:
+        if e.response.status_code != 404:
+            raise
+    if not dag_runs:
+        rich.print(f"[red]Dag run for {dag_id} with logical date {value!r} not 
found[/red]")
+        sys.exit(1)
+    return dag_runs[0].dag_run_id
+
+
+def _format_task_instance(ti: TaskInstanceResponse, has_mapped_instances: 
bool) -> dict[str, str]:
+    data = {
+        "dag_id": ti.dag_id,
+        "logical_date": ti.logical_date.isoformat() if ti.logical_date else "",
+        "task_id": ti.task_id,
+        "state": ti.state.value if ti.state else "",
+        "start_date": ti.start_date.isoformat() if ti.start_date else "",
+        "end_date": ti.end_date.isoformat() if ti.end_date else "",
+    }
+    if has_mapped_instances:
+        data["map_index"] = str(ti.map_index) if ti.map_index >= 0 else ""
+    return data
+
+
+@provide_api_client(kind=ClientKind.CLI)
+def states_for_dag_run(args, api_client=NEW_API_CLIENT) -> None:
+    """Get the status of all task instances in a Dag run."""
+    if (args.run_id is None) == (args.logical_date is None):
+        rich.print("[red]Provide either run_id or --logical-date, but not 
both[/red]")
+        sys.exit(1)
+
+    run_id = args.run_id or _find_run_id_by_logical_date(api_client, 
args.dag_id, args.logical_date)
+
+    try:
+        task_instances = api_client.task_instances.list(dag_id=args.dag_id, 
dag_run_id=run_id).task_instances
+    except ServerResponseError as e:
+        if e.response.status_code == 404:
+            rich.print(f"[red]Dag run {run_id!r} of Dag {args.dag_id!r} not 
found[/red]")
+            sys.exit(1)
+        raise
+
+    has_mapped_instances = any(ti.map_index >= 0 for ti in task_instances)
+
+    AirflowConsole().print_as(
+        data=[_format_task_instance(ti, has_mapped_instances) for ti in 
task_instances],
+        output=args.output,
+    )
diff --git a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml 
b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
index 69cc8a47a00..6aaf6d7d7c6 100644
--- a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
+++ b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
@@ -90,6 +90,12 @@ pools:
 providers:
   list: "List all installed Airflow providers"
 
+taskinstances:
+  list: "List all task instances for a given Dag run"
+
+tasks:
+  clear: "Clear task instances of a Dag by its ID"
+
 variables:
   get: "Retrieve a variable by its key"
   list: "List all variables"
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py 
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index d00e6d911e0..31985a866bc 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -52,6 +52,7 @@ from airflowctl.api.datamodels.generated import (
     BulkCreateActionPoolBody,
     BulkCreateActionVariableBody,
     BulkResponse,
+    ClearTaskInstancesBody,
     Config,
     ConfigOption,
     ConfigSection,
@@ -98,6 +99,9 @@ from airflowctl.api.datamodels.generated import (
     QueuedEventResponse,
     ReprocessBehavior,
     TaskInletAssetReference,
+    TaskInstanceCollectionResponse,
+    TaskInstanceResponse,
+    TaskInstanceState,
     TaskOutletAssetReference,
     TriggerDAGRunPostBody,
     VariableBody,
@@ -1710,6 +1714,83 @@ class TestProvidersOperations:
         assert response == self.provider_collection_response
 
 
+class TestTaskInstancesOperations:
+    task_instance_response = TaskInstanceResponse(
+        id=uuid.UUID("4d828a62-a417-4936-a7a6-2b3fabacecab"),
+        task_id="task_id",
+        dag_id="dag_id",
+        dag_run_id="dag_run_id",
+        map_index=-1,
+        run_after=datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc),
+        state=TaskInstanceState.SUCCESS,
+        try_number=1,
+        max_tries=0,
+        task_display_name="task_id",
+        dag_display_name="dag_id",
+        pool="default_pool",
+        pool_slots=1,
+        executor_config="{}",
+    )
+    task_instance_collection_response = TaskInstanceCollectionResponse(
+        task_instances=[task_instance_response],
+        total_entries=1,
+    )
+
+    def test_list(self):
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == 
"/api/v2/dags/dag_id/dagRuns/dag_run_id/taskInstances"
+            return httpx.Response(
+                200, 
json=json.loads(self.task_instance_collection_response.model_dump_json())
+            )
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.task_instances.list(dag_id="dag_id", 
dag_run_id="dag_run_id")
+        assert response == self.task_instance_collection_response
+
+
+class TestTasksOperations:
+    dag_id = "dag_id"
+    clear_task_instances = ClearTaskInstancesBody(
+        dry_run=True,
+        task_ids=["task_1"],
+        dag_run_id="dag_run_id",
+    )
+    task_instance_response = TaskInstanceResponse(
+        id=uuid.UUID("0195c8b0-8bb5-7fc1-9ac9-04c286c47b7f"),
+        task_id="task_1",
+        dag_id=dag_id,
+        dag_run_id="dag_run_id",
+        map_index=-1,
+        run_after=datetime.datetime(2025, 1, 1, 0, 0, 0),
+        try_number=1,
+        max_tries=2,
+        task_display_name="task_1",
+        dag_display_name="dag_display_name",
+        pool="default_pool",
+        pool_slots=1,
+        executor_config="{}",
+    )
+    task_instance_collection_response = TaskInstanceCollectionResponse(
+        task_instances=[task_instance_response],
+        total_entries=1,
+    )
+
+    def test_clear(self):
+        expected_body = self.clear_task_instances.model_dump(mode="json", 
exclude_none=True)
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == 
f"/api/v2/dags/{self.dag_id}/clearTaskInstances"
+            assert request.headers.get("content-type", 
"").startswith("application/json")
+            assert json.loads(request.content.decode()) == expected_body
+            return httpx.Response(
+                200, 
json=json.loads(self.task_instance_collection_response.model_dump_json())
+            )
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.tasks.clear(self.dag_id, self.clear_task_instances)
+        assert response == self.task_instance_collection_response
+
+
 class TestVariablesOperations:
     key = "key"
     value = "val"
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_task_command.py 
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_task_command.py
new file mode 100644
index 00000000000..cb134726711
--- /dev/null
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_task_command.py
@@ -0,0 +1,284 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import datetime
+import uuid
+from unittest import mock
+
+import httpx
+import pytest
+
+from airflowctl.api.datamodels.generated import (
+    TaskInstanceCollectionResponse,
+    TaskInstanceResponse,
+    TaskInstanceState,
+)
+from airflowctl.api.operations import ServerResponseError
+from airflowctl.ctl import cli_parser
+from airflowctl.ctl.commands import task_command
+
+
+def _make_server_error(status_code: int) -> ServerResponseError:
+    request = httpx.Request("GET", 
"http://testserver/api/v2/dags/test_dag/dagRuns/test_run";)
+    response = httpx.Response(status_code, request=request, json={"detail": 
"boom"})
+    return ServerResponseError(message="boom", request=request, 
response=response)
+
+
+def _normalize_rich_output(text: str) -> str:
+    return " ".join(text.split())
+
+
+class TestStatesForDagRun:
+    parser = cli_parser.get_parser()
+    dag_id = "test_dag"
+    run_id = "test_run"
+    logical_date = datetime.datetime(2025, 1, 1, tzinfo=datetime.timezone.utc)
+
+    def _make_task_instance(
+        self,
+        task_id: str,
+        *,
+        map_index: int = -1,
+        state: TaskInstanceState | None = TaskInstanceState.SUCCESS,
+        start_date: datetime.datetime | None = None,
+        end_date: datetime.datetime | None = None,
+    ) -> TaskInstanceResponse:
+        return TaskInstanceResponse(
+            id=uuid.uuid4(),
+            task_id=task_id,
+            dag_id=self.dag_id,
+            dag_run_id=self.run_id,
+            map_index=map_index,
+            logical_date=self.logical_date,
+            run_after=self.logical_date,
+            state=state,
+            start_date=start_date,
+            end_date=end_date,
+            try_number=1,
+            max_tries=0,
+            task_display_name=task_id,
+            dag_display_name=self.dag_id,
+            pool="default_pool",
+            pool_slots=1,
+            executor_config="{}",
+        )
+
+    def _make_api_client(self, task_instances: list[TaskInstanceResponse]) -> 
mock.MagicMock:
+        api_client = mock.MagicMock()
+        api_client.dag_runs.list.return_value.dag_runs = 
[mock.MagicMock(dag_run_id=self.run_id)]
+        api_client.task_instances.list.return_value = 
TaskInstanceCollectionResponse(
+            task_instances=task_instances,
+            total_entries=len(task_instances),
+        )
+        return api_client
+
+    @mock.patch("airflowctl.ctl.commands.task_command.AirflowConsole")
+    def test_states_for_dag_run_by_run_id(self, mock_console_cls):
+        start_date = datetime.datetime(2025, 1, 1, 1, 
tzinfo=datetime.timezone.utc)
+        end_date = datetime.datetime(2025, 1, 1, 2, 
tzinfo=datetime.timezone.utc)
+        api_client = self._make_api_client(
+            [
+                self._make_task_instance("task_a", start_date=start_date, 
end_date=end_date),
+                self._make_task_instance("task_b", state=None),
+            ]
+        )
+
+        task_command.states_for_dag_run(
+            self.parser.parse_args(["tasks", "states-for-dag-run", 
self.dag_id, self.run_id]),
+            api_client=api_client,
+        )
+
+        api_client.dag_runs.list.assert_not_called()
+        
api_client.task_instances.list.assert_called_once_with(dag_id=self.dag_id, 
dag_run_id=self.run_id)
+        mock_console_cls.return_value.print_as.assert_called_once_with(
+            data=[
+                {
+                    "dag_id": self.dag_id,
+                    "logical_date": "2025-01-01T00:00:00+00:00",
+                    "task_id": "task_a",
+                    "state": "success",
+                    "start_date": "2025-01-01T01:00:00+00:00",
+                    "end_date": "2025-01-01T02:00:00+00:00",
+                },
+                {
+                    "dag_id": self.dag_id,
+                    "logical_date": "2025-01-01T00:00:00+00:00",
+                    "task_id": "task_b",
+                    "state": "",
+                    "start_date": "",
+                    "end_date": "",
+                },
+            ],
+            output="json",
+        )
+
+    @mock.patch("airflowctl.ctl.commands.task_command.AirflowConsole")
+    def test_states_for_dag_run_by_logical_date(self, mock_console_cls):
+        api_client = 
self._make_api_client([self._make_task_instance("task_a")])
+
+        task_command.states_for_dag_run(
+            self.parser.parse_args(
+                ["tasks", "states-for-dag-run", self.dag_id, "--logical-date", 
self.logical_date.isoformat()]
+            ),
+            api_client=api_client,
+        )
+
+        api_client.dag_runs.list.assert_called_once_with(
+            dag_id=self.dag_id,
+            logical_date_gte=self.logical_date,
+            logical_date_lte=self.logical_date,
+            order_by="-id",
+            limit=1,
+            suppress_error_log=True,
+        )
+        
api_client.task_instances.list.assert_called_once_with(dag_id=self.dag_id, 
dag_run_id=self.run_id)
+        mock_console_cls.return_value.print_as.assert_called_once_with(
+            data=[
+                {
+                    "dag_id": self.dag_id,
+                    "logical_date": "2025-01-01T00:00:00+00:00",
+                    "task_id": "task_a",
+                    "state": "success",
+                    "start_date": "",
+                    "end_date": "",
+                },
+            ],
+            output="json",
+        )
+
+    @mock.patch("airflowctl.ctl.commands.task_command.AirflowConsole")
+    def test_states_for_dag_run_includes_map_index_for_mapped_instances(self, 
mock_console_cls):
+        api_client = self._make_api_client(
+            [
+                self._make_task_instance("plain_task"),
+                self._make_task_instance("mapped_task", map_index=0),
+                self._make_task_instance("mapped_task", map_index=1),
+            ]
+        )
+
+        task_command.states_for_dag_run(
+            self.parser.parse_args(["tasks", "states-for-dag-run", 
self.dag_id, self.run_id]),
+            api_client=api_client,
+        )
+
+        rows = mock_console_cls.return_value.print_as.call_args.kwargs["data"]
+        assert [row["map_index"] for row in rows] == ["", "0", "1"]
+
+    @pytest.mark.parametrize(
+        "extra_args",
+        [
+            [],
+            ["test_run", "--logical-date", "2025-01-01T00:00:00+00:00"],
+        ],
+        ids=["neither", "both"],
+    )
+    def 
test_states_for_dag_run_requires_exactly_one_of_run_id_and_logical_date(self, 
extra_args, capsys):
+        api_client = mock.MagicMock()
+
+        with pytest.raises(SystemExit, match="1"):
+            task_command.states_for_dag_run(
+                self.parser.parse_args(["tasks", "states-for-dag-run", 
self.dag_id, *extra_args]),
+                api_client=api_client,
+            )
+
+        api_client.task_instances.list.assert_not_called()
+        assert _normalize_rich_output(capsys.readouterr().out) == (
+            "Provide either run_id or --logical-date, but not both"
+        )
+
+    @pytest.mark.parametrize(
+        ("logical_date", "expected_message"),
+        [
+            ("not-a-date", "Invalid --logical-date: 'not-a-date'"),
+            ("2025-01-01T00:00:00", "--logical-date must include a timezone 
offset"),
+        ],
+        ids=["unparsable", "naive"],
+    )
+    def test_states_for_dag_run_rejects_bad_logical_date(self, logical_date, 
expected_message, capsys):
+        api_client = mock.MagicMock()
+
+        with pytest.raises(SystemExit, match="1"):
+            task_command.states_for_dag_run(
+                self.parser.parse_args(
+                    ["tasks", "states-for-dag-run", self.dag_id, 
"--logical-date", logical_date]
+                ),
+                api_client=api_client,
+            )
+
+        api_client.dag_runs.list.assert_not_called()
+        assert _normalize_rich_output(capsys.readouterr().out) == 
expected_message
+
+    @pytest.mark.parametrize("list_failure", ["no_matching_run", 
"dag_not_found_404"])
+    def test_states_for_dag_run_dag_run_not_found_by_logical_date(self, 
list_failure, capsys):
+        api_client = mock.MagicMock()
+        if list_failure == "no_matching_run":
+            api_client.dag_runs.list.return_value.dag_runs = []
+        else:
+            api_client.dag_runs.list.side_effect = _make_server_error(404)
+
+        with pytest.raises(SystemExit, match="1"):
+            task_command.states_for_dag_run(
+                self.parser.parse_args(
+                    [
+                        "tasks",
+                        "states-for-dag-run",
+                        self.dag_id,
+                        "--logical-date",
+                        self.logical_date.isoformat(),
+                    ]
+                ),
+                api_client=api_client,
+            )
+
+        api_client.task_instances.list.assert_not_called()
+        assert _normalize_rich_output(capsys.readouterr().out) == (
+            "Dag run for test_dag with logical date 
'2025-01-01T00:00:00+00:00' not found"
+        )
+
+    def test_states_for_dag_run_dag_run_not_found(self, capsys):
+        api_client = mock.MagicMock()
+        api_client.task_instances.list.side_effect = _make_server_error(404)
+
+        with pytest.raises(SystemExit, match="1"):
+            task_command.states_for_dag_run(
+                self.parser.parse_args(["tasks", "states-for-dag-run", 
self.dag_id, self.run_id]),
+                api_client=api_client,
+            )
+
+        assert _normalize_rich_output(capsys.readouterr().out) == (
+            "Dag run 'test_run' of Dag 'test_dag' not found"
+        )
+
+    @pytest.mark.parametrize(
+        "failing_call",
+        ["dag_runs_list", "task_instances_list"],
+    )
+    def test_states_for_dag_run_propagates_non_404_api_error(self, 
failing_call):
+        api_client = mock.MagicMock()
+        error = _make_server_error(500)
+        if failing_call == "dag_runs_list":
+            api_client.dag_runs.list.side_effect = error
+            argv = ["tasks", "states-for-dag-run", self.dag_id, 
"--logical-date", "2025-01-01T00:00:00+00:00"]
+        else:
+            api_client.task_instances.list.side_effect = error
+            argv = ["tasks", "states-for-dag-run", self.dag_id, self.run_id]
+
+        with pytest.raises(ServerResponseError) as ctx:
+            task_command.states_for_dag_run(self.parser.parse_args(argv), 
api_client=api_client)
+
+        assert ctx.value is error
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py 
b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
index 201b5eef2a0..15a0ceeaa71 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/test_cli_config.py
@@ -30,7 +30,7 @@ import httpx
 import pytest
 
 from airflowctl.api.client import Client
-from airflowctl.api.datamodels.generated import ConnectionTestResponse
+from airflowctl.api.datamodels.generated import ClearTaskInstancesBody, 
ConnectionTestResponse
 from airflowctl.api.operations import ConnectionsOperations, 
ServerResponseError
 from airflowctl.ctl import cli_parser
 from airflowctl.ctl.cli_config import (
@@ -886,11 +886,60 @@ class TestCliConfigMethods:
         # Should return params unchanged for other datamodels
         assert result == params, "Params should be unchanged for 
non-TriggerDAGRunPostBody datamodels"
 
+    def test_tasks_clear_args_follow_datamodel_defaults(self):
+        """Bool flags of ``tasks clear`` keep the ClearTaskInstancesBody 
defaults so a bare invocation only dry-runs."""
+        command_factory = CommandFactory()
+        tasks_group = next(
+            group_command for group_command in command_factory.group_commands 
if group_command.name == "tasks"
+        )
+        clear_command = next(
+            sub_command for sub_command in tasks_group.subcommands if 
sub_command.name == "clear"
+        )
+        args_by_flag = {arg.flags[0]: arg for arg in clear_command.args}
+
+        assert "dag_id" in args_by_flag, "required path parameter should be 
positional"
+        assert args_by_flag["--dry-run"].kwargs["action"] == 
BooleanOptionalAction
+        assert args_by_flag["--dry-run"].kwargs["default"] is True
+        assert args_by_flag["--only-failed"].kwargs["default"] is True
+        assert args_by_flag["--reset-dag-runs"].kwargs["default"] is True
+        assert args_by_flag["--only-running"].kwargs["default"] is False
+        assert args_by_flag["--run-on-latest-version"].kwargs["default"] is 
False
+        assert args_by_flag["--task-ids"].kwargs["type"] is str
+        assert "--output" in args_by_flag
+
+    @pytest.mark.parametrize(
+        ("raw_task_ids", "expected_task_ids"),
+        [
+            ("task_1", ["task_1"]),
+            ("task_1,task_2", ["task_1", "task_2"]),
+            (" task_1 , task_2 ,", ["task_1", "task_2"]),
+            ('["task_1", ["mapped_task", 0]]', ["task_1", ["mapped_task", 0]]),
+            (None, None),
+        ],
+    )
+    def test_apply_datamodel_defaults_clear_task_instances_task_ids(self, 
raw_task_ids, expected_task_ids):
+        """Test _apply_datamodel_defaults parses --task-ids strings for 
ClearTaskInstancesBody."""
+        command_factory = CommandFactory()
+        result = command_factory._apply_datamodel_defaults(
+            ClearTaskInstancesBody, {"task_ids": raw_task_ids, "dry_run": True}
+        )
+
+        assert result["task_ids"] == expected_task_ids
+        assert result["dry_run"] is True
+
+    def 
test_apply_datamodel_defaults_clear_task_instances_invalid_json_task_ids(self):
+        """Test _apply_datamodel_defaults rejects malformed JSON lists passed 
to --task-ids."""
+        command_factory = CommandFactory()
+        with pytest.raises(SystemExit, match="Invalid JSON list for 
--task-ids"):
+            command_factory._apply_datamodel_defaults(ClearTaskInstancesBody, 
{"task_ids": '["oops'})
+
     @pytest.mark.parametrize(
         ("group_name", "subcommand_name", "expected_help"),
         [
             ("assets", "get", "Retrieve an asset by its ID"),
             ("connections", "get", "Retrieve a connection by its ID"),
+            ("taskinstances", "list", "List all task instances for a given Dag 
run"),
+            ("tasks", "clear", "Clear task instances of a Dag by its ID"),
         ],
     )
     def test_help_texts_used_for_auto_generated_commands(self, group_name, 
subcommand_name, expected_help):
diff --git a/scripts/in_container/run_capture_airflowctl_help.py 
b/scripts/in_container/run_capture_airflowctl_help.py
index 9529dbe0439..52753d6a551 100644
--- a/scripts/in_container/run_capture_airflowctl_help.py
+++ b/scripts/in_container/run_capture_airflowctl_help.py
@@ -45,6 +45,8 @@ COMMANDS = [
     "jobs",
     "pools",
     "providers",
+    "taskinstances",
+    "tasks",
     "variables",
     "version",
     "plugins",

Reply via email to