What You’ll Master Here
A branch marks the paths not taken as skipped. Skipped is contagious under the default rule, so the fix belongs on the task where the paths reconverge.
Branching is where a DAG stops being a fixed sequence and starts making decisions. Every task still exists in the graph; only some of them run. That sounds simple, and it produces the single most common “my pipeline silently did nothing” bug in Airflow.
The cause is that a skipped task is not a neutral state — it propagates. Set the branch and the join’s trigger rule below and watch the pipeline work or quietly stop.
@task.branch
def choose_route(rows: int) -> str:
return "full_reload" if rows > 10_000 else "incremental"
@task(trigger_rule="all_success")
def publish() -> None:
...
route = choose_route(count())
route >> [full_reload(), incremental()] >> publish()A branch marks the paths not taken as skipped. Skipped is contagious under the default rule, so the fix belongs on the task where the paths reconverge.
A branch with the default trigger rule downstream produces a green run that published nothing. No task failed, so no alert fired, and the Grid view is a wall of pale skipped squares that look like they were meant to be there. This is the failure this whole chapter exists to prevent.
- @task.branch
- Returns the task_id or ids to follow. Every other downstream path is skipped. Returning None skips all of them.
- trigger_rule
- The condition a task requires of its upstream tasks before it will run. Defaults to all_success.
- Skip propagation
- Skipped tasks cascade through the all_success and all_failed rules, causing those tasks to skip as well.
Adding a branch and leaving the join task on the default trigger rule. The unchosen path skips, the skip cascades into the join, and everything below it skips too. The run finishes green having produced nothing, and there is nothing to alert on because no task failed.
Every time you add a branch, immediately set the trigger rule on the task where the paths rejoin.
Look for pale squares in the Grid view after a branch. Skipped is a state worth reading, not scenery.
Prefer none_failed_min_one_success on a join — it tolerates the skipped sibling but not a wholly empty branch.
It is a state that spreads. Under the default rule a single skip near the top of a DAG can skip everything below it, and the run still reports success.
Branching is easy; rejoining is the part that needs thought. The trigger rule on the join is the whole difference.
