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

turaga 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 4e32b681b7a Add XCom CLI commands to airflowctl (#61021)
4e32b681b7a is described below

commit 4e32b681b7a9b89f86d820a14848fe8655a19133
Author: Dheeraj Turaga <[email protected]>
AuthorDate: Thu Jan 29 22:34:07 2026 -0600

    Add XCom CLI commands to airflowctl (#61021)
    
    * Add XCom CLI commands to airflowctl
    
    This commit adds `add`, `edit`, `delete`, `get`, and `list` commands for 
XComs to `airflowctl`.
    These commands allow managing XCom entries for a specific task instance.
    The commands support handling JSON values for XComs.
    
    Key changes:
    - Added `XComOperations` to `airflowctl/api/operations.py`.
    - Added `xcom` property to `Client` in `airflowctl/api/client.py`.
    - Exposed commands: `airflowctl xcom [add|edit|delete|get|list]`.
    
    * Fix XCom CLI commands and add comprehensive unit tests
    
      This commit fixes critical issues with XCom CLI commands in airflowctl
      and adds comprehensive test coverage for all XCom operations.
    
    * Add integration tests
---
 .../airflowctl_tests/test_airflowctl_commands.py   |   6 +
 airflow-ctl/docs/images/command_hashes.txt         |   2 +-
 airflow-ctl/docs/images/output_main.svg            | 174 +++++------
 airflow-ctl/src/airflowctl/api/client.py           |   7 +
 airflow-ctl/src/airflowctl/api/operations.py       | 127 ++++++++
 airflow-ctl/src/airflowctl/ctl/cli_config.py       |   2 +-
 .../tests/airflow_ctl/api/test_operations.py       | 337 +++++++++++++++++++++
 7 files changed, 568 insertions(+), 87 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 4e85b45be52..ceb29f0143d 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -95,6 +95,12 @@ TEST_COMMANDS = [
     "dags update --dag-id=example_bash_operator --no-is-paused",
     # DAG Run commands
     "dagrun list --dag-id example_bash_operator --state success --limit=1",
+    # XCom commands - need a DAG run with completed tasks
+    f'xcom add --dag-id=example_bash_operator 
--dag-run-id="manual__{ONE_DATE_PARAM}" --task-id=runme_0 --key=test_xcom_key 
--value=\'{{"test": "value"}}\'',
+    f'xcom get --dag-id=example_bash_operator 
--dag-run-id="manual__{ONE_DATE_PARAM}" --task-id=runme_0 --key=test_xcom_key',
+    f'xcom list --dag-id=example_bash_operator 
--dag-run-id="manual__{ONE_DATE_PARAM}" --task-id=runme_0',
+    f'xcom edit --dag-id=example_bash_operator 
--dag-run-id="manual__{ONE_DATE_PARAM}" --task-id=runme_0 --key=test_xcom_key 
--value=\'{{"updated": "value"}}\'',
+    f'xcom delete --dag-id=example_bash_operator 
--dag-run-id="manual__{ONE_DATE_PARAM}" --task-id=runme_0 --key=test_xcom_key',
     # Jobs commands
     "jobs list",
     # Pools commands
diff --git a/airflow-ctl/docs/images/command_hashes.txt 
b/airflow-ctl/docs/images/command_hashes.txt
index c0e3b5995fa..8a450901218 100644
--- a/airflow-ctl/docs/images/command_hashes.txt
+++ b/airflow-ctl/docs/images/command_hashes.txt
@@ -1,4 +1,4 @@
-main:deacf21c6300eae16afbf8cbd538f1ef
+main:65249416abad6ad24c276fb44326ae15
 assets:b3ae2b933e54528bf486ff28e887804d
 auth:f396d4bce90215599dde6ad0a8f30f29
 backfill:bbce9859a2d1ce054ad22db92dea8c05
diff --git a/airflow-ctl/docs/images/output_main.svg 
b/airflow-ctl/docs/images/output_main.svg
index f6c7225a4eb..8e4ef71bdb0 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 811 660.0" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 933 684.4" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -19,138 +19,142 @@
         font-weight: 700;
     }
 
-    .terminal-3400494481-matrix {
+    .terminal-4108169915-matrix {
         font-family: Fira Code, monospace;
         font-size: 20px;
         line-height: 24.4px;
         font-variant-east-asian: full-width;
     }
 
-    .terminal-3400494481-title {
+    .terminal-4108169915-title {
         font-size: 18px;
         font-weight: bold;
         font-family: arial;
     }
 
-    .terminal-3400494481-r1 { fill: #ff8700 }
-.terminal-3400494481-r2 { fill: #c5c8c6 }
-.terminal-3400494481-r3 { fill: #808080 }
-.terminal-3400494481-r4 { fill: #68a0b3 }
+    .terminal-4108169915-r1 { fill: #ff8700 }
+.terminal-4108169915-r2 { fill: #c5c8c6 }
+.terminal-4108169915-r3 { fill: #808080 }
+.terminal-4108169915-r4 { fill: #68a0b3 }
     </style>
 
     <defs>
-    <clipPath id="terminal-3400494481-clip-terminal">
-      <rect x="0" y="0" width="792.0" height="609.0" />
+    <clipPath id="terminal-4108169915-clip-terminal">
+      <rect x="0" y="0" width="914.0" height="633.4" />
     </clipPath>
-    <clipPath id="terminal-3400494481-line-0">
-    <rect x="0" y="1.5" width="793" height="24.65"/>
+    <clipPath id="terminal-4108169915-line-0">
+    <rect x="0" y="1.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-1">
-    <rect x="0" y="25.9" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-1">
+    <rect x="0" y="25.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-2">
-    <rect x="0" y="50.3" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-2">
+    <rect x="0" y="50.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-3">
-    <rect x="0" y="74.7" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-3">
+    <rect x="0" y="74.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-4">
-    <rect x="0" y="99.1" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-4">
+    <rect x="0" y="99.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-5">
-    <rect x="0" y="123.5" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-5">
+    <rect x="0" y="123.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-6">
-    <rect x="0" y="147.9" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-6">
+    <rect x="0" y="147.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-7">
-    <rect x="0" y="172.3" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-7">
+    <rect x="0" y="172.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-8">
-    <rect x="0" y="196.7" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-8">
+    <rect x="0" y="196.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-9">
-    <rect x="0" y="221.1" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-9">
+    <rect x="0" y="221.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-10">
-    <rect x="0" y="245.5" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-10">
+    <rect x="0" y="245.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-11">
-    <rect x="0" y="269.9" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-11">
+    <rect x="0" y="269.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-12">
-    <rect x="0" y="294.3" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-12">
+    <rect x="0" y="294.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-13">
-    <rect x="0" y="318.7" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-13">
+    <rect x="0" y="318.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-14">
-    <rect x="0" y="343.1" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-14">
+    <rect x="0" y="343.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-15">
-    <rect x="0" y="367.5" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-15">
+    <rect x="0" y="367.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-16">
-    <rect x="0" y="391.9" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-16">
+    <rect x="0" y="391.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-17">
-    <rect x="0" y="416.3" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-17">
+    <rect x="0" y="416.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-18">
-    <rect x="0" y="440.7" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-18">
+    <rect x="0" y="440.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-19">
-    <rect x="0" y="465.1" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-19">
+    <rect x="0" y="465.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-20">
-    <rect x="0" y="489.5" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-20">
+    <rect x="0" y="489.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-21">
-    <rect x="0" y="513.9" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-21">
+    <rect x="0" y="513.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-22">
-    <rect x="0" y="538.3" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-22">
+    <rect x="0" y="538.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3400494481-line-23">
-    <rect x="0" y="562.7" width="793" height="24.65"/>
+<clipPath id="terminal-4108169915-line-23">
+    <rect x="0" y="562.7" width="915" height="24.65"/>
+            </clipPath>
+<clipPath id="terminal-4108169915-line-24">
+    <rect x="0" y="587.1" 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="809" height="658" rx="8"/>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="931" height="682.4" 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-3400494481-clip-terminal)">
+    <g transform="translate(9, 41)" 
clip-path="url(#terminal-4108169915-clip-terminal)">
     
-    <g class="terminal-3400494481-matrix">
-    <text class="terminal-3400494481-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-3400494481-line-0)">Usage:</text><text 
class="terminal-3400494481-r3" x="85.4" y="20" textLength="122" 
clip-path="url(#terminal-3400494481-line-0)">airflowctl</text><text 
class="terminal-3400494481-r2" x="207.4" y="20" textLength="24.4" 
clip-path="url(#terminal-3400494481-line-0)">&#160;[</text><text 
class="terminal-3400494481-r4" x="231.8" y="20" textLength="24.4" 
clip-path="url(#terminal-34 [...]
-</text><text class="terminal-3400494481-r2" x="793" y="44.4" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-1)">
-</text><text class="terminal-3400494481-r1" x="0" y="68.8" textLength="256.2" 
clip-path="url(#terminal-3400494481-line-2)">Positional&#160;Arguments:</text><text
 class="terminal-3400494481-r2" x="793" y="68.8" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-2)">
-</text><text class="terminal-3400494481-r4" x="24.4" y="93.2" 
textLength="195.2" 
clip-path="url(#terminal-3400494481-line-3)">GROUP_OR_COMMAND</text><text 
class="terminal-3400494481-r2" x="793" y="93.2" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-3)">
-</text><text class="terminal-3400494481-r2" x="793" y="117.6" 
textLength="12.2" clip-path="url(#terminal-3400494481-line-4)">
-</text><text class="terminal-3400494481-r4" x="0" y="142" textLength="122" 
clip-path="url(#terminal-3400494481-line-5)">&#160;&#160;&#160;&#160;Groups</text><text
 class="terminal-3400494481-r2" x="793" y="142" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-5)">
-</text><text class="terminal-3400494481-r4" x="73.2" y="166.4" 
textLength="73.2" 
clip-path="url(#terminal-3400494481-line-6)">assets</text><text 
class="terminal-3400494481-r2" x="244" y="166.4" textLength="305" 
clip-path="url(#terminal-3400494481-line-6)">Perform&#160;Assets&#160;operations</text><text
 class="terminal-3400494481-r2" x="793" y="166.4" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-6)">
-</text><text class="terminal-3400494481-r4" x="73.2" y="190.8" 
textLength="48.8" clip-path="url(#terminal-3400494481-line-7)">auth</text><text 
class="terminal-3400494481-r2" x="244" y="190.8" textLength="512.4" 
clip-path="url(#terminal-3400494481-line-7)">Manage&#160;authentication&#160;for&#160;CLI.&#160;Either&#160;pass</text><text
 class="terminal-3400494481-r2" x="793" y="190.8" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-7)">
-</text><text class="terminal-3400494481-r2" x="244" y="215.2" 
textLength="500.2" 
clip-path="url(#terminal-3400494481-line-8)">token&#160;from&#160;environment&#160;variable/parameter</text><text
 class="terminal-3400494481-r2" x="793" y="215.2" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-8)">
-</text><text class="terminal-3400494481-r2" x="244" y="239.6" textLength="366" 
clip-path="url(#terminal-3400494481-line-9)">or&#160;pass&#160;username&#160;and&#160;password.</text><text
 class="terminal-3400494481-r2" x="793" y="239.6" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-9)">
-</text><text class="terminal-3400494481-r4" x="73.2" y="264" textLength="97.6" 
clip-path="url(#terminal-3400494481-line-10)">backfill</text><text 
class="terminal-3400494481-r2" x="244" y="264" textLength="329.4" 
clip-path="url(#terminal-3400494481-line-10)">Perform&#160;Backfill&#160;operations</text><text
 class="terminal-3400494481-r2" x="793" y="264" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-10)">
-</text><text class="terminal-3400494481-r4" x="73.2" y="288.4" 
textLength="73.2" 
clip-path="url(#terminal-3400494481-line-11)">config</text><text 
class="terminal-3400494481-r2" x="244" y="288.4" textLength="305" 
clip-path="url(#terminal-3400494481-line-11)">Perform&#160;Config&#160;operations</text><text
 class="terminal-3400494481-r2" x="793" y="288.4" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-11)">
-</text><text class="terminal-3400494481-r4" x="73.2" y="312.8" 
textLength="134.2" 
clip-path="url(#terminal-3400494481-line-12)">connections</text><text 
class="terminal-3400494481-r2" x="244" y="312.8" textLength="366" 
clip-path="url(#terminal-3400494481-line-12)">Perform&#160;Connections&#160;operations</text><text
 class="terminal-3400494481-r2" x="793" y="312.8" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-12)">
