This repo contains a JavaScript/TypeScript SDK for use with the Azure Durable Task Scheduler. With this SDK, you can define, schedule, and manage durable orchestrations using ordinary TypeScript/JavaScript code.
Note that the core
@microsoft/durabletask-jspackage does not provide the Azure Durable Functions programming model, decorators, or worker-indexing metadata — it exposes low-level TaskHubSidecarService gRPC/protobuf helpers that host integrations can reuse (Node.js 22+). For the Azure Durable Functions programming model on the gRPC core, this repository also contains thedurable-functionsprovider package underpackages/azure-functions-durable. The classic v3 (extension-HTTP) predecessor lives at azure-functions-durable-js.
Host integrations that already own trigger metadata and transport encoding can depend on the @microsoft/durabletask-js package directly. TaskHubGrpcWorker registers orchestrators, activities, and entities, and can process raw TaskHubSidecarService protobuf payloads without starting the long-running gRPC worker loop:
const worker = new TaskHubGrpcWorker();
worker.addOrchestrator(myOrchestrator);
worker.addActivity(myActivity);
worker.addEntity(myEntity);
const orchestrationResponseBytes = await worker.processOrchestratorRequest(orchestrationRequestBytes);
const entityResponseBytes = await worker.processEntityBatchRequest(entityBatchRequestBytes);TaskHubGrpcClient already exposes orchestration start/query/event/terminate/suspend/resume/purge APIs and entity signal/read/query/clean APIs through its existing hostAddress and metadataGenerator options. Host integrations that need task-hub routing metadata should provide it through metadataGenerator, keeping host-specific metadata policy outside the core client. Azure-managed scheduler connection strings remain in @microsoft/durabletask-js-azuremanaged.
createTimer(Date | seconds) has no SDK-imposed total-duration cap. Like the Python SDK,
core workers default to three-day backend segments, including durable retry delays. A ten-day
timer uses 3 + 3 + 3 + 1 day segments but remains one logical TimerTask.
| Entry point | Timer behavior |
|---|---|
Core TaskHubGrpcWorker / TestOrchestrationWorker |
Three-day default |
| Azure-managed worker builder | Explicitly native timers; DTS supports long timers |
durable-functions worker / runOrchestrator |
Inherits the core three-day default |
Core TaskHubGrpcWorker({ maximumTimerIntervalMs }) and
TestOrchestrationWorker(backend, { maximumTimerIntervalMs }) accept the Python-equivalent
interval override in milliseconds. Omit it for three days; null, zero, or negative values
disable segmentation. Values must be finite. Python's timedelta supports microseconds;
JavaScript Date supports milliseconds, so positive fractions are rounded up to whole milliseconds.
Functions exposes no timer configuration and also segments when connected to DTS, as in Python;
there is no backend detection. Native in-memory timers still have Node.js's approximately
24.9-day timeout limit when segmentation is disabled.
Cancellation change: timer.cancel() now returns true on first cancellation and false
when already terminal. It removes the current segment, marks the timer canceled and complete
(isCanceled, isComplete, isCompleted), and notifies its parent; cancellation is not failure.
timer.getResult() and timer.result throw TaskCancelledError after cancellation. For timers,
result now aliases getResult() even while pending or failed. A canceled timer can win whenAny;
inspect isCanceled before reading its result. whenAll counts cancellation as terminal and
propagates the error when collecting final child results, which can throw from cancel() or
a sibling's completion callback. Do not yield a canceled timer expecting success.
If that callback throws, do not catch it and reuse the whenAll group or its parents:
the group can already be marked complete without a result and without notifying its parent.
Like Python, getResult() rejects this uninitialized result instead of treating it as success.
Parent notification is not resumed after the callback exception.
Custom Task subclasses now have their completed getResult() accessor called on each yield
instead of reading the raw result field. Accessors must be replay-safe; thrown errors fail execution.
Rollout: both the core default and cancellation semantics intentionally change to match Python. Existing single native timer histories replay at their recorded final deadline, but changed cancellation branching can affect replay. Avoid mixed versions; drain affected instances or use a new task hub before changing intervals, rolling back, or switching providers.
The following npm packages are available for download.
| Name | Latest version | Description |
|---|---|---|
| Core SDK | Core Durable Task SDK for JavaScript/TypeScript. | |
| AzureManaged SDK | Azure-managed Durable Task Scheduler support for the Durable Task JavaScript SDK. |
- Node.js 22 or higher
- An Azure Durable Task Scheduler instance, or the DTS Emulator for local development
This SDK can be used with the Durable Task Scheduler, a managed backend for running durable orchestrations in Azure.
To get started, install the npm packages:
npm install @microsoft/durabletask-js @microsoft/durabletask-js-azuremanagedYou can then use the following code to define a simple "Hello, cities" durable orchestration.
import { ActivityContext, OrchestrationContext, TOrchestrator } from "@microsoft/durabletask-js";
import { createAzureManagedClient, createAzureManagedWorkerBuilder } from "@microsoft/durabletask-js-azuremanaged";
// Define an activity function
const sayHello = async (_: ActivityContext, name: string): Promise<string> => {
return `Hello, ${name}!`;
};
// Define an orchestrator function
const helloCities: TOrchestrator = async function* (ctx: OrchestrationContext): any {
const result1 = yield ctx.callActivity(sayHello, "Tokyo");
const result2 = yield ctx.callActivity(sayHello, "London");
const result3 = yield ctx.callActivity(sayHello, "Seattle");
return [result1, result2, result3];
};
// Create client and worker using a connection string
const connectionString = process.env.DURABLE_TASK_SCHEDULER_CONNECTION_STRING!;
const client = createAzureManagedClient(connectionString);
const worker = createAzureManagedWorkerBuilder(connectionString)
.addOrchestrator(helloCities)
.addActivity(sayHello)
.build();
// Start the worker and schedule an orchestration
await worker.start();
const id = await client.scheduleNewOrchestration(helloCities);
const state = await client.waitForOrchestrationCompletion(id, true, 60);
console.log(`Result: ${state?.serializedOutput}`);TaskHubGrpcClient.waitForOrchestrationStart() and waitForOrchestrationCompletion() accept
an optional fourth AbortSignal argument:
const controller = new AbortController();
const waiting = client.waitForOrchestrationCompletion(id, true, 60, controller.signal);
controller.abort(); // Cancel this wait, not the orchestration.
await waiting; // Rejects with controller.signal.reason (an AbortError by default).The timeout is in seconds (default: 60) and includes metadata generation and all retry delays.
Timeouts reject with TimeoutError and cancel the pending RPC. Completion waits recover from
server DEADLINE_EXCEEDED responses with backoff, without resetting that total timeout;
start waits and other errors are not retried by this wait logic. A later wait can still observe
the orchestration's result after a previous wait was cancelled or timed out.
Core workers can send independent orchestration, activity, and entity concurrency hints to the backend:
const worker = new TaskHubGrpcWorker({
concurrency: {
maximumConcurrentOrchestrationWorkItems: 10,
maximumConcurrentActivityWorkItems: 20,
maximumConcurrentEntityWorkItems: 5,
},
});The worker does not locally throttle handlers, and the backend may have more than the requested
number of work items in flight or prefetched. Current Azure DTS versions treat 0 as no limit, so
do not use 0 to disable a work-item kind.
Omitted values default to 100 times os.availableParallelism(). Values must be non-negative
safe integers; 0 is forwarded unchanged. Values above the protocol's signed 32-bit range
are capped at 2147483647 on the wire. The same three hints are sent on initial and
reconnected work-item streams.
You can find more samples in the examples/azure-managed directory.
An activity can return 42 but fail to report that result because of a transient gRPC error.
Workers retry the same completion response and token instead of running the activity again.
If a resend is accepted, the backend can advance the workflow using the saved result.
This also applies to orchestration and entity responses, including version-rejection responses.
The policy follows the .NET worker: up to ten SDK sends for UNAVAILABLE, UNKNOWN,
DEADLINE_EXCEEDED, or INTERNAL, with backoff starting at 200 ms, doubling to a 15-second
cap before adding 0-20% jitter. Permanent errors and exhausted attempts use the existing
error logs. Configured gRPC transport retries remain enabled, so ten SDK sends can involve
more than ten network attempts.
stop() cancels all response RPCs, including the initial send, and retry backoff using
the worker run's signal captured when the work item was dispatched. This applies equally
to inline and streamed orchestrations, activities, entities, and version-failure/rejection
responses. Work finishing after stop cannot send its first response, even after a restart.
User code and metadata generation are not canceled; if metadata finishes after stop,
the response RPC is not started. Channel retirement and backend lock durations are unchanged.
Retries do not guarantee connection recovery, acceptance of expired tokens, or exactly-once execution.
Set the top-level dedupeStatuses start option when an instance ID may be reused. The list
contains the existing runtime statuses that must continue to produce an
OrchestrationAlreadyExistsError;
instances in every other supported runtime status are atomically replaced:
import { OrchestrationStatus } from "@microsoft/durabletask-js";
await client.scheduleNewOrchestration(helloCities, undefined, {
instanceId: "daily-greeting",
dedupeStatuses: [OrchestrationStatus.RUNNING, OrchestrationStatus.PENDING],
});For TaskHubGrpcClient, omitting dedupeStatuses preserves the backend's default duplicate-ID
behavior; passing [] makes every supported runtime status replaceable. The in-memory
TestOrchestrationClient mirrors the .NET shim, where omission also makes all statuses reusable.
ValidDedupeStatuses exports the seven supported statuses. The transient CONTINUED_AS_NEW
status is not replaceable. A list containing TERMINATED must also contain RUNNING, PENDING,
and SUSPENDED, because replacing a running instance first terminates it. The production client
forwards this validation to the backend and maps its INVALID_ARGUMENT response to TypeError;
the in-memory client validates it directly. The current shared protocol does not define a
no-op/IGNORE action: a matching dedupe status is an error, while a non-matching status is replaced.
The following orchestration patterns are supported.
The getting-started example above demonstrates function chaining, where an orchestration calls a sequence of activities one after another. You can find the full sample at examples/hello-world/activity-sequence.ts.
An orchestration can fan-out a dynamic number of function calls in parallel and then fan-in the results:
import { whenAll } from "@microsoft/durabletask-js";
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any {
const workItems = yield ctx.callActivity(getWorkItems);
const tasks = [];
for (const item of workItems) {
tasks.push(ctx.callActivity(processWorkItem, item));
}
const results: number[] = yield whenAll(tasks);
return results.reduce((sum, val) => sum + val, 0);
};You can find the full sample at examples/hello-world/fanout-fanin.ts.
An orchestration can wait for external events, such as a human approval, with optional timeout handling:
import { whenAny } from "@microsoft/durabletask-js";
const purchaseOrderWorkflow: TOrchestrator = async function* (ctx: OrchestrationContext, order: Order): any {
// Orders under $1000 are auto-approved
if (order.cost < 1000) {
return "Auto-approved";
}
// Orders of $1000 or more require manager approval
yield ctx.callActivity(sendApprovalRequest, order);
// Approvals must be received within 24 hours or they will be canceled
const approvalEvent = ctx.waitForExternalEvent("approval_received");
const timeoutEvent = ctx.createTimer(24 * 60 * 60);
const winner = yield whenAny([approvalEvent, timeoutEvent]);
if (winner == timeoutEvent) {
return "Cancelled";
}
yield ctx.callActivity(placeOrder, order);
const approvalDetails = approvalEvent.getResult();
return `Approved by ${approvalDetails.approver}`;
};You can find the full sample at examples/hello-world/human_interaction.ts.
Long-running orchestrations can restart with fresh history and optionally move to a new orchestration version:
const eternalOrchestrator: TOrchestrator = async function* (
ctx: OrchestrationContext,
iteration: number,
): any {
yield ctx.callActivity(processIteration, iteration);
ctx.continueAsNew(iteration + 1, true, "2.0.0");
};The second argument controls whether unprocessed external events carry over. The optional third
argument becomes the restarted orchestration's ctx.version; omit it to retain the existing
continue-as-new behavior.
Durable entities provide a way to manage small pieces of state with a simple object-oriented programming model:
import { TaskEntity } from "@microsoft/durabletask-js";
interface CounterState {
value: number;
}
class CounterEntity extends TaskEntity<CounterState> {
add(amount: number): number {
this.state.value += amount;
return this.state.value;
}
get(): number {
return this.state.value;
}
reset(): void {
this.state.value = 0;
}
protected initializeState(): CounterState {
return { value: 0 };
}
}
// Register with the worker
worker.addNamedEntity("Counter", () => new CounterEntity());You can find the full entity samples at examples/entity-counter and examples/entity-orchestration.
This project utilizes protobuf definitions from durabletask-protobuf. To download the latest proto files, run:
npm run download-protoThis will download the proto files to internal/durabletask-protobuf/protos/. Once the proto files are available, the corresponding TypeScript source code can be regenerated using:
npm run generate-grpcThis project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft's Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies.