@@ -135,6 +135,43 @@ data is the wiring object itself — `summarize({ north: extractNorth(), south:
135135does. This matches ` Before ` /` After ` in the Go SDK's native Dag interface, spelled to TypeScript
136136convention.
137137
138+ ### Conditional branching: ` if ` and ` else `
139+
140+ ` dag.if(condition) ` is TypeScript's spelling of the construct
141+ [ ` airflow-core/adr/lang-sdk/0008 ` ] ( ../../airflow-core/adr/lang-sdk/0008-control-flow-constructs.md )
142+ names after the host language's control flow:
143+
144+ ``` ts
145+ const gated = dag .task (" has_rows" , async ({ rows }: { rows: number }) => rows > 0 )({
146+ rows: extracted ,
147+ });
148+
149+ dag .if (gated ).then (loadIfReady ).else (loadFallback );
150+ ```
151+
152+ ** The condition is a task reference, not a task id and a function.** ` dag.if ` takes a ` TaskRef ` the
153+ Dag already handed back, whose handler's return type the compiler checks is ` boolean ` . So nothing
154+ depends on the task's id or on its function name, and a condition is declared, named and typed the
155+ same way every other task is. The same reasoning that makes a * case* a reference in that ADR's
156+ decision 2 applies to the condition itself.
157+
158+ ** A ` then ` chain is a thenable, and is guarded rather than avoided.** An object with a callable
159+ ` then ` is a * thenable* : were the object ` dag.if ` returns to reach an ` await ` , the runtime would hand
160+ its ` then ` a resolve function where a task reference belongs. Two things contain that. ` .then(...) `
161+ returns an object carrying only ` .else ` , so nothing past the first step is awaitable at all; and
162+ ` .then ` rejects a function argument by naming the cause, so an author who does await it reads "this
163+ builds a branch, drop the await" rather than a type error about references.
164+
165+ ** A branch is a real branch to Airflow.** The control edges serialize as ordinary order-only edges
166+ and carry no branch-candidate field, as that ADR's consequences require. The condition task is
167+ serialized with ` _can_skip_downstream ` , and it writes the ` skipmixin_key ` XCom alongside the skip, so
168+ clearing a skipped branch re-skips it the way a Python ` @task.branch ` does rather than running the
169+ side the condition rejected.
170+
171+ A one-sided ` if ` is a branch with one candidate — it skips ` then ` and follows nothing — rather than a
172+ ` ShortCircuitOperator ` , which would also skip the whole downstream closure and ignore trigger rules.
173+ A guarded task takes no argument for the control edge: a condition's boolean is a signal, not data.
174+
138175## Consequences
139176
140177- One authoring surface (` dag.task() ` plus its factory) covers the graph and each task's arguments,
0 commit comments