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 ecd6fd4ad24 Add airflowctl dags state command (#68564)
ecd6fd4ad24 is described below

commit ecd6fd4ad24a88c8831c8a802d0ed020a64b5f44
Author: Henry Chen <[email protected]>
AuthorDate: Thu Jul 2 02:52:36 2026 +0800

    Add airflowctl dags state command (#68564)
---
 .../airflowctl_tests/test_airflowctl_commands.py   |   4 +-
 airflow-ctl/docs/images/command_hashes.txt         |   2 +-
 airflow-ctl/docs/images/output_dags.svg            | 130 +++++++++++----------
 airflow-ctl/src/airflowctl/api/client.py           |   3 +-
 airflow-ctl/src/airflowctl/api/operations.py       |  21 +++-
 airflow-ctl/src/airflowctl/ctl/cli_config.py       |  14 +++
 .../src/airflowctl/ctl/commands/dag_command.py     |  57 ++++++++-
 airflow-ctl/src/airflowctl/ctl/help_texts.yaml     |   1 +
 airflow-ctl/tests/airflow_ctl/api/test_client.py   |  30 ++++-
 .../tests/airflow_ctl/api/test_operations.py       |  36 ++++++
 .../airflow_ctl/ctl/commands/test_dag_command.py   | 117 +++++++++++++++++++
 docs/spelling_wordlist.txt                         |   2 +
 12 files changed, 347 insertions(+), 70 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 e1cfc665804..2d14b1d66f5 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -82,8 +82,10 @@ TEST_COMMANDS = [
     "dags list-import-errors",
     "dags list-version example_bash_operator",
     "dags list-warning",
-    # Order of trigger and pause/unpause is important for test stability 
because state checked
+    # Order of trigger, state, and pause/unpause is important for test 
stability
     "dags trigger example_bash_operator --logical-date={date_param} 
--run-after={date_param}",
+    'dags state example_bash_operator "manual__{date_param}"',
+    'dags state example_bash_operator "{date_param}"',
     # Test trigger without logical-date (should default to now)
     "dags trigger example_bash_operator",
     "dags next-execution example_bash_operator",
diff --git a/airflow-ctl/docs/images/command_hashes.txt 
b/airflow-ctl/docs/images/command_hashes.txt
index 3a0962cc4ee..4f43fd10bdf 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:6b38e6bcd491bc1941e7814b77e63bde
+dags:5ad68174a1111563dff870a241914b30
 dagrun:c32e0011aa9a845456c778786717208e
 jobs:a5b644c5da8889443bb40ee10b599270
 pools:19efe105b9515ab1926ebcaf0e028d71
diff --git a/airflow-ctl/docs/images/output_dags.svg 
b/airflow-ctl/docs/images/output_dags.svg
index 7f95b26bdbf..70f2cda4cd0 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 684.4" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 933 708.8" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -19,142 +19,146 @@
         font-weight: 700;
     }
 
-    .terminal-323819653-matrix {
+    .terminal-3245301439-matrix {
         font-family: Fira Code, monospace;
         font-size: 20px;
         line-height: 24.4px;
         font-variant-east-asian: full-width;
     }
 
-    .terminal-323819653-title {
+    .terminal-3245301439-title {
         font-size: 18px;
         font-weight: bold;
         font-family: arial;
     }
 
-    .terminal-323819653-r1 { fill: #ff8700 }
-.terminal-323819653-r2 { fill: #c5c8c6 }
-.terminal-323819653-r3 { fill: #808080 }
-.terminal-323819653-r4 { fill: #68a0b3 }
+    .terminal-3245301439-r1 { fill: #ff8700 }
+.terminal-3245301439-r2 { fill: #c5c8c6 }
+.terminal-3245301439-r3 { fill: #808080 }
+.terminal-3245301439-r4 { fill: #68a0b3 }
     </style>
 
     <defs>
-    <clipPath id="terminal-323819653-clip-terminal">
-      <rect x="0" y="0" width="914.0" height="633.4" />
+    <clipPath id="terminal-3245301439-clip-terminal">
+      <rect x="0" y="0" width="914.0" height="657.8" />
     </clipPath>
-    <clipPath id="terminal-323819653-line-0">
+    <clipPath id="terminal-3245301439-line-0">
     <rect x="0" y="1.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-1">
+<clipPath id="terminal-3245301439-line-1">
     <rect x="0" y="25.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-2">
+<clipPath id="terminal-3245301439-line-2">
     <rect x="0" y="50.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-3">
+<clipPath id="terminal-3245301439-line-3">
     <rect x="0" y="74.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-4">
+<clipPath id="terminal-3245301439-line-4">
     <rect x="0" y="99.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-5">
+<clipPath id="terminal-3245301439-line-5">
     <rect x="0" y="123.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-6">
+<clipPath id="terminal-3245301439-line-6">
     <rect x="0" y="147.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-7">
+<clipPath id="terminal-3245301439-line-7">
     <rect x="0" y="172.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-8">
+<clipPath id="terminal-3245301439-line-8">
     <rect x="0" y="196.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-9">
+<clipPath id="terminal-3245301439-line-9">
     <rect x="0" y="221.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-10">
+<clipPath id="terminal-3245301439-line-10">
     <rect x="0" y="245.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-11">
+<clipPath id="terminal-3245301439-line-11">
     <rect x="0" y="269.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-12">
+<clipPath id="terminal-3245301439-line-12">
     <rect x="0" y="294.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-13">
+<clipPath id="terminal-3245301439-line-13">
     <rect x="0" y="318.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-14">
+<clipPath id="terminal-3245301439-line-14">
     <rect x="0" y="343.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-15">
+<clipPath id="terminal-3245301439-line-15">
     <rect x="0" y="367.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-16">
+<clipPath id="terminal-3245301439-line-16">
     <rect x="0" y="391.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-17">
+<clipPath id="terminal-3245301439-line-17">
     <rect x="0" y="416.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-18">
+<clipPath id="terminal-3245301439-line-18">
     <rect x="0" y="440.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-19">
+<clipPath id="terminal-3245301439-line-19">
     <rect x="0" y="465.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-20">
+<clipPath id="terminal-3245301439-line-20">
     <rect x="0" y="489.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-21">
+<clipPath id="terminal-3245301439-line-21">
     <rect x="0" y="513.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-22">
+<clipPath id="terminal-3245301439-line-22">
     <rect x="0" y="538.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-23">
+<clipPath id="terminal-3245301439-line-23">
     <rect x="0" y="562.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-323819653-line-24">
+<clipPath id="terminal-3245301439-line-24">
     <rect x="0" y="587.1" width="915" height="24.65"/>
             </clipPath>
+<clipPath id="terminal-3245301439-line-25">
+    <rect x="0" y="611.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="682.4" rx="8"/>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="931" height="706.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-323819653-clip-terminal)">
+    <g transform="translate(9, 41)" 
clip-path="url(#terminal-3245301439-clip-terminal)">
     
-    <g class="terminal-323819653-matrix">
-    <text class="terminal-323819653-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-323819653-line-0)">Usage:</text><text 
class="terminal-323819653-r3" x="85.4" y="20" textLength="183" 
clip-path="url(#terminal-323819653-line-0)">airflowctl&#160;dags</text><text 
class="terminal-323819653-r2" x="268.4" y="20" textLength="24.4" 
clip-path="url(#terminal-323819653-line-0)">&#160;[</text><text 
class="terminal-323819653-r4" x="292.8" y="20" textLength="24.4" 
clip-path="url(#terminal [...]
-</text><text class="terminal-323819653-r2" x="915" y="44.4" textLength="12.2" 
clip-path="url(#terminal-323819653-line-1)">
-</text><text class="terminal-323819653-r2" x="0" y="68.8" textLength="280.6" 
clip-path="url(#terminal-323819653-line-2)">Perform&#160;Dags&#160;operations</text><text
 class="terminal-323819653-r2" x="915" y="68.8" textLength="12.2" 
clip-path="url(#terminal-323819653-line-2)">
-</text><text class="terminal-323819653-r2" x="915" y="93.2" textLength="12.2" 
clip-path="url(#terminal-323819653-line-3)">
-</text><text class="terminal-323819653-r1" x="0" y="117.6" textLength="256.2" 
clip-path="url(#terminal-323819653-line-4)">Positional&#160;Arguments:</text><text
 class="terminal-323819653-r2" x="915" y="117.6" textLength="12.2" 
clip-path="url(#terminal-323819653-line-4)">
-</text><text class="terminal-323819653-r4" x="24.4" y="142" textLength="85.4" 
clip-path="url(#terminal-323819653-line-5)">COMMAND</text><text 
class="terminal-323819653-r2" x="915" y="142" textLength="12.2" 
clip-path="url(#terminal-323819653-line-5)">
-</text><text class="terminal-323819653-r4" x="48.8" y="166.4" 
textLength="73.2" 
clip-path="url(#terminal-323819653-line-6)">delete</text><text 
class="terminal-323819653-r2" x="268.4" y="166.4" textLength="268.4" 
clip-path="url(#terminal-323819653-line-6)">Delete&#160;a&#160;Dag&#160;by&#160;its&#160;ID</text><text
 class="terminal-323819653-r2" x="915" y="166.4" textLength="12.2" 
clip-path="url(#terminal-323819653-line-6)">
-</text><text class="terminal-323819653-r4" x="48.8" y="190.8" 
textLength="36.6" clip-path="url(#terminal-323819653-line-7)">get</text><text 
class="terminal-323819653-r2" x="268.4" y="190.8" textLength="292.8" 
clip-path="url(#terminal-323819653-line-7)">Retrieve&#160;a&#160;Dag&#160;by&#160;its&#160;ID</text><text
 class="terminal-323819653-r2" x="915" y="190.8" textLength="12.2" 
clip-path="url(#terminal-323819653-line-7)">
-</text><text class="terminal-323819653-r4" x="48.8" y="215.2" 
textLength="134.2" 
clip-path="url(#terminal-323819653-line-8)">get-details</text><text 
class="terminal-323819653-r2" x="268.4" y="215.2" textLength="475.8" 
clip-path="url(#terminal-323819653-line-8)">Retrieve&#160;detailed&#160;information&#160;for&#160;a&#160;Dag</text><text
 class="terminal-323819653-r2" x="915" y="215.2" textLength="12.2" 
clip-path="url(#terminal-323819653-line-8)">
-</text><text class="terminal-323819653-r4" x="48.8" y="239.6" 
textLength="195.2" 
clip-path="url(#terminal-323819653-line-9)">get-import-error</text><text 
class="terminal-323819653-r2" x="268.4" y="239.6" textLength="451.4" 
clip-path="url(#terminal-323819653-line-9)">Retrieve&#160;a&#160;Dag&#160;import&#160;error&#160;by&#160;its&#160;ID</text><text
 class="terminal-323819653-r2" x="915" y="239.6" textLength="12.2" 
clip-path="url(#terminal-323819653-line-9)">
-</text><text class="terminal-323819653-r4" x="48.8" y="264" textLength="109.8" 
clip-path="url(#terminal-323819653-line-10)">get-stats</text><text 
class="terminal-323819653-r2" x="268.4" y="264" textLength="536.8" 
clip-path="url(#terminal-323819653-line-10)">Retrieve&#160;run&#160;statistics&#160;for&#160;one&#160;or&#160;more&#160;Dags</text><text
 class="terminal-323819653-r2" x="915" y="264" textLength="12.2" 
clip-path="url(#terminal-323819653-line-10)">
-</text><text class="terminal-323819653-r4" x="48.8" y="288.4" 
textLength="97.6" 
clip-path="url(#terminal-323819653-line-11)">get-tags</text><text 
class="terminal-323819653-r2" x="268.4" y="288.4" textLength="366" 
clip-path="url(#terminal-323819653-line-11)">List&#160;all&#160;tags&#160;used&#160;across&#160;Dags</text><text
 class="terminal-323819653-r2" x="915" y="288.4" textLength="12.2" 
clip-path="url(#terminal-323819653-line-11)">
-</text><text class="terminal-323819653-r4" x="48.8" y="312.8" 
textLength="134.2" 
clip-path="url(#terminal-323819653-line-12)">get-version</text><text 
class="terminal-323819653-r2" x="268.4" y="312.8" textLength="439.2" 
clip-path="url(#terminal-323819653-line-12)">Retrieve&#160;a&#160;specific&#160;version&#160;of&#160;a&#160;Dag</text><text
 class="terminal-323819653-r2" x="915" y="312.8" textLength="12.2" 
clip-path="url(#terminal-323819653-line-12)">
-</text><text class="terminal-323819653-r4" x="48.8" y="337.2" 
textLength="48.8" clip-path="url(#terminal-323819653-line-13)">list</text><text 
class="terminal-323819653-r2" x="268.4" y="337.2" textLength="158.6" 
clip-path="url(#terminal-323819653-line-13)">List&#160;all&#160;Dags</text><text
 class="terminal-323819653-r2" x="915" y="337.2" textLength="12.2" 
clip-path="url(#terminal-323819653-line-13)">
-</text><text class="terminal-323819653-r4" x="48.8" y="361.6" 
textLength="219.6" 
clip-path="url(#terminal-323819653-line-14)">list-import-errors</text><text 
class="terminal-323819653-r2" x="915" y="361.6" textLength="12.2" 
clip-path="url(#terminal-323819653-line-14)">
-</text><text class="terminal-323819653-r2" x="268.4" y="386" 
textLength="317.2" 
clip-path="url(#terminal-323819653-line-15)">List&#160;all&#160;Dag&#160;import&#160;errors</text><text
 class="terminal-323819653-r2" x="915" y="386" textLength="12.2" 
clip-path="url(#terminal-323819653-line-15)">
-</text><text class="terminal-323819653-r4" x="48.8" y="410.4" 
textLength="146.4" 
clip-path="url(#terminal-323819653-line-16)">list-version</text><text 
class="terminal-323819653-r2" x="268.4" y="410.4" textLength="317.2" 
clip-path="url(#terminal-323819653-line-16)">List&#160;all&#160;versions&#160;of&#160;a&#160;Dag</text><text
 class="terminal-323819653-r2" x="915" y="410.4" textLength="12.2" 
clip-path="url(#terminal-323819653-line-16)">
-</text><text class="terminal-323819653-r4" x="48.8" y="434.8" 
textLength="146.4" 
clip-path="url(#terminal-323819653-line-17)">list-warning</text><text 
class="terminal-323819653-r2" x="268.4" y="434.8" textLength="256.2" 
clip-path="url(#terminal-323819653-line-17)">List&#160;all&#160;Dag&#160;warnings</text><text
 class="terminal-323819653-r2" x="915" y="434.8" textLength="12.2" 
clip-path="url(#terminal-323819653-line-17)">
-</text><text class="terminal-323819653-r4" x="48.8" y="459.2" 
textLength="170.8" 
clip-path="url(#terminal-323819653-line-18)">next-execution</text><text 
class="terminal-323819653-r2" x="268.4" y="459.2" textLength="585.6" 
clip-path="url(#terminal-323819653-line-18)">Show&#160;the&#160;next&#160;scheduled&#160;execution&#160;time&#160;for&#160;a&#160;Dag</text><text
 class="terminal-323819653-r2" x="915" y="459.2" textLength="12.2" 
clip-path="url(#terminal-323819653-line-18)">
-</text><text class="terminal-323819653-r4" x="48.8" y="483.6" textLength="61" 
clip-path="url(#terminal-323819653-line-19)">pause</text><text 
class="terminal-323819653-r2" x="268.4" y="483.6" textLength="134.2" 
clip-path="url(#terminal-323819653-line-19)">Pause&#160;a&#160;Dag</text><text 
class="terminal-323819653-r2" x="915" y="483.6" textLength="12.2" 
clip-path="url(#terminal-323819653-line-19)">
-</text><text class="terminal-323819653-r4" x="48.8" y="508" textLength="85.4" 
clip-path="url(#terminal-323819653-line-20)">trigger</text><text 
class="terminal-323819653-r2" x="268.4" y="508" textLength="256.2" 
clip-path="url(#terminal-323819653-line-20)">Trigger&#160;a&#160;new&#160;Dag&#160;run</text><text
 class="terminal-323819653-r2" x="915" y="508" textLength="12.2" 
clip-path="url(#terminal-323819653-line-20)">
-</text><text class="terminal-323819653-r4" x="48.8" y="532.4" 
textLength="85.4" 
clip-path="url(#terminal-323819653-line-21)">unpause</text><text 
class="terminal-323819653-r2" x="268.4" y="532.4" textLength="158.6" 
clip-path="url(#terminal-323819653-line-21)">Unpause&#160;a&#160;Dag</text><text
 class="terminal-323819653-r2" x="915" y="532.4" textLength="12.2" 
clip-path="url(#terminal-323819653-line-21)">
-</text><text class="terminal-323819653-r4" x="48.8" y="556.8" 
textLength="73.2" 
clip-path="url(#terminal-323819653-line-22)">update</text><text 
class="terminal-323819653-r2" x="268.4" y="556.8" textLength="317.2" 
clip-path="url(#terminal-323819653-line-22)">Update&#160;properties&#160;of&#160;a&#160;Dag</text><text
 class="terminal-323819653-r2" x="915" y="556.8" textLength="12.2" 
clip-path="url(#terminal-323819653-line-22)">
-</text><text class="terminal-323819653-r2" x="915" y="581.2" textLength="12.2" 
clip-path="url(#terminal-323819653-line-23)">
-</text><text class="terminal-323819653-r1" x="0" y="605.6" textLength="97.6" 
clip-path="url(#terminal-323819653-line-24)">Options:</text><text 
class="terminal-323819653-r2" x="915" y="605.6" textLength="12.2" 
clip-path="url(#terminal-323819653-line-24)">
-</text><text class="terminal-323819653-r4" x="24.4" y="630" textLength="24.4" 
clip-path="url(#terminal-323819653-line-25)">-h</text><text 
class="terminal-323819653-r2" x="48.8" y="630" textLength="24.4" 
clip-path="url(#terminal-323819653-line-25)">,&#160;</text><text 
class="terminal-323819653-r4" x="73.2" y="630" textLength="73.2" 
clip-path="url(#terminal-323819653-line-25)">--help</text><text 
class="terminal-323819653-r2" x="268.4" y="630" textLength="378.2" 
clip-path="url(#terminal-323 [...]
+    <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 [...]
 </text>
     </g>
     </g>
diff --git a/airflow-ctl/src/airflowctl/api/client.py 
b/airflow-ctl/src/airflowctl/api/client.py
index b01200fac1c..de2cec4ea5f 100644
--- a/airflow-ctl/src/airflowctl/api/client.py
+++ b/airflow-ctl/src/airflowctl/api/client.py
@@ -111,7 +111,8 @@ def get_json_error(response: httpx.Response):
     if err:
         # This part is used in integration tests to verify the error message
         # If you are updating here don't forget to update the airflow-ctl-tests
-        log.warning("Server error ", extra=dict(err.response.json()))
+        if not 
response.request.extensions.get("airflowctl_suppress_error_log"):
+            log.warning("Server error ", extra=dict(err.response.json()))
         raise err
 
 
diff --git a/airflow-ctl/src/airflowctl/api/operations.py 
b/airflow-ctl/src/airflowctl/api/operations.py
index d1782afaaaa..94f39a3a915 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -601,10 +601,15 @@ class DagsOperations(BaseOperations):
 class DagRunOperations(BaseOperations):
     """Dag run operations."""
 
-    def get(self, dag_id: str, dag_run_id: str) -> DAGRunResponse | 
ServerResponseError:
+    def get(
+        self, dag_id: str, dag_run_id: str, *, suppress_error_log: bool = False
+    ) -> DAGRunResponse | ServerResponseError:
         """Get a dag run."""
         try:
-            self.response = 
self.client.get(f"/dags/{dag_id}/dagRuns/{dag_run_id}")
+            self.response = self.client.get(
+                f"/dags/{dag_id}/dagRuns/{dag_run_id}",
+                extensions={"airflowctl_suppress_error_log": 
suppress_error_log},
+            )
             return DAGRunResponse.model_validate_json(self.response.content)
         except ServerResponseError as e:
             raise e
@@ -616,6 +621,9 @@ class DagRunOperations(BaseOperations):
         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,
+        order_by: str | None = None,
     ) -> DAGRunCollectionResponse | ServerResponseError:
         """
         List dag runs (at most `limit` results).
@@ -626,6 +634,9 @@ class DagRunOperations(BaseOperations):
             end_date: Filter dag runs by end date (optional)
             limit: Limit the number of results returned
             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.
+            order_by: Order the results by the specified field.
         """
         # Use "~" for all DAGs if dag_id is not specified
         if not dag_id:
@@ -638,6 +649,12 @@ class DagRunOperations(BaseOperations):
             params["start_date"] = start_date.isoformat()
         if end_date is not None:
             params["end_date"] = end_date.isoformat()
+        if logical_date_gte is not None:
+            params["logical_date_gte"] = logical_date_gte.isoformat()
+        if logical_date_lte is not None:
+            params["logical_date_lte"] = logical_date_lte.isoformat()
+        if order_by is not None:
+            params["order_by"] = order_by
 
         try:
             self.response = self.client.get(f"/dags/{dag_id}/dagRuns", 
params=params)
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py 
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index 11ff4542e01..d0901e795ef 100755
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -267,6 +267,11 @@ ARG_DAG_ID = Arg(
     type=str,
     help="The Dag ID of the Dag to pause or unpause",
 )
+ARG_LOGICAL_DATE_OR_RUN_ID = Arg(
+    flags=("logical_date_or_run_id",),
+    type=str,
+    help="The logical date with a timezone offset or run ID of the Dag run",
+)
 
 ARG_ACTION_ON_EXISTING_KEY = Arg(
     flags=("-a", "--action-on-existing-key"),
@@ -977,6 +982,15 @@ DAG_COMMANDS = (
             ARG_OUTPUT,
         ),
     ),
+    ActionCommand(
+        name="state",
+        help="Get the status of a Dag run",
+        func=lazy_load_command("airflowctl.ctl.commands.dag_command.state"),
+        args=(
+            ARG_DAG_ID,
+            ARG_LOGICAL_DATE_OR_RUN_ID,
+        ),
+    ),
     ActionCommand(
         name="unpause",
         help="Unpause 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 301821f9c2c..f0562891d11 100644
--- a/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/dag_command.py
@@ -17,13 +17,16 @@
 
 from __future__ import annotations
 
+import datetime
+import json
 import sys
 from typing import Literal
 
 import rich
+from rich.text import Text
 
 from airflowctl.api.client import NEW_API_CLIENT, ClientKind, 
ServerResponseError, provide_api_client
-from airflowctl.api.datamodels.generated import DAGPatchBody
+from airflowctl.api.datamodels.generated import DAGPatchBody, DAGRunResponse
 from airflowctl.ctl.console_formatting import AirflowConsole
 
 
@@ -103,3 +106,55 @@ def next_execution(args, api_client=NEW_API_CLIENT) -> 
dict | None:
         output=args.output,
     )
     return result
+
+
+def _parse_logical_date(value: str) -> datetime.datetime | None:
+    """Parse an ISO-formatted logical date."""
+    try:
+        logical_date = datetime.datetime.fromisoformat(value.replace("Z", 
"+00:00"))
+    except ValueError:
+        return None
+    if logical_date.tzinfo is None:
+        raise SystemExit("Logical date must include a timezone offset")
+    return logical_date
+
+
+def _get_dag_run_by_run_id_or_logical_date(api_client, dag_id: str, value: 
str) -> DAGRunResponse | None:
+    """Get a Dag run by run ID, falling back to an exact logical date match."""
+    try:
+        return api_client.dag_runs.get(dag_id=dag_id, dag_run_id=value, 
suppress_error_log=True)
+    except ServerResponseError as e:
+        if e.response.status_code != 404:
+            raise
+
+    if logical_date := _parse_logical_date(value):
+        response = api_client.dag_runs.list(
+            dag_id=dag_id,
+            logical_date_gte=logical_date,
+            logical_date_lte=logical_date,
+            order_by="-id",
+            limit=1,
+        )
+        if response.dag_runs:
+            return response.dag_runs[0]
+    else:
+        api_client.dag_runs.list(dag_id=dag_id, limit=1)
+    return None
+
+
+@provide_api_client(kind=ClientKind.CLI)
+def state(args, api_client=NEW_API_CLIENT) -> None:
+    """Show the state and configuration of a Dag run."""
+    dag_run = _get_dag_run_by_run_id_or_logical_date(
+        api_client=api_client,
+        dag_id=args.dag_id,
+        value=args.logical_date_or_run_id,
+    )
+    if not dag_run:
+        rich.print("[yellow]No matching Dag run found.[/yellow]")
+    else:
+        state_value = getattr(dag_run.state, "value", dag_run.state)
+        if dag_run.conf:
+            rich.print(Text(f"{state_value}, {json.dumps(dag_run.conf)}"))
+        else:
+            rich.print(Text(state_value))
diff --git a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml 
b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
index 2e3266fc205..88cc0a15c9d 100644
--- a/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
+++ b/airflow-ctl/src/airflowctl/ctl/help_texts.yaml
@@ -64,6 +64,7 @@ dags:
   get-version: "Retrieve a specific version of a Dag"
   list-version: "List all versions of a Dag"
   list-warning: "List all Dag warnings"
+  state: "Get the status of a Dag run"
   trigger: "Trigger a new Dag run"
 
 dagrun:
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_client.py 
b/airflow-ctl/tests/airflow_ctl/api/test_client.py
index 56cab7f7a8e..3ce4466efef 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_client.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_client.py
@@ -28,7 +28,14 @@ import pytest
 import time_machine
 from httpx import URL
 
-from airflowctl.api.client import Client, ClientKind, Credentials, 
_bounded_get_new_password, get_client
+from airflowctl.api.client import (
+    Client,
+    ClientKind,
+    Credentials,
+    _bounded_get_new_password,
+    get_client,
+    get_json_error,
+)
 from airflowctl.api.operations import ServerResponseError
 from airflowctl.exceptions import (
     AirflowCtlCredentialNotFoundException,
@@ -100,6 +107,27 @@ class TestClient:
             client.get("http://error";)
         assert err.value.args == ("Client error message: {'detail': 'Not 
found'}",)
 
+    @pytest.mark.parametrize(
+        ("suppress_error_log", "expected_warning_count"),
+        [
+            pytest.param(False, 1, id="logs-error"),
+            pytest.param(True, 0, id="suppresses-error-log"),
+        ],
+    )
+    @patch("airflowctl.api.client.log.warning")
+    def test_error_log_suppression(self, mock_warning, suppress_error_log, 
expected_warning_count):
+        request = httpx.Request(
+            "GET",
+            "http://error";,
+            extensions={"airflowctl_suppress_error_log": suppress_error_log},
+        )
+        response = httpx.Response(404, request=request, json={"detail": "Not 
found"})
+
+        with pytest.raises(ServerResponseError):
+            get_json_error(response)
+
+        assert mock_warning.call_count == expected_warning_count
+
     @pytest.mark.parametrize(
         ("base_url", "client_kind", "expected_base_url"),
         [
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py 
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index 74bc68ed05e..5d8d0c0f689 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -1187,6 +1187,20 @@ class TestDagRunOperations:
         response = client.dag_runs.get(dag_id=self.dag_id, 
dag_run_id=self.dag_run_id)
         assert response == self.dag_run_response
 
+    @pytest.mark.parametrize("suppress_error_log", [False, True])
+    def test_get_passes_error_log_suppression_extension(self, 
suppress_error_log):
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.extensions["airflowctl_suppress_error_log"] is 
suppress_error_log
+            return httpx.Response(200, 
json=json.loads(self.dag_run_response.model_dump_json()))
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.dag_runs.get(
+            dag_id=self.dag_id,
+            dag_run_id=self.dag_run_id,
+            suppress_error_log=suppress_error_log,
+        )
+        assert response == self.dag_run_response
+
     def test_list(self):
         def handle_request(request: httpx.Request) -> httpx.Response:
             assert request.url.path == f"/api/v2/dags/{self.dag_id}/dagRuns"
@@ -1202,6 +1216,28 @@ class TestDagRunOperations:
         )
         assert response == self.dag_run_collection_response
 
+    def test_list_with_logical_date_filters_and_order(self):
+        logical_date = datetime.datetime(2025, 1, 1, 
tzinfo=datetime.timezone.utc)
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert dict(request.url.params) == {
+                "limit": "1",
+                "logical_date_gte": logical_date.isoformat(),
+                "logical_date_lte": logical_date.isoformat(),
+                "order_by": "-id",
+            }
+            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,
+            logical_date_gte=logical_date,
+            logical_date_lte=logical_date,
+            order_by="-id",
+            limit=1,
+        )
+        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 405eb030065..f5966f09c0f 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,15 +17,24 @@
 from __future__ import annotations
 
 import datetime
+from unittest import mock
 
+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.ctl import cli_parser
 from airflowctl.ctl.commands import dag_command
 
 
+def _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)
+
+
 class TestDagCommands:
     parser = cli_parser.get_parser()
     dag_id = "test_dag"
@@ -215,3 +224,111 @@ class TestDagCommands:
                 self.parser.parse_args(["dags", "next-execution", 
self.dag_id]),
                 api_client=api_client,
             )
+
+    def test_state_by_run_id(self, capsys):
+        api_client = mock.MagicMock()
+        api_client.dag_runs.get.return_value = mock.MagicMock(state="success", 
conf={})
+
+        dag_command.state(
+            self.parser.parse_args(["dags", "state", self.dag_id, "test_run"]),
+            api_client=api_client,
+        )
+
+        assert capsys.readouterr().out.strip() == "success"
+        api_client.dag_runs.get.assert_called_once_with(
+            dag_id=self.dag_id,
+            dag_run_id="test_run",
+            suppress_error_log=True,
+        )
+        api_client.dag_runs.list.assert_not_called()
+
+    def test_state_by_logical_date(self, capsys):
+        api_client = mock.MagicMock()
+        logical_date = datetime.datetime(2025, 1, 1, 
tzinfo=datetime.timezone.utc)
+        api_client.dag_runs.get.side_effect = _server_error(404)
+        api_client.dag_runs.list.return_value.dag_runs = [
+            mock.MagicMock(state="failed", conf={"reason": "[red]test[/red]"})
+        ]
+
+        dag_command.state(
+            self.parser.parse_args(["dags", "state", self.dag_id, 
logical_date.isoformat()]),
+            api_client=api_client,
+        )
+
+        assert capsys.readouterr().out.strip() == 'failed, {"reason": 
"[red]test[/red]"}'
+        api_client.dag_runs.list.assert_called_once_with(
+            dag_id=self.dag_id,
+            logical_date_gte=logical_date,
+            logical_date_lte=logical_date,
+            order_by="-id",
+            limit=1,
+        )
+
+    @pytest.mark.parametrize(
+        ("value", "expected_list_kwargs"),
+        [
+            pytest.param("missing_run", {"dag_id": dag_id, "limit": 1}, 
id="run-id"),
+            pytest.param(
+                "2025-01-01T00:00:00+00:00",
+                {
+                    "dag_id": dag_id,
+                    "logical_date_gte": datetime.datetime(2025, 1, 1, 
tzinfo=datetime.timezone.utc),
+                    "logical_date_lte": datetime.datetime(2025, 1, 1, 
tzinfo=datetime.timezone.utc),
+                    "order_by": "-id",
+                    "limit": 1,
+                },
+                id="logical-date",
+            ),
+        ],
+    )
+    @mock.patch("rich.print")
+    def test_state_missing_run_prints_message(self, mock_rich_print, value, 
expected_list_kwargs):
+        api_client = mock.MagicMock()
+        api_client.dag_runs.get.side_effect = _server_error(404)
+        api_client.dag_runs.list.return_value.dag_runs = []
+
+        dag_command.state(
+            self.parser.parse_args(["dags", "state", self.dag_id, value]),
+            api_client=api_client,
+        )
+
+        mock_rich_print.assert_called_once_with("[yellow]No matching Dag run 
found.[/yellow]")
+        
api_client.dag_runs.list.assert_called_once_with(**expected_list_kwargs)
+
+    def test_state_missing_dag_propagates_api_error(self):
+        api_client = mock.MagicMock()
+        api_client.dag_runs.get.side_effect = _server_error(404)
+        api_client.dag_runs.list.side_effect = error = _server_error(404)
+
+        with pytest.raises(ServerResponseError) as ctx:
+            dag_command.state(
+                self.parser.parse_args(["dags", "state", self.dag_id, 
"missing_run"]),
+                api_client=api_client,
+            )
+
+        assert ctx.value is error
+
+    def test_state_rejects_naive_logical_date(self):
+        api_client = mock.MagicMock()
+        api_client.dag_runs.get.side_effect = _server_error(404)
+
+        with pytest.raises(SystemExit, match="Logical date must include a 
timezone offset"):
+            dag_command.state(
+                self.parser.parse_args(["dags", "state", self.dag_id, 
"2025-01-01T00:00:00"]),
+                api_client=api_client,
+            )
+
+        api_client.dag_runs.list.assert_not_called()
+
+    def test_state_propagates_non_404_api_error(self):
+        api_client = mock.MagicMock()
+        api_client.dag_runs.get.side_effect = error = _server_error(500)
+
+        with pytest.raises(ServerResponseError) as ctx:
+            dag_command.state(
+                self.parser.parse_args(["dags", "state", self.dag_id, 
"test_run"]),
+                api_client=api_client,
+            )
+
+        assert ctx.value is error
+        api_client.dag_runs.list.assert_not_called()
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 50651dcfd0a..0d4ccbfffdd 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -746,6 +746,7 @@ Grpc
 gRPC
 grpc
 gsuite
+gte
 Gunicorn
 gunicorn
 gz
@@ -1014,6 +1015,7 @@ logstash
 longblob
 lookups
 lshift
+lte
 lxml
 m-NCUs
 machineTypes


Reply via email to