Skip to content

Sensor task is not skipped when used after BranchSQLOperator in Airflow 3.x #52219

Closed

Description

@kandharvishnu

Apache Airflow version

3.0.2

If "Other Airflow 2 version" selected, which one?

No response

What happened?

In Airflow 3.x, when a SqlSensor is used as one of the downstream tasks of a BranchSQLOperator, it is not being skipped as expected based on the branch condition. Even when the sensor task is not selected by the branching logic, it still runs instead of being skipped.

This behavior breaks the expected semantics of branching, where only one downstream path should execute and others should be marked as skipped.

Airflow 2.X Log and Graph view

[2025-06-25, 05:14:37 UTC] {sql.py:1223} INFO - Query returns 0, type '<class 'int'>'
[2025-06-25, 05:14:37 UTC] {skipmixin.py:233} INFO - Following branch ('task_2',)
[2025-06-25, 05:14:37 UTC] {skipmixin.py:281} INFO - Skipping tasks [('task_1', -1)]

Image

Airflow 3.X Log and Graph view

[2025-06-25, 10:36:45] INFO - Query returns 0, type '<class 'int'>': source="airflow.task.operators.airflow.providers.common.sql.operators.sql.BranchSQLOperator"
[2025-06-25, 10:36:45] INFO - Following branch {'task_2'}: source="airflow.task.operators.airflow.providers.common.sql.operators.sql.BranchSQLOperator"
[2025-06-25, 10:36:45] INFO - Skipping tasks []: source="airflow.task.operators.airflow.providers.common.sql.operators.sql.BranchSQLOperator"

Image

If we add an EmptyOperator between BranchSQLOperator and Sensor task, the EmptyOperator and sensor tasks are skipped.

What you think should happen instead?

Only the task(s) that are selected by the BranchSQLOperator should run. All other downstream tasks, including sensors, should be skipped automatically. This was the observed and expected behavior in Airflow 2.x.

How to reproduce

Use the following minimal DAG code:

from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.providers.common.sql.operators.sql import BranchSQLOperator
from airflow.providers.common.sql.sensors.sql import SqlSensor
from datetime import datetime

CONN_ID = "postgres_conn"

def task_2_func():
    print("Branch: TASK - 2 – Executing this task.")

with DAG(
    dag_id="example_branch_sql_with_python",
    start_date=datetime(2024, 1, 1),
    schedule=None,
    catchup=False,
    default_args={"owner": "airflow"},
) as dag:

    branch_task = BranchSQLOperator(
        task_id="branch_sql",
        sql="SELECT CASE WHEN 1=1 THEN 0 ELSE 1 END",
        conn_id=CONN_ID,
        follow_task_ids_if_true=["task_1"],
        follow_task_ids_if_false=["task_2"],
    )

    task_1 = SqlSensor(
        task_id="task_1",
        sql="SELECT CASE WHEN 1=1 THEN 1 ELSE 0 END",
        conn_id=CONN_ID,
    )

    task_2 = PythonOperator(
        task_id="task_2",
        python_callable=task_2_func,
    )

    branch_task >> [task_1, task_2]

In this example, task_1 should be skipped since the SQL in BranchSQLOperator returns 0, selecting task_2. But in Airflow 3.x, both tasks are executed.

Operating System

Debian

Versions of Apache Airflow Providers

No response

Deployment

Astronomer

Deployment details

No response

Anything else?

No response

Are you willing to submit PR?

  • Yes I am willing to submit a PR!

Code of Conduct

Activity

  1. fweilun commented on Jun 30, 2025

    @fweilun
    Contributor

    I found that Skipping tasks [] is empty because the line below in SkipMixin::skip_all_except filters out all operators except MappedOperator and BaseOperator:
    downstream_tasks = _ensure_tasks(task.downstream_list)
    However, BaseSensorOperator from airflow.sdk.bases.sensor.BaseSensorOperator is not recognized as a BaseOperator due to differing module origins, even though it nominally inherits from BaseOperator. As a result, tasks like SqlSensor (which subclass BaseSensorOperator) are excluded from downstream_tasks, and Skipping tasks [], which is derived from downstream_tasks, ends up empty.

    Below is the log. task_1 is filtered out in line 11-12.
    Image

    I temporarily added BaseSensorOperator to _ensure_tasks() as a workaround, and it resolved the issue:

    Image

    adjusted code:

    def _ensure_tasks(nodes: Iterable[DAGNode]) -> Sequence[Operator]:
        from airflow.models.baseoperator import BaseOperator
        from airflow.models.mappedoperator import MappedOperator
        from airflow.sensors.base import BaseSensorOperator
        return [n for n in nodes if isinstance(n, (BaseOperator, MappedOperator, BaseSensorOperator))]
  2. added this to the Airflow 3.0.4 milestone on Jul 17, 2025
  3. kaxil commented on Jul 17, 2025

    @kaxil
    Member

    On it, will be fixed asap

  4. added a commit that references this issue on Jul 17, 2025
    ef2fdd6
  5. kaxil commented on Jul 17, 2025

    @kaxil
    Member
  6. added a commit that references this issue on Jul 17, 2025
    2bc6a9a
  7. removed this from the Airflow 3.0.4 milestone on Jul 17, 2025
  8. added 3 commits that reference this issue on Jul 17, 2025
    d5aa29f
    5aaf154
    07e3fa1
  9. added a commit that references this issue on Jul 17, 2025
    fc5410c
  10. added a commit that references this issue on Jul 17, 2025
    50b3c33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:corekind:bugThis is a clearly a bugneeds-triagelabel for new issues that we didn't triage yetpriority:mediumBug that should be fixed before next release but would not block a release

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions