Data Engineering  /  Airflow

๐ŸŒฌ๏ธ Apache Airflow Guide 3 of 4 18 guides ยท updated 2026

Workflow orchestration with Airflow โ€” DAGs, operators, scheduling, executors, and the production practices that keep pipelines reliable.

Task Dependencies & the Bitshift Operators in Airflow

The >> and << operators you see in nearly every Airflow DAG arenโ€™t special DAG syntax โ€” theyโ€™re Pythonโ€™s actual bitshift operators, overloaded by Airflowโ€™s BaseOperator class to mean โ€œset as downstreamโ€ and โ€œset as upstreamโ€ respectively. Understanding that theyโ€™re just syntactic sugar over set_upstream()/set_downstream() method calls demystifies a lot of dependency-graph code that otherwise looks like magic.


The Two Equivalent Forms

# Bitshift form (idiomatic, most common)
extract >> transform >> load
# Explicit method form (identical result)
extract.set_downstream(transform)
transform.set_downstream(load)

Both produce the exact same dependency graph. The bitshift form won out as the convention because it reads left-to-right the same way the data flows.


Fan-Out: One Task, Multiple Downstream

extract >> [validate_schema, validate_row_counts, validate_nulls]

All three validation tasks become downstream of extract and run in parallel (as parallel as your executorโ€™s available worker capacity allows) once extract succeeds โ€” none of them depend on each other.

Fan-In: Multiple Tasks, One Downstream

[validate_schema, validate_row_counts, validate_nulls] >> load

load wonโ€™t start until all three validation tasks have succeeded โ€” by default, a task only runs once every one of its upstream dependencies has completed successfully (this default is controlled by the taskโ€™s trigger_rule, covered below).

Combining Both

extract >> [validate_schema, validate_row_counts, validate_nulls] >> load >> notify

This chains a fan-out into a fan-in into a straight line โ€” a genuinely common real-world shape: one extraction, several independent checks running in parallel, a join point, then a final notification step.


Cross Dependencies with cross_downstream

Sometimes every task in one group needs to depend on every task in another group โ€” not a single fan-in/fan-out, but a full cross product:

from airflow.models.baseoperator import cross_downstream
cross_downstream([extract_a, extract_b], [transform_a, transform_b])
# equivalent to: extract_a >> transform_a, extract_a >> transform_b,
# extract_b >> transform_a, extract_b >> transform_b

Writing this manually with bitshift operators for anything beyond a 2x2 grid gets unwieldy fast โ€” cross_downstream (and its sibling chain, for building long linear or partially-parallel sequences programmatically) exist specifically to avoid that.


Trigger Rules: Changing โ€œAll Must Succeedโ€

By default, a taskโ€™s trigger_rule is all_success โ€” every upstream task must succeed for it to run. This can be overridden:

from airflow.operators.python import PythonOperator
cleanup = PythonOperator(
task_id="cleanup",
python_callable=do_cleanup,
trigger_rule="all_done", # runs regardless of whether upstream tasks succeeded or failed
)

Common trigger rules: all_success (default), all_failed, all_done (runs once upstream tasks finish, regardless of outcome โ€” the standard choice for cleanup/notification tasks that should run โ€œno matter whatโ€), one_failed (useful for alerting tasks), and none_failed.


Common Mistakes

Assuming fan-out tasks run truly simultaneously. Theyโ€™re only eligible to run in parallel โ€” actual concurrency is bounded by executor worker capacity and DAG/task concurrency limits (max_active_tasks, pool slots). With one worker, โ€œparallelโ€ tasks still run one at a time.

Forgetting to set trigger_rule="all_done" on cleanup/notification tasks, which then silently never run at all if any upstream task in the DAG fails โ€” exactly the scenario where youโ€™d most want the notification to fire.

Building deeply nested dependency logic with manual >> chains instead of chain()/cross_downstream() when the pattern is regular โ€” it becomes unreadable and error-prone past a handful of tasks.

Frequently Asked Questions

Can a task depend on a task in a different DAG? Yes, via ExternalTaskSensor, which polls another DAGโ€™s task for completion โ€” this is a cross-DAG dependency, not a same-DAG bitshift dependency.

What happens if I create a circular dependency? Airflow will raise a AirflowDagCycleException at DAG parse time โ€” DAGs are, by definition, acyclic, and Airflow enforces this rather than allowing it to fail silently at runtime.

Does dependency order in the file matter? No โ€” what matters is the >>/<< relationships you declare, not the order task objects are defined in the Python file; Airflow builds the graph from the relationships, not from source-code position.

Summary

>> and << are just readable syntax over set_downstream()/set_upstream(), and combined with list syntax for fan-out/fan-in and helpers like chain()/cross_downstream() for regular patterns, theyโ€™re expressive enough to represent almost any real pipeline shape. trigger_rule is the other half of the picture โ€” itโ€™s what determines whether โ€œall upstream must succeedโ€ actually holds for a given task, which matters most for cleanup and alerting tasks that need to run regardless of what happened upstream.