-</text><text class="terminal-3400494481-r4" x="73.2" y="337.2" 
textLength="73.2" 
clip-path="url(#terminal-3400494481-line-13)">dagrun</text><text 
class="terminal-3400494481-r2" x="244" y="337.2" textLength="305" 
clip-path="url(#terminal-3400494481-line-13)">Perform&#160;DagRun&#160;operations</text><text
 class="terminal-3400494481-r2" x="793" y="337.2" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-13)">
-</text><text class="terminal-3400494481-r4" x="73.2" y="361.6" 
textLength="48.8" 
clip-path="url(#terminal-3400494481-line-14)">dags</text><text 
class="terminal-3400494481-r2" x="244" y="361.6" textLength="280.6" 
clip-path="url(#terminal-3400494481-line-14)">Perform&#160;Dags&#160;operations</text><text
 class="terminal-3400494481-r2" x="793" y="361.6" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-14)">
-</text><text class="terminal-3400494481-r4" x="73.2" y="386" textLength="48.8" 
clip-path="url(#terminal-3400494481-line-15)">jobs</text><text 
class="terminal-3400494481-r2" x="244" y="386" textLength="280.6" 
clip-path="url(#terminal-3400494481-line-15)">Perform&#160;Jobs&#160;operations</text><text
 class="terminal-3400494481-r2" x="793" y="386" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-15)">
