Airflow offers three mechanisms for expressing "run this after that". They look interchangeable in documentation; they are not interchangeable in production.

MechanismScopeTrigger
Direct (A >> B)Intra-DAGDAG schedule
AssetCross-DAG, within AirflowProducer DAG writes the Asset
AssetWatcherCross-system, outside AirflowExternal event (SQS, Kafka, S3, UC table)

Pick the lightest mechanism that expresses what you actually need.

Direct dependencies

The classic Python DAG definition with the >> operator:

with DAG(...) as dag:
    extract = ...
    transform = ...
    load = ...
    extract >> transform >> load

Use when:

Do not use for:

Asset dependencies

Event-driven, cross-DAG. DAG B schedules on an Asset. DAG A's final task updates that Asset. When A writes, B triggers.

# Producer (DAG A)
from airflow.assets import Asset

orders_gold = Asset("s3://data-lake-prod/gold/fct_orders/_delta_log/")

with DAG(dag_id="refresh_gold_orders", schedule="@hourly") as dag_a:
    transform = ...
    write = ... >> orders_gold   # writing updates the Asset

# Consumer (DAG B)
with DAG(dag_id="refresh_dashboard", schedule=[orders_gold]) as dag_b:
    refresh_powerbi = ...

Use when:

This is the canonical pattern for:

Note

Assets replaced the old ExternalTaskSensor pattern, which held a worker slot while it polled an upstream DAG. ExternalTaskSensor cost executor capacity and coupled teams to the same scheduling interval. Assets cost nothing to wait on; the scheduler triggers DAG B only when the Asset actually updates.

AssetWatcher dependencies

Event-driven, from outside Airflow. DAG B schedules on an AssetWatcher that listens to an external event stream (SQS, Kafka, S3 events, Unity Catalog table updates).

from airflow.assets import AssetWatcher
from airflow.providers.amazon.aws.assets import S3AssetWatcher

new_files = AssetWatcher(
    name="new_orders_in_landing",
    source=S3AssetWatcher(
        bucket_name="causeway-landing",
        key_prefix="orders/",
    ),
)

with DAG(dag_id="process_new_orders", schedule=new_files) as dag:
    process = ...

Use when:

This replaces the old poke_interval=30 sensor pattern. Watchers subscribe to real events; they do not burn worker slots polling.

Decision framework

Walk these questions in order:

  1. Are A and B in the same logical pipeline, same cadence, same ownership? → direct dependency.
  2. Is the producer another Airflow DAG? → Asset.
  3. Is the producer outside Airflow entirely? → AssetWatcher.

Reach for TriggerDagRunOperator only when none of the above fits. It re-introduces the tight coupling through the back door.

The anti-pattern: sensors

The old PokeUntilTrue sensor pattern is the most common bug in Airflow code in the wild. It held a worker slot the entire time it waited. At scale, sensors starved real work.

In 2026:

Danger

A synchronous sensor with no timeout is a production-incident-in-waiting. It holds a worker forever, starves other tasks, and produces no useful log until it eventually times out or someone kills the DAG. Deferrable sensors are not optional; they are the baseline. Older tutorials show the synchronous pattern; ignore them.

The TaskGroup distinction

TaskGroups look like a dependency mechanism but are not. A TaskGroup is a visual grouping of tasks inside one DAG; dependencies between groups are still direct >> dependencies under the hood.

with TaskGroup("ingest") as ingest:
    extract = ...
    validate = ...

with TaskGroup("transform") as transform:
    enrich = ...
    dedupe = ...

ingest >> transform

Use TaskGroups for readability on DAGs with more than ~10 tasks. They are not a cross-DAG mechanism; for that you want Assets.

Assets vs. cross-DAG TriggerDagRunOperator

A common pattern in pre-Airflow-2 code:

# The old way; still legal but usually wrong
trigger_downstream = TriggerDagRunOperator(
    task_id="trigger_dashboard",
    trigger_dag_id="refresh_dashboard",
)

Problems:

Assets invert the dependency:

Use Assets over TriggerDagRunOperator in every new design. Keep TriggerDagRunOperator for legacy compatibility only.

Summary

See also