You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
TypeScript SDK: native TypeScript Dag declaration Design #69288
The TypeScript SDK intentionally ships in "Python-stub-Dag mode": TypeScript handlers are registered for Dag/task IDs declared in Python. Higher-level TypeScript Dag authoring was deferred until coordinator-mode execution is reviewed.
What needs to happen
Allow declaring Dags natively in TypeScript.
Revisit bundle ergonomics alongside the authoring interface.
Acceptance criteria
A Dag can be authored end to end in TypeScript with no Python stub file.
I think we can start discussing the user interface for the native TypeScript Dag definition here, if you'd like to join. Otherwise, I'll start the design on my side. (No need to start implementing at this point, just a discussion about what exactly the interface will look like.)
The high-level idea of how the native Dag works for each Lang-SDK is:
We first define a DSL for the Dag and Task (we should be able to set, for example, schedule="@daily" at the Dag level, retries=3 at the Task level, etc.).
Then the Lang-SDK runtime should be able to return an airflow-core/src/airflow/serialization/schema.json-compatible Dag JSON back to the coordinator via the same comm protocol.
Some points we need to be aware of:
From a language perspective, whether the syntax itself can feasibly return a serialized Dag JSON.
Whether the syntax is "TaskFlow"-like enough. By TaskFlow, I mean users can define dependencies through "chaining," without needing something like a "taskId as a string/array" reference on the other task.
How arguments get resolved in the TS-SDK runtime across tasks in the same language, and for the same task across different languages.
Here is one possible TypeScript shape for discussion. The goal is to preserve Python TaskFlow's dependency semantics while keeping the API natural for TypeScript; none of these names are final.
The Dag could be declared first, with tasks defined on it:
TypeScript cannot reproduce Python's >> and << because JavaScript uses those operators for numeric bit shifting. I also find before() and after() easier to read than setUpstream() and setDownstream(). The Python-style
methods could potentially be added later as aliases. This is just a proposal right now.
I'd also like to remove registerTask and introduce registerDags(d1, d2) as the entrypoint.
Before removing registerTask, I think we should clarify one compatibility question: does native TypeScript Dag authoring replace the existing Python @task.stub Dag with TypeScript handlers, or should the SDK continue supporting both modes?
If both remain supported, we could keep registerTask() as a compatibility API and use registerDags() only for complete TypeScript-authored Dags.
I would avoid making registerDags() accept a special pseudo-Dag for a Python-defined Dag. TypeScript does not own that Dag’s schedule or dependency graph, so treating its handlers as a Dag could make the source of truth unclear.
Thanks, both proposals make sense to me. For naming, I agree with keeping task() lowercase because it is a factory method rather than a class or constructor.
I also like separating generated Airflow options under spec, but I would keep taskId explicit and top-level. I find this more self-documenting than a positional "extract", which could represent an ID, display name, or task type.
Do you have any thoughts on the equivalent for the TS world, where a user could explicitly reference the source argument name?
For the Python @task.stub → TypeScript-handler direction, I think the name already carried by each arg_binding can become the property name in a single input object.
Here, region_code is the source argument name and region is only a local TypeScript alias. Keeping task arguments under input also avoids collisions with SDK-provided fields such as ctx and client (ctx.signal remains available for cancellation).
The runtime could validate each resolved value against its value_schema when provided before constructing input.
I'd also like to remove registerTask and introduce registerDags(d1, d2) as the entrypoint.
Before removing registerTask, I think we should clarify one compatibility question: does native TypeScript Dag authoring replace the existing Python @task.stub Dag with TypeScript handlers, or should the SDK continue supporting both modes?
The existing mixed-language Dag should remain functional - the native TypeScript Dag is a new, additional feature. On top of that, the native TypeScript Dag will respect Dag-level and Task-level arguments, as well as task dependencies, defined on the native Dag side.
If both remain supported, we could keep registerTask() as a compatibility API and use registerDags() only for complete TypeScript-authored Dags.
The TS SDK isn't released yet, so we're free to introduce any kind of breaking change at this point. I still lean toward removing the registerTask interface. The reason being that I'd like to support just one Dag authoring interface. This is also exactly why I challenged the registerTask design back in #67908 (comment). It's fine to have registerTask in the early stage, but I'd like to collapse the interface into the dag_instance = new Dag(), t1 = dag_instance.task() style. Whether the TypeScript Dag serves a "mixed" or "native" purpose, both can be authored in the exact style we're discussing here.
I would avoid making registerDags() accept a special pseudo-Dag for a Python-defined Dag.
This won't happen.
TypeScript does not own that Dag's schedule or dependency graph, so treating its handlers as a Dag could make the source of truth unclear.
The "native Dag" here will own the Dag's schedule and dependency graph. It just focuses on the basics of an SDK — users can define Dag- and Task-level arguments, define each task callable, render properly on the Airflow UI, and trigger it as a normal Dag — except the whole Dag is defined in TypeScript.
Okay, I think it makes sense to keep a single authoring interface and remove registerTask().
I want to clarify how the existing Python-Dag/TypeScript-handler mode would work. In that mode, Python defines the complete Dag, while TypeScript only provides the implementation for one or more tasks.
If the TypeScript user creates a new Dag() with the same dagId and adds those handlers, it would only be a partial representation. Python would still own the schedule, dependencies, and remaining tasks.
Normally, registerDags() would tell the coordinator to serialize the TypeScript Dag as a complete Dag. Doing that here could create a second, incomplete definition with the same dagId.
How should the coordinator know that this TypeScript object is only registering handlers and should not be serialized? Would Dag need an explicit handler-only/external mode, or would this association happen through another mechanism?
Something like:
new Dag({
dagId: "sales_pipeline",
mode: "handlerOnly",
});
▎ How should the coordinator know that this TypeScript object is only registering handlers and should not be serialized?
I'd prefer to avoid a mode string on Dag and make the distinction type-level instead:
// native: TS owns schedule + dependencies, gets serializedconstpipeline=newDag({dagId: "sales_pipeline",schedule: "@daily"});pipeline.task("extract",{run: extractFn,spec: {retries: 3}});// handler-only: a reference to a Python-owned Dag, never serializedconstexternal=Dag.external("sales_pipeline");external.task("transform",{run: transformFn});
The type returned by Dag.external() simply doesn't have schedule/spec/dependency fields, so "TypeScript does not own this Dag" is enforced at compile time rather than ignored at runtime.
The coordinator needs no flag check: Dag instances serialize; external refs only register (dagId, taskId) → handler.
This keeps the single authoring interface — new Dag() remains the only way to author a Dag, and .task() feels identical on both. Handler-only is binding, not authoring.
Compared to a registerHandlers("id", { extract: fn }) map, the .task() form still has room for a per-task args schema to validate the resolved input object.
Yeah, I think both are valid options. Or another direction, having new ExternalDag as interface. Though we will need to check if there's better term than the "external". Or mode: mixed_lang_dag and mode: native_dag.
Thanks both. I agree that a type-level distinction such as Dag.external(...) or ExternalDag is cleaner while preserving the same .task() interface. We can refine the exact name separately.
changed the title [-]TypeScript SDK: native TypeScript Dag declaration[/-][+]TypeScript SDK: native TypeScript Dag declaration Design[/+]on Sep 15, 2026
Activity
[-]TypeScript SDK: native TypeScript Dag declaration (remove Python stub requirement)[/-][+]TypeScript SDK: native TypeScript Dag declaration[/+]jason810496 commentedon Jul 24, 2026
Hi @guan404ming, @shivaam,
I think we can start discussing the user interface for the native TypeScript Dag definition here, if you'd like to join. Otherwise, I'll start the design on my side. (No need to start implementing at this point, just a discussion about what exactly the interface will look like.)
The high-level idea of how the native Dag works for each Lang-SDK is:
schedule="@daily"at the Dag level,retries=3at the Task level, etc.).airflow-core/src/airflow/serialization/schema.json-compatible Dag JSON back to the coordinator via the same comm protocol.Some points we need to be aware of:
For reference, here are the Go-SDK counterparts:
Either way, let's focus only on the user interface design for now.
Please let me know WDYT, thanks.
shivaam commentedon Jul 26, 2026
Here is one possible TypeScript shape for discussion. The goal is to preserve Python TaskFlow's dependency semantics while keeping the API natural for TypeScript; none of these names are final.
The Dag could be declared first, with tasks defined on it:
pipeline.task(...)could use the existingregisterTask()mechanism internally while also recording the information needed to serialize the Dag.Passing task outputs would create data dependencies:
This constructs:
For ordering without passing data, we could provide both
before()andafter():TypeScript cannot reproduce Python's
>>and<<because JavaScript uses those operators for numeric bit shifting. I also findbefore()andafter()easier to read thansetUpstream()andsetDownstream(). The Python-stylemethods could potentially be added later as aliases. This is just a proposal right now.
shivaam commentedon Jul 26, 2026
For dependency helpers, I think we can preserve the useful behavior of Python's
chain().A linear chain:
would create:
We could also support Python's pairwise behavior for equally sized arrays:
This would create:
The initial API would not need a separate cross-product helper because the same graph can be expressed with
before():If this becomes repetitive in real Dags,
crossDownstream()could be added later as a convenience:3 remaining items
shivaam commentedon Jul 28, 2026
Before removing
registerTask, I think we should clarify one compatibility question: does native TypeScript Dag authoring replace the existing Python@task.stubDag with TypeScript handlers, or should the SDK continue supporting both modes?If both remain supported, we could keep
registerTask()as a compatibility API and useregisterDags()only for complete TypeScript-authored Dags.Alternatively, we could provide a new API:
I would avoid making
registerDags()accept a special pseudo-Dag for a Python-defined Dag. TypeScript does not own that Dag’s schedule or dependency graph, so treating its handlers as a Dag could make the source of truth unclear.shivaam commentedon Jul 28, 2026
Thanks, both proposals make sense to me. For naming, I agree with keeping
task()lowercase because it is a factory method rather than a class or constructor.I also like separating generated Airflow options under
spec, but I would keeptaskIdexplicit and top-level. I find this more self-documenting than a positional"extract", which could represent an ID, display name, or task type.shivaam commentedon Jul 28, 2026
For the Python
@task.stub→ TypeScript-handler direction, I think thenamealready carried by eacharg_bindingcan become the property name in a singleinputobject.For example:
Assuming
extract()pushes"us-east-1", the TypeScript runtime would resolve the XCom and literal bindings, then invoke the handler as:Here,
region_codeis the source argument name andregionis only a local TypeScript alias. Keeping task arguments underinputalso avoids collisions with SDK-provided fields such asctxandclient(ctx.signalremains available for cancellation).The runtime could validate each resolved value against its
value_schemawhen provided before constructinginput.jason810496 commentedon Jul 28, 2026
The existing mixed-language Dag should remain functional - the native TypeScript Dag is a new, additional feature. On top of that, the native TypeScript Dag will respect Dag-level and Task-level arguments, as well as task dependencies, defined on the native Dag side.
The TS SDK isn't released yet, so we're free to introduce any kind of breaking change at this point. I still lean toward removing the
registerTaskinterface. The reason being that I'd like to support just one Dag authoring interface. This is also exactly why I challenged theregisterTaskdesign back in #67908 (comment). It's fine to haveregisterTaskin the early stage, but I'd like to collapse the interface into thedag_instance = new Dag(),t1 = dag_instance.task()style. Whether the TypeScript Dag serves a "mixed" or "native" purpose, both can be authored in the exact style we're discussing here.This won't happen.
The "native Dag" here will own the Dag's schedule and dependency graph. It just focuses on the basics of an SDK — users can define Dag- and Task-level arguments, define each task callable, render properly on the Airflow UI, and trigger it as a normal Dag — except the whole Dag is defined in TypeScript.
shivaam commentedon Jul 29, 2026
Okay, I think it makes sense to keep a single authoring interface and remove
registerTask().I want to clarify how the existing Python-Dag/TypeScript-handler mode would work. In that mode, Python defines the complete Dag, while TypeScript only provides the implementation for one or more tasks.
If the TypeScript user creates a
new Dag()with the samedagIdand adds those handlers, it would only be a partial representation. Python would still own the schedule, dependencies, and remaining tasks.Normally,
registerDags()would tell the coordinator to serialize the TypeScript Dag as a complete Dag. Doing that here could create a second, incomplete definition with the samedagId.How should the coordinator know that this TypeScript object is only registering handlers and should not be serialized? Would
Dagneed an explicit handler-only/external mode, or would this association happen through another mechanism?Something like:
guan404ming commentedon Aug 3, 2026
▎ How should the coordinator know that this TypeScript object is only registering handlers and should not be serialized?
I'd prefer to avoid a mode string on Dag and make the distinction type-level instead:
Dag.external()simply doesn't have schedule/spec/dependency fields, so "TypeScript does not own this Dag" is enforced at compile time rather than ignored at runtime.jason810496 commentedon Aug 3, 2026
Yeah, I think both are valid options. Or another direction, having
new ExternalDagas interface. Though we will need to check if there's better term than the "external". Ormode: mixed_lang_dagandmode: native_dag.shivaam commentedon Aug 4, 2026
Thanks both. I agree that a type-level distinction such as
Dag.external(...)orExternalDagis cleaner while preserving the same.task()interface. We can refine the exact name separately.TypeScript SDK: replace registerTask with Dag and registerDags
TypeScript SDK: replace registerTask with Dag and registerDags
jason810496 commentedon Sep 15, 2026
Design is finished in #72047.
[-]TypeScript SDK: native TypeScript Dag declaration[/-][+]TypeScript SDK: native TypeScript Dag declaration Design[/+]