-</text><text class="terminal-3400494481-r4" x="73.2" y="410.4" textLength="61" 
clip-path="url(#terminal-3400494481-line-16)">pools</text><text 
class="terminal-3400494481-r2" x="244" y="410.4" textLength="292.8" 
clip-path="url(#terminal-3400494481-line-16)">Perform&#160;Pools&#160;operations</text><text
 class="terminal-3400494481-r2" x="793" y="410.4" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-16)">
-</text><text class="terminal-3400494481-r4" x="73.2" y="434.8" 
textLength="109.8" 
clip-path="url(#terminal-3400494481-line-17)">providers</text><text 
class="terminal-3400494481-r2" x="244" y="434.8" textLength="341.6" 
clip-path="url(#terminal-3400494481-line-17)">Perform&#160;Providers&#160;operations</text><text
 class="terminal-3400494481-r2" x="793" y="434.8" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-17)">
-</text><text class="terminal-3400494481-r4" x="73.2" y="459.2" 
textLength="109.8" 
clip-path="url(#terminal-3400494481-line-18)">variables</text><text 
class="terminal-3400494481-r2" x="244" y="459.2" textLength="341.6" 
clip-path="url(#terminal-3400494481-line-18)">Perform&#160;Variables&#160;operations</text><text
 class="terminal-3400494481-r2" x="793" y="459.2" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-18)">
-</text><text class="terminal-3400494481-r2" x="793" y="483.6" 
textLength="12.2" clip-path="url(#terminal-3400494481-line-19)">
-</text><text class="terminal-3400494481-r4" x="0" y="508" textLength="158.6" 
clip-path="url(#terminal-3400494481-line-20)">&#160;&#160;&#160;&#160;Commands:</text><text
 class="terminal-3400494481-r2" x="793" y="508" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-20)">
-</text><text class="terminal-3400494481-r4" x="73.2" y="532.4" 
textLength="85.4" 
clip-path="url(#terminal-3400494481-line-21)">version</text><text 
class="terminal-3400494481-r2" x="244" y="532.4" textLength="292.8" 
clip-path="url(#terminal-3400494481-line-21)">Show&#160;version&#160;information</text><text
 class="terminal-3400494481-r2" x="793" y="532.4" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-21)">
-</text><text class="terminal-3400494481-r2" x="793" y="556.8" 
textLength="12.2" clip-path="url(#terminal-3400494481-line-22)">
-</text><text class="terminal-3400494481-r1" x="0" y="581.2" textLength="97.6" 
clip-path="url(#terminal-3400494481-line-23)">Options:</text><text 
class="terminal-3400494481-r2" x="793" y="581.2" textLength="12.2" 
clip-path="url(#terminal-3400494481-line-23)">
-</text><text class="terminal-3400494481-r4" x="24.4" y="605.6" 
textLength="24.4" clip-path="url(#terminal-3400494481-line-24)">-h</text><text 
class="terminal-3400494481-r2" x="48.8" y="605.6" textLength="24.4" 
clip-path="url(#terminal-3400494481-line-24)">,&#160;</text><text 
class="terminal-3400494481-r4" x="73.2" y="605.6" textLength="73.2" 
clip-path="url(#terminal-3400494481-line-24)">--help</text><text 
class="terminal-3400494481-r2" x="244" y="605.6" textLength="378.2" 
clip-path="url( [...]
+    <g class="terminal-4108169915-matrix">
+    <text class="terminal-4108169915-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-4108169915-line-0)">Usage:</text><text 
class="terminal-4108169915-r3" x="85.4" y="20" textLength="122" 
clip-path="url(#terminal-4108169915-line-0)">airflowctl</text><text 
class="terminal-4108169915-r2" x="207.4" y="20" textLength="24.4" 
clip-path="url(#terminal-4108169915-line-0)">&#160;[</text><text 
class="terminal-4108169915-r4" x="231.8" y="20" textLength="24.4" 
clip-path="url(#terminal-41 [...]
+</text><text class="terminal-4108169915-r2" x="915" y="44.4" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-1)">
+</text><text class="terminal-4108169915-r1" x="0" y="68.8" textLength="256.2" 
clip-path="url(#terminal-4108169915-line-2)">Positional&#160;Arguments:</text><text
 class="terminal-4108169915-r2" x="915" y="68.8" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-2)">
+</text><text class="terminal-4108169915-r4" x="24.4" y="93.2" 
textLength="195.2" 
clip-path="url(#terminal-4108169915-line-3)">GROUP_OR_COMMAND</text><text 
class="terminal-4108169915-r2" x="915" y="93.2" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-3)">
+</text><text class="terminal-4108169915-r2" x="915" y="117.6" 
textLength="12.2" clip-path="url(#terminal-4108169915-line-4)">
+</text><text class="terminal-4108169915-r4" x="0" y="142" textLength="122" 
clip-path="url(#terminal-4108169915-line-5)">&#160;&#160;&#160;&#160;Groups</text><text
 class="terminal-4108169915-r2" x="915" y="142" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-5)">
+</text><text class="terminal-4108169915-r4" x="73.2" y="166.4" 
textLength="73.2" 
clip-path="url(#terminal-4108169915-line-6)">assets</text><text 
class="terminal-4108169915-r2" x="244" y="166.4" textLength="305" 
clip-path="url(#terminal-4108169915-line-6)">Perform&#160;Assets&#160;operations</text><text
 class="terminal-4108169915-r2" x="915" y="166.4" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-6)">
+</text><text class="terminal-4108169915-r4" x="73.2" y="190.8" 
textLength="48.8" clip-path="url(#terminal-4108169915-line-7)">auth</text><text 
class="terminal-4108169915-r2" x="244" y="190.8" textLength="646.6" 
clip-path="url(#terminal-4108169915-line-7)">Manage&#160;authentication&#160;for&#160;CLI.&#160;Either&#160;pass&#160;token&#160;from</text><text
 class="terminal-4108169915-r2" x="915" y="190.8" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-7)">
+</text><text class="terminal-4108169915-r2" x="244" y="215.2" 
textLength="622.2" 
clip-path="url(#terminal-4108169915-line-8)">environment&#160;variable/parameter&#160;or&#160;pass&#160;username&#160;and</text><text
 class="terminal-4108169915-r2" x="915" y="215.2" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-8)">
+</text><text class="terminal-4108169915-r2" x="244" y="239.6" 
textLength="109.8" 
clip-path="url(#terminal-4108169915-line-9)">password.</text><text 
class="terminal-4108169915-r2" x="915" y="239.6" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-9)">
+</text><text class="terminal-4108169915-r4" x="73.2" y="264" textLength="97.6" 
clip-path="url(#terminal-4108169915-line-10)">backfill</text><text 
class="terminal-4108169915-r2" x="244" y="264" textLength="329.4" 
clip-path="url(#terminal-4108169915-line-10)">Perform&#160;Backfill&#160;operations</text><text
 class="terminal-4108169915-r2" x="915" y="264" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-10)">
+</text><text class="terminal-4108169915-r4" x="73.2" y="288.4" 
textLength="73.2" 
clip-path="url(#terminal-4108169915-line-11)">config</text><text 
class="terminal-4108169915-r2" x="244" y="288.4" textLength="305" 
clip-path="url(#terminal-4108169915-line-11)">Perform&#160;Config&#160;operations</text><text
 class="terminal-4108169915-r2" x="915" y="288.4" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-11)">
+</text><text class="terminal-4108169915-r4" x="73.2" y="312.8" 
textLength="134.2" 
clip-path="url(#terminal-4108169915-line-12)">connections</text><text 
class="terminal-4108169915-r2" x="244" y="312.8" textLength="366" 
clip-path="url(#terminal-4108169915-line-12)">Perform&#160;Connections&#160;operations</text><text
 class="terminal-4108169915-r2" x="915" y="312.8" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-12)">
+</text><text class="terminal-4108169915-r4" x="73.2" y="337.2" 
textLength="73.2" 
clip-path="url(#terminal-4108169915-line-13)">dagrun</text><text 
class="terminal-4108169915-r2" x="244" y="337.2" textLength="305" 
clip-path="url(#terminal-4108169915-line-13)">Perform&#160;DagRun&#160;operations</text><text
 class="terminal-4108169915-r2" x="915" y="337.2" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-13)">
