This is an automated email from the ASF dual-hosted git repository.
shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new e227cf56078 Show the same Dag in each language SDK in tabs (#77002)
e227cf56078 is described below
commit e227cf56078078f5d3b99daad24788e3cbf548e2
Author: Jason(Zhe-You) Liu <[email protected]>
AuthorDate: Fri Nov 13 13:53:44 2026 +0800
Show the same Dag in each language SDK in tabs (#77002)
The "Dag definition" section of the language SDK overview described
the model but showed no code, and linked only the TypeScript guide.
Add a tab set with one Dag, orders_daily, declared in TypeScript, Java
and Go: a task group, a TaskFlow-style sum, if/else and switch/case
branching, and a triggered Dag on a labeled edge. Each tab links to its
SDK's Dag definition guide. The examples follow the APIs on main
(Label.of(...).on(...) in TypeScript and Java, airflow.Label(...).On(...)
in Go, and the @apache-airflow/ts-sdk package name).
Tabs use sphinx-design's tab-set, which the docs build already loads.
---
.../language-sdks/index.rst | 239 ++++++++++++++++++++-
1 file changed, 238 insertions(+), 1 deletion(-)
diff --git a/airflow-core/docs/authoring-and-scheduling/language-sdks/index.rst
b/airflow-core/docs/authoring-and-scheduling/language-sdks/index.rst
index eb71b673b79..78720f2e2df 100644
--- a/airflow-core/docs/authoring-and-scheduling/language-sdks/index.rst
+++ b/airflow-core/docs/authoring-and-scheduling/language-sdks/index.rst
@@ -81,7 +81,244 @@ from the same Dag bundle.
The **Code** view in the Airflow UI shows the source the SDK embeds in the
artifact.
-For TypeScript, see :ref:`typescript-sdk/dag-definition`.
+Each tab below declares the same Dag, ``orders_daily``:
+
+* the ``extract`` task group counts orders for two regions,
+* ``summarize`` adds them up,
+* ``has_orders`` runs ``load`` when there are orders, and ``alert`` when there
are none,
+* ``pick_report`` picks a full or a brief report,
+* ``trigger_finance`` starts a run of the ``finance_close`` Dag after
``load``, on a labeled edge.
+
+.. tab-set::
+ :sync-group: sdk
+
+ .. tab-item:: TypeScript
+ :sync: typescript
+
+ A handler takes one object of named arguments, and calling a task
names its inputs. The calls you
+ write are the dependencies, and the compiler checks each one.
+
+ .. code-block:: typescript
+
+ import { Bundle, Dag, Label, triggerDagRun } from
"@apache-airflow/ts-sdk";
+
+ const dag = new Dag("orders_daily", { schedule: "@daily", tags:
["orders"], queue: "typescript" });
+
+ // A task group: its task ids are "extract.eu" and "extract.us".
+ const extract = dag.taskGroup("extract");
+ const eu = extract.task("eu", async (): Promise<number> => 120)();
+ const us = extract.task("us", async (): Promise<number> => 80)();
+
+ // Passing a task's result draws the dependency and delivers the
value.
+ const summarize = dag.task("summarize", async ({ eu, us }: { eu:
number; us: number }) => eu + us);
+ const total = summarize({ eu, us });
+
+ const load = dag.task("load", async ({ total, table }: { total:
number; table: string }) => {
+ console.log(`Loading ${total} orders into ${table}`);
+ });
+ const loaded = load({ total, table: "warehouse.orders" });
+ const alerted = dag.task("alert", async () => console.log("No
orders today"))();
+
+ // if / else: the side not taken is skipped.
+ const hasOrders = async ({ total }: { total: number }) => total >
0;
+ dag.if(hasOrders, { total }, { taskId: "has_orders"
}).then(loaded).else(alerted);
+
+ // switch / case: the decider returns the task to run.
+ const full = dag.task("report_full", async () => console.log("Full
report"))();
+ const brief = dag.task("report_brief", async () =>
console.log("Brief report"))();
+ const pickReport = async ({ total }: { total: number }) => (total
> 1000 ? full : brief);
+ dag.switch(pickReport, { total }, { taskId: "pick_report"
}).case(full).case(brief);
+
+ // Trigger another Dag after the load, on a labeled edge.
+ const triggered = dag.task("trigger_finance", triggerDagRun({
dagId: "finance_close" }))();
+ loaded.before(Label.of("hand off").on(triggered));
+
+ await new Bundle(dag).serve();
+
+ See :ref:`typescript-sdk/dag-definition`.
+
+ .. tab-item:: Java
+ :sync: java
+
+ Annotations carry the configuration. An ``@Airflow.Deps`` class wires
the graph through a view that the
+ annotation processor generates, so ``javac`` type-checks every edge.
An interface-based API is also
+ available.
+
+ .. code-block:: java
+
+ package com.example;
+
+ import static java.lang.System.Logger.Level.INFO;
+
+ import org.apache.airflow.sdk.*;
+
+ @Airflow.Dag(id = "orders_daily", schedule = "@daily", tags =
{"orders"}, queue = "java")
+ public class OrdersDaily {
+ private static final System.Logger log =
System.getLogger(OrdersDaily.class.getName());
+
+ // A task group: its task ids are "extract.eu" and "extract.us".
+ @Airflow.TaskGroup(id = "extract")
+ static class Extract {
+ @Airflow.Task
+ public long eu() { return 120L; }
+
+ @Airflow.Task
+ public long us() { return 80L; }
+ }
+
+ @Airflow.Task
+ public long summarize(long eu, long us) { return eu + us; }
+
+ @Airflow.Task
+ public void load(long total, String table) {
+ log.log(INFO, "Loading {0} orders into {1}", total, table);
+ }
+
+ @Airflow.Task
+ public void alert() { log.log(INFO, "No orders today"); }
+
+ // if / else: the side not taken is skipped.
+ @Airflow.If(id = "has_orders")
+ public boolean hasOrders(long total) { return total > 0; }
+
+ @Airflow.Task(id = "report_full")
+ public void reportFull() { log.log(INFO, "Full report"); }
+
+ @Airflow.Task(id = "report_brief")
+ public void reportBrief() { log.log(INFO, "Brief report"); }
+
+ // switch / case: the decider returns the class generated for
the task to run.
+ @Airflow.Switch(id = "pick_report")
+ public Class<? extends Task> pickReport(long total) {
+ return total > 1000 ? OrdersDailyBuilder.ReportFull.class :
OrdersDailyBuilder.ReportBrief.class;
+ }
+
+ // Trigger another Dag. This method runs when the Dag is built,
not when the task runs.
+ @Airflow.Task(id = "trigger_finance")
+ public TriggerDagRun triggerFinance() { return new
TriggerDagRun("finance_close"); }
+
+ // Each call is a task, and passing its result to another call
draws the dependency.
+ @Airflow.Deps
+ static class Wiring implements OrdersDailyDeps {
+ void depends() {
+ var total = summarize(extract().eu(), extract().us());
+ var loaded = load(total, lit("warehouse.orders"));
+ hasOrders(total).Then(loaded).Else(alert());
+ pickReport(total).Case(reportFull()).Case(reportBrief());
+ loaded.before(Label.of("hand off").on(triggerFinance()));
+ }
+ }
+
+ public static void main(String[] args) {
+ Server.create(args).serve(new
Bundle().register(OrdersDaily.class));
+ }
+ }
+
+ See :ref:`java-sdk/dag-definition`.
+
+ .. tab-item:: Go
+ :sync: go
+
+ ``airflow.Inputs`` passes task results and literals to a task
function, in parameter order. A task's id
+ is its function's name unless a ``TaskSpec`` sets one.
+
+ .. code-block:: go
+
+ package main
+
+ import (
+ "log"
+
+ "github.com/apache/airflow/go-sdk/airflow"
+ )
+
+ func main() {
+ bundle := airflow.Bundle()
+ bundle.Register(ordersDaily())
+ if err := bundle.Serve(); err != nil {
+ log.Fatal(err)
+ }
+ }
+
+ func ordersDaily() *airflow.DagRef {
+ dag := airflow.Dag("orders_daily", airflow.DagSpec{
+ Schedule: "@daily",
+ Tags: []string{"orders"},
+ Queue: "golang",
+ })
+
+ // A task group: its task ids are "extract.eu" and
"extract.us".
+ extract := dag.TaskGroup("extract")
+ eu := extract.Task(countOrders, airflow.TaskSpec{TaskID:
"eu"}, airflow.Inputs(airflow.Literal("eu")))
+ us := extract.Task(countOrders, airflow.TaskSpec{TaskID:
"us"}, airflow.Inputs(airflow.Literal("us")))
+
+ // Passing a task's result draws the dependency and delivers
the value.
+ total := dag.Task(summarize, airflow.Inputs(eu, us))
+ loaded := dag.Task(load, airflow.Inputs(total,
airflow.Literal("warehouse.orders")))
+
+ // if / else: the side not taken is skipped.
+ dag.If(hasOrders, airflow.TaskSpec{TaskID: "has_orders"},
airflow.Inputs(total)).
+ Then(loaded).
+ Else(dag.Task(alert))
+
+ // switch / case: the decider returns the task to run.
+ r := &reports{
+ full: dag.Task(reportFull, airflow.TaskSpec{TaskID:
"report_full"}),
+ brief: dag.Task(reportBrief, airflow.TaskSpec{TaskID:
"report_brief"}),
+ }
+ dag.Switch(r.pick, airflow.TaskSpec{TaskID: "pick_report"},
airflow.Inputs(total)).
+ Case(r.full).
+ Case(r.brief)
+
+ // Trigger another Dag after the load, on a labeled edge.
+ triggered := dag.Task(
+ airflow.TriggerDagRun(airflow.TriggerDagRunSpec{DagID:
"finance_close"}),
+ airflow.TaskSpec{TaskID: "trigger_finance"},
+ )
+ loaded.Before(airflow.Label("hand off").On(triggered))
+
+ return dag
+ }
+
+ var ordersByRegion = map[string]int{"eu": 120, "us": 80}
+
+ func countOrders(_ airflow.Context, region string) (int, error) {
return ordersByRegion[region], nil }
+
+ func summarize(_ airflow.Context, eu, us int) (int, error) {
return eu + us, nil }
+
+ func load(actx airflow.Context, total int, table string) error {
+ actx.Logger().InfoContext(actx, "loading orders", "total",
total, "table", table)
+ return nil
+ }
+
+ func alert(actx airflow.Context) error {
+ actx.Logger().InfoContext(actx, "no orders today")
+ return nil
+ }
+
+ func hasOrders(_ airflow.Context, total int) (bool, error) {
return total > 0, nil }
+
+ func reportFull(actx airflow.Context) error {
+ actx.Logger().InfoContext(actx, "full report")
+ return nil
+ }
+
+ func reportBrief(actx airflow.Context) error {
+ actx.Logger().InfoContext(actx, "brief report")
+ return nil
+ }
+
+ // reports holds the switch's cases, because a decider returns a
*TaskRef.
+ type reports struct{ full, brief *airflow.TaskRef }
+
+ func (r *reports) pick(_ airflow.Context, total int)
(*airflow.TaskRef, error) {
+ if total > 1000 {
+ return r.full, nil
+ }
+ return r.brief, nil
+ }
+
+ See :ref:`go-sdk/dag-definition`.
.. _language-sdks/stub-taskhandler: