@@ -135,6 +135,49 @@ 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(handler) ` 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+ async function hasRows({ rows }: { rows: number }): Promise <boolean > {
146+ return rows > 0 ;
147+ }
148+
149+ const gate = dag .if (hasRows , { rows: validated });
150+ gate .then (loadIfReady ).else (loadFallback );
151+ ```
152+
153+ ** The condition is a handler, declared and wired in one call.** That ADR writes
154+ ` dag.If(hasRows, airflow.Inputs(validated), airflow.TaskSpec{...}) ` in Go, and
155+ ` dag.if(hasRows, { rows: validated }, { ... }) ` reads the same way: inputs, then an optional
156+ ` TaskSpec ` . The deciding task takes its id from the function's name unless the spec's ` taskId ` sets
157+ it, and the spec carries its other options, such as ` queue ` and ` retries ` . The compiler checks that
158+ the handler returns a ` boolean ` , and that the inputs match its argument.
159+
160+ ** A condition is a node.** What ` dag.if ` returns carries ` before ` and ` after ` , and stands at the
161+ other end of an edge as the deciding task, so ` notified.after(gate) ` reads as Go's ` .After(gate) ` . It
162+ is also a valid branch of another condition.
163+
164+ ** A ` then ` chain is a thenable, and is guarded rather than avoided.** An object with a callable
165+ ` then ` is a * thenable* : were the condition ` dag.if ` returns to reach an ` await ` , the runtime would hand
166+ its ` then ` a resolve function where a task reference belongs. Two things contain that. ` .then(...) `
167+ returns an object carrying only ` .else ` , so nothing past the first step is awaitable at all; and
168+ ` .then ` rejects a function argument by naming the cause, so an author who does await it reads "this
169+ builds a branch, drop the await" rather than a type error about references.
170+
171+ ** A branch is a real branch to Airflow.** The control edges serialize as ordinary order-only edges
172+ and carry no branch-candidate field, as that ADR's consequences require. The condition task is
173+ serialized with ` _can_skip_downstream ` , and it writes the ` skipmixin_key ` XCom alongside the skip, so
174+ clearing a skipped branch re-skips it the way a Python ` @task.branch ` does rather than running the
175+ side the condition rejected.
176+
177+ A one-sided ` if ` is a branch with one candidate — it skips ` then ` and follows nothing — rather than a
178+ ` ShortCircuitOperator ` , which would also skip the whole downstream closure and ignore trigger rules.
179+ A guarded task takes no argument for the control edge: a condition's boolean is a signal, not data.
180+
138181## Consequences
139182
140183- One authoring surface (` dag.task() ` plus its factory) covers the graph and each task's arguments,
0 commit comments