+</text><text class="terminal-4108169915-r4" x="73.2" y="361.6" 
textLength="48.8" 
clip-path="url(#terminal-4108169915-line-14)">dags</text><text 
class="terminal-4108169915-r2" x="244" y="361.6" textLength="280.6" 
clip-path="url(#terminal-4108169915-line-14)">Perform&#160;Dags&#160;operations</text><text
 class="terminal-4108169915-r2" x="915" y="361.6" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-14)">
+</text><text class="terminal-4108169915-r4" x="73.2" y="386" textLength="48.8" 
clip-path="url(#terminal-4108169915-line-15)">jobs</text><text 
class="terminal-4108169915-r2" x="244" y="386" textLength="280.6" 
clip-path="url(#terminal-4108169915-line-15)">Perform&#160;Jobs&#160;operations</text><text
 class="terminal-4108169915-r2" x="915" y="386" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-15)">
+</text><text class="terminal-4108169915-r4" x="73.2" y="410.4" textLength="61" 
clip-path="url(#terminal-4108169915-line-16)">pools</text><text 
class="terminal-4108169915-r2" x="244" y="410.4" textLength="292.8" 
clip-path="url(#terminal-4108169915-line-16)">Perform&#160;Pools&#160;operations</text><text
 class="terminal-4108169915-r2" x="915" y="410.4" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-16)">
+</text><text class="terminal-4108169915-r4" x="73.2" y="434.8" 
textLength="109.8" 
clip-path="url(#terminal-4108169915-line-17)">providers</text><text 
class="terminal-4108169915-r2" x="244" y="434.8" textLength="341.6" 
clip-path="url(#terminal-4108169915-line-17)">Perform&#160;Providers&#160;operations</text><text
 class="terminal-4108169915-r2" x="915" y="434.8" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-17)">
+</text><text class="terminal-4108169915-r4" x="73.2" y="459.2" 
textLength="109.8" 
clip-path="url(#terminal-4108169915-line-18)">variables</text><text 
class="terminal-4108169915-r2" x="244" y="459.2" textLength="341.6" 
clip-path="url(#terminal-4108169915-line-18)">Perform&#160;Variables&#160;operations</text><text
 class="terminal-4108169915-r2" x="915" y="459.2" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-18)">
+</text><text class="terminal-4108169915-r4" x="73.2" y="483.6" 
textLength="48.8" 
clip-path="url(#terminal-4108169915-line-19)">xcom</text><text 
class="terminal-4108169915-r2" x="244" y="483.6" textLength="280.6" 
clip-path="url(#terminal-4108169915-line-19)">Perform&#160;XCom&#160;operations</text><text
 class="terminal-4108169915-r2" x="915" y="483.6" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-19)">
+</text><text class="terminal-4108169915-r2" x="915" y="508" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-20)">
+</text><text class="terminal-4108169915-r4" x="0" y="532.4" textLength="158.6" 
clip-path="url(#terminal-4108169915-line-21)">&#160;&#160;&#160;&#160;Commands:</text><text
 class="terminal-4108169915-r2" x="915" y="532.4" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-21)">
+</text><text class="terminal-4108169915-r4" x="73.2" y="556.8" 
textLength="85.4" 
clip-path="url(#terminal-4108169915-line-22)">version</text><text 
class="terminal-4108169915-r2" x="244" y="556.8" textLength="292.8" 
clip-path="url(#terminal-4108169915-line-22)">Show&#160;version&#160;information</text><text
 class="terminal-4108169915-r2" x="915" y="556.8" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-22)">
+</text><text class="terminal-4108169915-r2" x="915" y="581.2" 
textLength="12.2" clip-path="url(#terminal-4108169915-line-23)">
+</text><text class="terminal-4108169915-r1" x="0" y="605.6" textLength="97.6" 
clip-path="url(#terminal-4108169915-line-24)">Options:</text><text 
class="terminal-4108169915-r2" x="915" y="605.6" textLength="12.2" 
clip-path="url(#terminal-4108169915-line-24)">
+</text><text class="terminal-4108169915-r4" x="24.4" y="630" textLength="24.4" 
clip-path="url(#terminal-4108169915-line-25)">-h</text><text 
class="terminal-4108169915-r2" x="48.8" y="630" textLength="24.4" 
clip-path="url(#terminal-4108169915-line-25)">,&#160;</text><text 
class="terminal-4108169915-r4" x="73.2" y="630" textLength="73.2" 
clip-path="url(#terminal-4108169915-line-25)">--help</text><text 
class="terminal-4108169915-r2" x="244" y="630" textLength="378.2" 
clip-path="url(#termina [...]
 </text>
     </g>
     </g>
