Tasks can run on different machines, so they cannot share Python variables. XCom (“cross communication”) stores a small value from one task in the Airflow database, where other tasks of the same run can read it. With TaskFlow you only return the value and pass it on as an argument.
Copy and paste
TaskFlow: return and pass as an argument
from airflow.sdk import task
@task
def extract() -> dict:
return {"rows": 120, "path": "/data/sales_2026-05-15.csv"}
@task
def load(info: dict):
print(f"Loading {info['rows']} rows from {info['path']}")
load(extract())
Pass one key on its own:
@task(multiple_outputs=True)
def stats() -> dict:
return {"rows": 120, "path": "/data/sales_2026-05-15.csv"}
@task
def check_rows(rows: int):
...
check_rows(stats()["rows"])
In a template:
from airflow.providers.standard.operators.bash import BashOperator
report = BashOperator(
task_id="report",
bash_command="echo rows={{ ti.xcom_pull(task_ids='extract')['rows'] }}",
)
In Python, from any task:
@task
def check(ti=None):
info = ti.xcom_pull(task_ids="extract")
From a BashOperator. The last line the command prints becomes its XCom:
count = BashOperator(task_id="count_lines", bash_command="wc -l < /data/sales.csv")
# later: {{ ti.xcom_pull(task_ids='count_lines') }}
Dependencies
Passing a TaskFlow result as an argument, as in load(extract()), also sets the order: load waits
for extract. Pulling a value with xcom_pull does not. Set the order yourself, or the task may run
before the value exists:
extract_task = extract()
extract_task >> report
Things to know
xcom_pull()needstask_ids. In Airflow 3,ti.xcom_pull()withouttask_idsreads only the current task’s own XCom, which usually returnsNone.- Keep values small. XComs live in the Airflow metadata database. Pass IDs, paths, counts and dates. For data, write a file or a table and pass its location.
- Use JSON-like values: dicts, lists, strings, numbers, booleans and
None. Airflow 3 no longer pickles XComs, so arbitrary Python objects cannot be passed. - BashOperator values are strings. The
wc -lexample returns'42', not42. Convert it before doing arithmetic. - Turn it off when not needed.
do_xcom_push=Falsestops a classic operator from storing its result.
Tested with Airflow 3.3.2.