TypeScript SDK
This is an experimental feature.
The TypeScript SDK lets you group task handlers in a Dag and implement their logic in TypeScript (or
plain JavaScript), running on Node.js. A matching Python stub Dag still declares the scheduling shape and
dependencies; individual tasks delegate to a Node.js subprocess that is spawned by
NodeCoordinator for each task instance.
The SDK is the apache-airflow-ts-sdk package (ESM-only). It is currently in beta and its API may change.
Warning
Install an available release from npm. To try an unreleased change, build it from source in the
ts-sdk/ directory of the Airflow repository
and depend on it locally (see ts-sdk/example/ for a working setup).
See also
For the full TypeScript API reference (Dag, DagRegistry, serveDags, task handlers,
TaskClient, supporting types, and exceptions),
see the TypeScript SDK API reference.
Prerequisites
Node.js 22 or later must be available on the Airflow worker nodes.
The packed bundle (a single
bundle.mjsfile, see Building and packaging) must be accessible from the worker, under a directory the coordinator scans.The
apache-airflow-task-sdkpackage (installed with Airflow) provides the coordinator; no additional Python packages are needed.In the TypeScript project, install the
apache-airflow-ts-sdknpm package to author task handlers:npm install apache-airflow-ts-sdk
Quick start
The following example shows the minimal moving parts: a Python Dag with a stub task, and a TypeScript implementation of that task.
Python Dag (the scheduling side)
from airflow.sdk import dag, task
@dag
def typescript_example():
@task
def python_start():
return "hello from Python"
@task.stub(queue="typescript")
def build_message(): ...
python_start() >> build_message()
typescript_example()
@task.stub declares the shape of the TypeScript task without any Python implementation. The queue
value routes the task to the Node.js coordinator.
TypeScript implementation
A task is an ordinary (usually async) function receiving TaskHandlerArgs. Create a Dag with
the dag_id it implements, attach each handler with dag.task, collect the Dags in a DagRegistry,
then serve them to Airflow with serveDags; that top-level await makes the module a runnable bundle
entry point.
import { Dag, DagRegistry, serveDags, type TaskHandlerArgs } from "apache-airflow-ts-sdk";
export async function buildMessage({ ctx, client }: TaskHandlerArgs) {
const upstream = await client.getXCom<string>({
key: "return_value",
taskId: "python_start",
});
const greeting = await client.getVariable("typescript_example_greeting");
return `${greeting ?? "hello from TypeScript"}; upstream=${upstream ?? "missing"}`;
}
const dag = new Dag("typescript_example");
dag.task("build_message", buildMessage);
await serveDags(new DagRegistry(dag));
The dagId passed to new Dag(...) must match the dag_id of the Python Dag, and each taskId
passed to dag.task must match a @task.stub function in that Dag. The registry passed to
serveDags is the bundle’s complete set of Dags; a second serveDags call is rejected. A Dag left out
of the registry is not part of the packed bundle, and its tasks are marked removed at runtime.
DagRegistry holds no sockets and starts nothing, so a unit test can build one and dispatch a handler
through registry.getTaskHandler(dagId, taskId) without a coordinator runtime. A bundle that collects
its Dags across several modules can add them incrementally with registry.register(...).
new Dag and dag.task take a trailing options object — spec on both, plus inputs on a task.
These are not used yet; do not set them. Any other key is rejected.
Note
As with the other language SDKs, XCom dependencies are declared in the Python stub Dag (they define task
order). The value must still be read explicitly in TypeScript via client.getXCom, and produced either
by the task’s return value or by client.setXCom.
Coordinator configuration
Register the coordinator and route the queue to it under [sdk] in airflow.cfg (or the equivalent
AIRFLOW__SDK__* environment variables):
[sdk]
coordinators = {
"ts": {
"classpath": "airflow.sdk.coordinators.node.NodeCoordinator",
"kwargs": {"bundles_root": ["/opt/airflow/ts-bundles"]}
}
}
queue_to_coordinator = {"typescript": "ts"}
bundles_root is one or more directories the coordinator scans for bundles; queue_to_coordinator
routes stub tasks with queue="typescript" to this coordinator. See
NodeCoordinator configuration for the full list of accepted kwargs.
There is no separate Node.js worker to run: the Airflow worker launches the bundle with node once per
task instance.
Note
The coordinator runs inside the Airflow worker, so the [sdk] config (and the packed bundle.mjs
files in bundles_root) only need to be present wherever tasks actually execute. With
CeleryExecutor, setting them on the Celery workers is sufficient. With LocalExecutor, tasks run
inside the scheduler process, so they must be present where the scheduler can read them. The API server
and Dag processor do not need them.
Writing tasks
Every task handler receives a single TaskHandlerArgs object:
Field |
Value |
|---|---|
|
The task’s execution context: |
|
A |
A non-undefined return value becomes the task’s return_value XCom, matching Python @task
behavior. An uncaught exception (or rejected promise) marks the task instance failed in Airflow, triggering
retries if configured on the stub.
The TaskClient surface
getVariable(key)— returns the Variable as a string, ornullwhen it is missing;getVariableOrThrow(key)throwsVariableNotFoundErrorinstead, matching PythonVariable.getwith no default.getConnection(connId)— returns aConnectionResultwith fieldsidandtype, plus the optional fieldshost,schema,login,password,port, andextra(each may be missing ornull), ornullwhen the connection does not exist;getConnectionOrThrow(connId)throwsConnectionNotFoundErrorinstead, matching PythonBaseHook.get_connection.getXCom<T>({key, ...})— reads an XCom value, ornullwhen it is missing. The locator fields (dagId,runId,taskId,mapIndex) default to the current task; passtaskIdto read an upstream task’s XCom. See XCom type mapping for how the stored JSON maps to JavaScript types.setXCom({key, value, ...})— publishes an XCom value.
Logging
Anything the task writes to stdout or stderr (console.log, console.error) is captured by the worker
and shown in the Airflow task log (stdout at INFO level, stderr at ERROR level). The SDK does not
yet expose a dedicated structured-logging API.
XCom type mapping
XCom values are stored as JSON in Airflow’s metadata database. The table below shows how those JSON types
surface as JavaScript values when read back via getXCom.
Python type |
JSON |
JavaScript type (from |
|---|---|---|
|
number (integer) |
|
|
number (decimal) |
|
|
string |
|
|
boolean |
|
|
null |
|
|
array |
|
|
object |
|
Note
JavaScript has a single number type (an IEEE 754 double), so integers and decimals arrive as the same
type, and integers larger than Number.MAX_SAFE_INTEGER (253 − 1) may lose precision.
Building and packaging
airflow-ts-pack (shipped with the SDK) bundles the entry module and all of its imports with esbuild into
a single self-contained ESM file, bundle.mjs, and embeds the manifest (the dag_id and task_id
map plus the supervisor schema version) as a leading //# airflowMetadata=<base64> comment — one file to
deploy, with no separate manifest or node_modules.
esbuild is an optional peer dependency: packing is build-time only, so the runtime install of
apache-airflow-ts-sdk skips it, and it must be installed separately before running airflow-ts-pack.
npm install --save-dev esbuild
npx airflow-ts-pack src/main.ts --outdir dist
Use --outdir <dir> to choose the output directory (default dist) and --source <name> to set the
source name displayed in the Airflow UI (default: the entry file’s basename).
Deploying
Copy or mount bundle.mjs into a directory listed in the coordinator’s bundles_root.
NodeCoordinator searches the configured directories in order and
launches the first usable bundle with node.
NodeCoordinator configuration
All kwargs in the coordinators config entry are passed to the
NodeCoordinator constructor:
Parameter |
Default |
Description |
|---|---|---|
|
(required) |
One or more directories searched, in order, for a |
|
|
Path to the |
|
|
Seconds to wait for the Node.js subprocess to connect after launch. Increase this if your bundle startup is slow (e.g. on constrained hardware). |
Limitations
A Python stub Dag is still required. The Execution API does not yet carry Dag structure for non-Python languages, so task names and dependencies are declared in Python with
@task.stub.Beta status. The SDK API may change in incompatible ways between releases.
One bundle per coordinator.
NodeCoordinatorlaunches the first usable bundle found inbundles_root; it does not yet route different Dags or tasks to different bundles. To serve multiple bundles, register multiple coordinators on separate queues.One Node.js subprocess per task instance. Tasks that need to share in-process state between instances should use XCom or an external store instead.