diff --git a/airflow-ctl/src/airflowctl/api/client.py 
b/airflow-ctl/src/airflowctl/api/client.py
index 9e138ffc87b..5a719cac164 100644
--- a/airflow-ctl/src/airflowctl/api/client.py
+++ b/airflow-ctl/src/airflowctl/api/client.py
@@ -48,6 +48,7 @@ from airflowctl.api.operations import (
     ServerResponseError,
     VariablesOperations,
     VersionOperations,
+    XComOperations,
 )
 from airflowctl.exceptions import (
     AirflowCtlCredentialNotFoundException,
@@ -301,6 +302,12 @@ class Client(httpx.Client):
         """Get the version of the server."""
         return VersionOperations(self)
 
+    @lru_cache()  # type: ignore[prop-decorator]
+    @property
+    def xcom(self):
+        """Operations related to XComs."""
+        return XComOperations(self)
+
 
 # API Client Decorator for CLI Actions
 @contextlib.contextmanager
diff --git a/airflow-ctl/src/airflowctl/api/operations.py 
b/airflow-ctl/src/airflowctl/api/operations.py
index 31e0298a19d..588d85569e3 100644
--- a/airflow-ctl/src/airflowctl/api/operations.py
+++ b/airflow-ctl/src/airflowctl/api/operations.py
@@ -18,6 +18,7 @@
 from __future__ import annotations
 
 import datetime
+import json
 from typing import TYPE_CHECKING, Any, TypeVar
 
 import httpx
@@ -70,6 +71,10 @@ from airflowctl.api.datamodels.generated import (
     VariableCollectionResponse,
     VariableResponse,
     VersionInfo,
+    XComCollectionResponse,
+    XComCreateBody,
+    XComResponseNative,
+    XComUpdateBody,
 )
 from airflowctl.exceptions import AirflowCtlConnectionException
 
@@ -697,3 +702,125 @@ class VersionOperations(BaseOperations):
             return VersionInfo.model_validate_json(self.response.content)
         except ServerResponseError as e:
             raise e
+
+
+class XComOperations(BaseOperations):
+    """XCom operations."""
+
+    def get(
+        self,
+        dag_id: str,
+        dag_run_id: str,
+        task_id: str,
+        key: str,
+        map_index: int = None,  # type: ignore
+    ) -> XComResponseNative | ServerResponseError:
+        """Get an XCom entry."""
+        try:
+            params: dict[str, Any] = {}
+            if map_index is not None:
+                params["map_index"] = map_index
+            self.response = self.client.get(
+                
f"dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries/{key}",
+                params=params,
+            )
+            return 
XComResponseNative.model_validate_json(self.response.content)
+        except ServerResponseError as e:
+            raise e
+
+    def list(
+        self,
+        dag_id: str,
+        dag_run_id: str,
+        task_id: str,
+        map_index: int = None,  # type: ignore
+        key: str = None,  # type: ignore
+    ) -> XComCollectionResponse | ServerResponseError:
+        """List XCom entries."""
+        params: dict[str, Any] = {}
+        if map_index is not None:
+            params["map_index"] = map_index
+        if key is not None:
+            params["xcom_key"] = key
+        return super().execute_list(
+            
path=f"dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries",
+            data_model=XComCollectionResponse,
+            params=params,
+        )
+
+    def add(
+        self,
+        dag_id: str,
+        dag_run_id: str,
+        task_id: str,
+        key: str,
+        value: str,
+        map_index: int = None,  # type: ignore
+    ) -> XComResponseNative | ServerResponseError:
+        """Add an XCom entry."""
+        try:
+            parsed_value = json.loads(value)
+        except (ValueError, TypeError):
+            parsed_value = value
+
+        body_dict: dict[str, Any] = {"key": key, "value": parsed_value}
+        if map_index is not None:
+            body_dict["map_index"] = map_index
+        body = XComCreateBody(**body_dict)
+        try:
+            self.response = self.client.post(
+                
f"dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries",
+                json=body.model_dump(mode="json", exclude_unset=True),
+            )
+            return 
XComResponseNative.model_validate_json(self.response.content)
+        except ServerResponseError as e:
+            raise e
+
+    def edit(
+        self,
+        dag_id: str,
+        dag_run_id: str,
+        task_id: str,
+        key: str,
+        value: str,
+        map_index: int = None,  # type: ignore
+    ) -> XComResponseNative | ServerResponseError:
+        """Edit an XCom entry."""
+        try:
+            parsed_value = json.loads(value)
+        except (ValueError, TypeError):
+            parsed_value = value
+
+        body_dict: dict[str, Any] = {"value": parsed_value}
+        if map_index is not None:
+            body_dict["map_index"] = map_index
+        body = XComUpdateBody(**body_dict)
+        try:
+            self.response = self.client.patch(
+                
f"dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries/{key}",
+                json=body.model_dump(mode="json", exclude_unset=True),
+            )
+            return 
XComResponseNative.model_validate_json(self.response.content)
+        except ServerResponseError as e:
+            raise e
+
+    def delete(
+        self,
+        dag_id: str,
+        dag_run_id: str,
+        task_id: str,
+        key: str,
+        map_index: int = None,  # type: ignore
+    ) -> str | ServerResponseError:
+        """Delete an XCom entry."""
+        try:
+            params: dict[str, Any] = {}
+            if map_index is not None:
+                params["map_index"] = map_index
+            self.client.delete(
+                
f"dags/{dag_id}/dagRuns/{dag_run_id}/taskInstances/{task_id}/xcomEntries/{key}",
+                params=params,
+            )
+            return key
+        except ServerResponseError as e:
+            raise e
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py 
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index 6c455693b49..dad1cdd14ff 100644
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -381,7 +381,7 @@ class CommandFactory:
         # Exclude parameters that are not needed for CLI from datamodels
         self.excluded_parameters = ["schema_"]
         # This list is used to determine if the command/operation needs to 
output data
-        self.output_command_list = ["list", "get", "create", "delete", 
"update", "trigger"]
+        self.output_command_list = ["list", "get", "create", "delete", 
"update", "trigger", "add", "edit"]
         self.exclude_operation_names = ["LoginOperations", 
"VersionOperations", "BaseOperations"]
         self.exclude_method_names = [
             "error",
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py 
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index f0a638475c4..4e8c0ab7590 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -92,6 +92,9 @@ from airflowctl.api.datamodels.generated import (
     VariableCollectionResponse,
     VariableResponse,
     VersionInfo,
+    XComCollectionResponse,
+    XComResponse,
+    XComResponseNative,
 )
 from airflowctl.api.operations import BaseOperations
 from airflowctl.exceptions import AirflowCtlConnectionException
@@ -1265,3 +1268,337 @@ class TestAuthOperations:
             )
         )
         assert response.access_token == "NO_TOKEN"
