Hi all,

I'd like to propose a way for a task's trigger condition to refer to the 
specific state of an upstream task, rather than counting states across all 
upstream tasks.

Business use case

We run a multi-step batch pipeline with the following task graph:

task_a >> task_b >> task_c
task_a >> task_d
task_b >> task_d
task_c >> task_e
task_d >> task_e
task_d >> task_f
task_e >> task_f


The scheduling conditions required are:

Task
Condition
task_d
task_a = success AND task_b = failed
task_e
task_c = success OR task_d = success
task_f
task_d = failed AND task_e = success

These conditions describe when each task is eligible to be scheduled, not what 
happens inside the task:

  *   task_d has two upstream tasks. It must run only when one specific 
upstream succeeded and the other specific upstream failed — not when any 
upstream failed, and not when both failed.
  *   task_e has two upstream tasks. It must run when either one of them 
specifically succeeded — not when any upstream in the whole graph succeeded.
  *   task_f has two upstream tasks. It must run when one specific upstream 
failed and the other specific upstream succeeded — opposite state requirements 
on each named upstream simultaneously.

In all three cases the condition is a precondition on scheduling, not on 
execution. The scheduler already evaluates preconditions before dispatching a 
task — trigger rules are exactly that mechanism. The missing capability is that 
trigger rules count states across the whole upstream pool anonymously; they 
cannot test the state of a specific named upstream task.

A BranchPythonOperator or inline Python check cannot substitute here — it runs 
only after the task is already scheduled and started, and it does not affect 
how the scheduler treats downstream tasks that depend on the outcome.

Why existing trigger rules cannot express these

All 13 trigger rules treat upstream tasks as an anonymous pool — they count or 
ratio states across the whole group. They have no concept of which task is in 
which state. The moment a condition requires naming a specific upstream task, 
every existing rule breaks down.

Attempting task_d (task_a = success AND task_b = failed):

  *   one_failed — catches task_b = failed ✓, but also fires when task_a itself 
failed ✗.
  *   all_done — fires for every combination of outcomes, including both 
succeeded or both failed ✗.
  *   none_failed — fires only when nothing failed, the opposite of what we 
need ✗.

No existing rule can say "this specific upstream must succeed AND that specific 
upstream must fail."

Attempting task_e (task_c = success OR task_d = success):

  *   one_success — fires when at least one upstream succeeded, but task_e has 
upstream tasks beyond task_c and task_d in the graph. one_success fires as long 
as any one of them succeeded, so it fires even when neither task_c nor task_d 
specifically succeeded ✗.
  *   all_success — requires every upstream to succeed, far too strict ✗.
  *   all_done — fires for every outcome combination ✗.

No existing rule can express OR across two specific named tasks while ignoring 
the states of all other upstreams.

Attempting task_f (task_d = failed AND task_e = success):

  *   one_failed — fires when any upstream failed, not specifically task_d ✗.
  *   none_failed — blocks entirely when task_d failed, which is exactly the 
case we want to allow ✗.
  *   all_done — fires regardless of which tasks succeeded or failed ✗.

No existing rule can combine a required failure on one named task with a 
required success on another.

The fundamental problem: trigger rules count states across the whole upstream 
pool. As soon as the condition requires knowing which task is in which state, 
every rule either over-fires or under-fires.

Current workaround

Insert a gate task with trigger_rule="all_done" that reads upstream states at 
runtime (via RuntimeTaskInstance.get_task_states, which is not documented as 
public API), evaluates the expression, and raises AirflowSkipException when it 
is false. It works, but:

  *   every condition adds an extra task to the graph and to scheduling
  *   the real dependency logic is hidden in Python instead of shown in the DAG
  *   it relies on internal APIs that may change
  *   an OR condition can't fire early, because the gate always waits for every 
upstream task to finish

In the pipeline above, three gate tasks would be needed — one per conditional 
task — tripling the graph complexity for logic that should be expressible in 
the DAG definition itself.

Relationship to AIP-106

AIP-106 (Composable Trigger Rules) adds count and ratio expressions over 
upstream states, which is a different problem. It explicitly lists per-upstream 
named conditions as out of scope and a possible separate AIP. This proposal is 
meant as that follow-up and could reuse the same TR namespace.

Possible API-Draft

from airflow.sdk import TR, success, failed

task_d = MyOperator(
    task_id="task_d",
    trigger_rule=TR.when(success(task_a) & failed(task_b)),
)

task_e = MyOperator(
    task_id="task_e",
    trigger_rule=TR.when(success(task_c) | success(task_d)),
)

task_f = MyOperator(
    task_id="task_f",
    trigger_rule=TR.when(failed(task_d) & success(task_e)),
)


The scheduler would evaluate this in the trigger-rule dependency check, using 
states it already has. Named tasks would automatically become upstream tasks.

I am preparing a full AIP draft and will share the Confluence link once my wiki 
account is approved.

In the meantime, I am happy to share the draft document directly if that helps 
the discussion.

Thanks,

Gowthami


Reply via email to