+
+
+class TestXComOperations:
+    """Test suite for XCom operations."""
+
+    dag_id: str = "test_dag"
+    dag_run_id: str = "manual__2025-01-24T00:00:00+00:00"
+    task_id: str = "test_task"
+    key: str = "test_key"
+    map_index: int = 0
+
+    xcom_response_native = XComResponseNative(
+        key=key,
+        timestamp=datetime.datetime(2025, 1, 24, 0, 0, 0),
+        logical_date=datetime.datetime(2025, 1, 24, 0, 0, 0),
+        map_index=-1,
+        task_id=task_id,
+        dag_id=dag_id,
+        run_id=dag_run_id,
+        dag_display_name=dag_id,
+        task_display_name=task_id,
+        value={"result": "success"},
+    )
+
+    xcom_response = XComResponse(
+        key=key,
+        timestamp=datetime.datetime(2025, 1, 24, 0, 0, 0),
+        logical_date=datetime.datetime(2025, 1, 24, 0, 0, 0),
+        map_index=-1,
+        task_id=task_id,
+        dag_id=dag_id,
+        run_id=dag_run_id,
+        dag_display_name=dag_id,
+        task_display_name=task_id,
+    )
+
+    xcom_collection_response = XComCollectionResponse(
+        xcom_entries=[xcom_response],
+        total_entries=1,
+    )
+
+    def test_get(self):
+        """Test fetching a single XCom entry without map_index."""
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == (
+                f"/api/v2/dags/{self.dag_id}/dagRuns/{self.dag_run_id}/"
+                f"taskInstances/{self.task_id}/xcomEntries/{self.key}"
+            )
+            # Verify map_index is not in query params when not provided
+            assert "map_index" not in str(request.url.query)
+            return httpx.Response(200, 
json=json.loads(self.xcom_response_native.model_dump_json()))
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.xcom.get(
+            dag_id=self.dag_id,
+            dag_run_id=self.dag_run_id,
+            task_id=self.task_id,
+            key=self.key,
+        )
+        assert response == self.xcom_response_native
+
+    def test_get_with_map_index(self):
+        """Test fetching XCom entry for a mapped task with map_index."""
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == (
+                f"/api/v2/dags/{self.dag_id}/dagRuns/{self.dag_run_id}/"
+                f"taskInstances/{self.task_id}/xcomEntries/{self.key}"
+            )
+            # Verify map_index is included in query params
+            assert f"map_index={self.map_index}" in str(request.url.query)
+            return httpx.Response(200, 
json=json.loads(self.xcom_response_native.model_dump_json()))
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.xcom.get(
+            dag_id=self.dag_id,
+            dag_run_id=self.dag_run_id,
+            task_id=self.task_id,
+            key=self.key,
+            map_index=self.map_index,
+        )
+        assert response == self.xcom_response_native
+
+    def test_list(self):
+        """Test listing all XCom entries for a task instance without 
filters."""
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == (
+                f"/api/v2/dags/{self.dag_id}/dagRuns/{self.dag_run_id}/"
+                f"taskInstances/{self.task_id}/xcomEntries"
+            )
+            # Verify no filters in query params
+            assert "map_index" not in str(request.url.query)
+            assert "xcom_key" not in str(request.url.query)
+            return httpx.Response(200, 
json=json.loads(self.xcom_collection_response.model_dump_json()))
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.xcom.list(
+            dag_id=self.dag_id,
+            dag_run_id=self.dag_run_id,
+            task_id=self.task_id,
+        )
+        assert response == self.xcom_collection_response
+
+    def test_list_with_map_index_filter(self):
+        """Test listing XCom entries filtered by map_index."""
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == (
+                f"/api/v2/dags/{self.dag_id}/dagRuns/{self.dag_run_id}/"
+                f"taskInstances/{self.task_id}/xcomEntries"
+            )
+            # Verify map_index filter is included
+            assert f"map_index={self.map_index}" in str(request.url.query)
+            return httpx.Response(200, 
json=json.loads(self.xcom_collection_response.model_dump_json()))
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.xcom.list(
+            dag_id=self.dag_id,
+            dag_run_id=self.dag_run_id,
+            task_id=self.task_id,
+            map_index=self.map_index,
+        )
+        assert response == self.xcom_collection_response
+
+    def test_list_with_key_filter(self):
+        """Test listing XCom entries filtered by key."""
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == (
+                f"/api/v2/dags/{self.dag_id}/dagRuns/{self.dag_run_id}/"
+                f"taskInstances/{self.task_id}/xcomEntries"
+            )
+            # Verify xcom_key filter is included
+            assert f"xcom_key={self.key}" in str(request.url.query)
+            return httpx.Response(200, 
json=json.loads(self.xcom_collection_response.model_dump_json()))
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.xcom.list(
+            dag_id=self.dag_id,
+            dag_run_id=self.dag_run_id,
+            task_id=self.task_id,
+            key=self.key,
+        )
+        assert response == self.xcom_collection_response
+
+    def test_list_with_both_filters(self):
+        """Test listing XCom entries with both map_index and key filters."""
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == (
+                f"/api/v2/dags/{self.dag_id}/dagRuns/{self.dag_run_id}/"
+                f"taskInstances/{self.task_id}/xcomEntries"
+            )
+            # Verify both filters are included
+            assert f"map_index={self.map_index}" in str(request.url.query)
+            assert f"xcom_key={self.key}" in str(request.url.query)
+            return httpx.Response(200, 
json=json.loads(self.xcom_collection_response.model_dump_json()))
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.xcom.list(
+            dag_id=self.dag_id,
+            dag_run_id=self.dag_run_id,
+            task_id=self.task_id,
+            map_index=self.map_index,
+            key=self.key,
+        )
+        assert response == self.xcom_collection_response
+
+    def test_add_with_json_value(self):
+        """Test adding a new XCom entry with JSON value."""
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == (
+                f"/api/v2/dags/{self.dag_id}/dagRuns/{self.dag_run_id}/"
+                f"taskInstances/{self.task_id}/xcomEntries"
+            )
+            # Verify request body
+            request_body = json.loads(request.content)
+            assert request_body["key"] == self.key
+            assert request_body["value"] == {"result": "success"}
+            # Verify map_index is NOT in body when not provided
+            assert "map_index" not in request_body
+            return httpx.Response(200, 
json=json.loads(self.xcom_response_native.model_dump_json()))
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.xcom.add(
+            dag_id=self.dag_id,
+            dag_run_id=self.dag_run_id,
+            task_id=self.task_id,
+            key=self.key,
+            value='{"result": "success"}',
+        )
+        assert response == self.xcom_response_native
+
+    def test_add_with_string_value(self):
+        """Test adding XCom entry with non-JSON string value."""
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == (
+                f"/api/v2/dags/{self.dag_id}/dagRuns/{self.dag_run_id}/"
+                f"taskInstances/{self.task_id}/xcomEntries"
+            )
+            # Verify plain string is stored as-is
+            request_body = json.loads(request.content)
+            assert request_body["value"] == "plain string value"
+            return httpx.Response(200, 
json=json.loads(self.xcom_response_native.model_dump_json()))
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.xcom.add(
+            dag_id=self.dag_id,
+            dag_run_id=self.dag_run_id,
+            task_id=self.task_id,
+            key=self.key,
+            value="plain string value",
+        )
+        assert response == self.xcom_response_native
+
+    def test_add_with_map_index(self):
+        """Test adding XCom entry for a mapped task with map_index."""
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == (
+                f"/api/v2/dags/{self.dag_id}/dagRuns/{self.dag_run_id}/"
+                f"taskInstances/{self.task_id}/xcomEntries"
+            )
+            # Verify map_index is included in request body
+            request_body = json.loads(request.content)
+            assert request_body["map_index"] == self.map_index
+            return httpx.Response(200, 
json=json.loads(self.xcom_response_native.model_dump_json()))
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.xcom.add(
+            dag_id=self.dag_id,
+            dag_run_id=self.dag_run_id,
+            task_id=self.task_id,
+            key=self.key,
+            value='{"result": "success"}',
+            map_index=self.map_index,
+        )
+        assert response == self.xcom_response_native
+
+    def test_edit_with_json_value(self):
+        """Test editing an existing XCom entry with JSON value."""
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == (
+                f"/api/v2/dags/{self.dag_id}/dagRuns/{self.dag_run_id}/"
+                f"taskInstances/{self.task_id}/xcomEntries/{self.key}"
+            )
+            # Verify request body
+            request_body = json.loads(request.content)
+            assert request_body["value"] == {"updated": "value"}
+            # Verify map_index is NOT in body when not provided
+            assert "map_index" not in request_body
+            return httpx.Response(200, 
json=json.loads(self.xcom_response_native.model_dump_json()))
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.xcom.edit(
+            dag_id=self.dag_id,
+            dag_run_id=self.dag_run_id,
+            task_id=self.task_id,
+            key=self.key,
+            value='{"updated": "value"}',
+        )
+        assert response == self.xcom_response_native
+
+    def test_edit_with_map_index(self):
+        """Test editing XCom entry for a mapped task with map_index."""
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == (
+                f"/api/v2/dags/{self.dag_id}/dagRuns/{self.dag_run_id}/"
+                f"taskInstances/{self.task_id}/xcomEntries/{self.key}"
+            )
+            # Verify map_index is included in request body
+            request_body = json.loads(request.content)
+            assert request_body["map_index"] == self.map_index
+            return httpx.Response(200, 
json=json.loads(self.xcom_response_native.model_dump_json()))
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.xcom.edit(
+            dag_id=self.dag_id,
+            dag_run_id=self.dag_run_id,
+            task_id=self.task_id,
+            key=self.key,
+            value='{"updated": "value"}',
+            map_index=self.map_index,
+        )
+        assert response == self.xcom_response_native
+
+    def test_delete(self):
+        """Test deleting an XCom entry without map_index."""
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == (
+                f"/api/v2/dags/{self.dag_id}/dagRuns/{self.dag_run_id}/"
+                f"taskInstances/{self.task_id}/xcomEntries/{self.key}"
+            )
+            # Verify map_index is NOT in query params when not provided
+            assert "map_index" not in str(request.url.query)
+            return httpx.Response(204)
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.xcom.delete(
+            dag_id=self.dag_id,
+            dag_run_id=self.dag_run_id,
+            task_id=self.task_id,
+            key=self.key,
+        )
+        assert response == self.key
+
+    def test_delete_with_map_index(self):
+        """Test deleting XCom entry for a mapped task with map_index."""
+
+        def handle_request(request: httpx.Request) -> httpx.Response:
+            assert request.url.path == (
+                f"/api/v2/dags/{self.dag_id}/dagRuns/{self.dag_run_id}/"
+                f"taskInstances/{self.task_id}/xcomEntries/{self.key}"
+            )
+            # Verify map_index is included in query params
+            assert f"map_index={self.map_index}" in str(request.url.query)
+            return httpx.Response(204)
+
+        client = make_api_client(transport=httpx.MockTransport(handle_request))
+        response = client.xcom.delete(
+            dag_id=self.dag_id,
+            dag_run_id=self.dag_run_id,
+            task_id=self.task_id,
+            key=self.key,
+            map_index=self.map_index,
+        )
+        assert response == self.key

Reply via email to