Node.js/TypeScript SDK for the Duroxide durable execution runtime. Write reliable, long-running workflows in JavaScript using generator functions — backed by a Rust runtime that handles persistence, replay, and fault tolerance.
See CHANGELOG.md for release notes.
- Durable orchestrations — generator-based workflows that survive process restarts
- Automatic replay — the Rust runtime replays history on restart, your code picks up where it left off
- Activities — async functions for side effects (API calls, DB writes, etc.)
- Timers — durable delays that persist across restarts
- Sub-orchestrations — compose workflows from smaller workflows
- External events — pause workflows and wait for signals
- Fan-out/fan-in — run tasks in parallel with
ctx.all()(supports all task types) - Race conditions — wait for the first of multiple tasks with
ctx.race()(supports all task types) - Cooperative cancellation — activities detect when they're no longer needed via
ctx.isCancelled() - Activity client access — activities can start new orchestrations via
ctx.getClient() - Custom status — set orchestration progress visible to external clients via
ctx.setCustomStatus() - Event queues — persistent FIFO message passing with
ctx.dequeueEvent()andclient.enqueueEvent() - Continue-as-new — restart orchestrations with fresh history for eternal workflows
- Structured tracing — orchestration and activity logs route through Rust's
tracingcrate - Runtime metrics —
metricsSnapshot()for orchestration/activity counters - Per-instance orchestration stats — inspect history size, pending queue carry-forward, and KV usage with
client.getOrchestrationStats() - SQLite & PostgreSQL — pluggable storage backends
- KV store — durable per-instance key-value state with snapshots and pruning via
ctx.getKvAllValues()/client.getKvAllValues()
npm install duroxidePrebuilt native packages are published for:
| OS | Architecture | Native package |
|---|---|---|
| macOS | arm64 | duroxide-darwin-arm64 |
| macOS | x64 | duroxide-darwin-x64 |
| Linux glibc | arm64 | duroxide-linux-arm64-gnu |
| Linux glibc | x64 | duroxide-linux-x64-gnu |
| Windows | x64 | duroxide-windows-x64 |
const{ SqliteProvider, Client, Runtime }=require('duroxide');asyncfunctionmain(){// 1. Open a storage backendconstprovider=awaitSqliteProvider.open('sqlite:myapp.db');constclient=newClient(provider);construntime=newRuntime(provider);// 2. Register activities (async functions with side effects)runtime.registerActivity('Greet',async(ctx,name)=>{ctx.traceInfo(`greeting ${name}`);return`Hello, ${name}!`;});// 3. Register orchestrations (generator functions)runtime.registerOrchestration('GreetWorkflow',function*(ctx,input){constgreeting=yieldctx.scheduleActivity('Greet',input.name);ctx.traceInfo(`got: ${greeting}`);returngreeting;});// 4. Start the runtimeawaitruntime.start();// 5. Start an orchestration and wait for itawaitclient.startOrchestration('greet-1','GreetWorkflow',{name: 'World'});constresult=awaitclient.waitForOrchestration('greet-1');console.log(result.output);// "Hello, World!"awaitruntime.shutdown();}main();Duroxide uses function* generators instead of async function for orchestrations. This is a deliberate design choice — see Architecture for the full explanation. The short version: generators give Rust full control over when and how each step executes, which is essential for deterministic replay.
// ✅ Orchestrations use yieldruntime.registerOrchestration('MyWorkflow',function*(ctx,input){constresult=yieldctx.scheduleActivity('DoWork',input);returnresult;});// ✅ Activities use async/await (normal async functions)runtime.registerActivity('DoWork',async(ctx,input)=>{constdata=awaitfetch(`https://api.example.com/${input}`);returndata;});All scheduling methods return descriptors that must be yielded:
| Method | Description |
|---|---|
yield ctx.scheduleActivity(name, input) | Run an activity |
yield ctx.scheduleActivityWithRetry(name, input, retryPolicy) | Run with retry |
yield ctx.scheduleTimer(delayMs) | Durable delay |
yield ctx.waitForEvent(eventName) | Wait for external signal |
yield ctx.scheduleSubOrchestration(name, input) | Run child workflow (await result) |
yield ctx.scheduleSubOrchestrationWithId(name, id, input) | Child with explicit ID |
yield ctx.startOrchestration(name, id, input) | Fire-and-forget orchestration |
yield ctx.all([task1, task2, ...]) | Parallel execution (like Promise.all) |
yield ctx.race(task1, task2) | First-to-complete (like Promise.race) |
yield ctx.utcNow() | Deterministic timestamp |
yield ctx.newGuid() | Deterministic GUID |
yield ctx.dequeueEvent(queueName) | Dequeue from persistent FIFO mailbox |
yield ctx.scheduleActivityWithRetryOnSession(name, input, retry, sessionId) | Retry with session affinity |
yield ctx.continueAsNew(newInput) | Restart with fresh history |
ctx.setValue(key, value) | Set a durable KV entry (no yield) |
ctx.getValue(key) | Read a KV entry for the current instance (no yield) |
ctx.clearValue(key) | Remove a single KV entry (no yield) |
ctx.clearAllValues() | Remove all KV entries (no yield) |
ctx.getKvAllValues() | Snapshot all KV entries for the current instance |
ctx.getKvAllKeys() | List all KV keys for the current instance |
ctx.getKvLength() | Count KV entries for the current instance |
ctx.pruneKvValuesUpdatedBefore(cutoffMs) | Remove persisted KV entries older than a cutoff |
yield ctx.getValueFromInstance(instanceId, key) | Read another instance's KV entry |
Tracing methods are fire-and-forget (no yield needed):
| Method | Description |
|---|---|
ctx.traceInfo(message) | INFO log (suppressed during replay) |
ctx.traceWarn(message) | WARN log |
ctx.traceError(message) | ERROR log |
ctx.traceDebug(message) | DEBUG log |
ctx.setCustomStatus(status) | Set progress visible to clients |
ctx.resetCustomStatus() | Clear custom status |
constprovider=awaitSqliteProvider.open('sqlite:path/to/db.db');// or in-memory:constprovider=awaitSqliteProvider.inMemory();constprovider=awaitPostgresProvider.connectWithSchema('postgresql://user:pass@host:5432/db','my_schema');For Azure Database for PostgreSQL Flexible Server, use Entra ID token authentication instead of a password:
constprovider=awaitPostgresProvider.connectWithEntra('myserver.postgres.database.azure.com',5432,'mydb','my-entra-user@contoso.onmicrosoft.com');// With a custom schema and options:constprovider=awaitPostgresProvider.connectWithSchemaAndEntra('myserver.postgres.database.azure.com',5432,'mydb','my-entra-user@contoso.onmicrosoft.com','my_schema',{maxConnections: 20,acquireTimeoutMs: 45_000});Credentials are resolved automatically via the default chain:
WorkloadIdentityCredential(AKS Workload Identity, if env vars present)ManagedIdentityCredential(Azure VMs, Container Apps, etc.)DeveloperToolsCredential(local development:az login)
All connections use TLS (PgSslMode::VerifyFull). See PostgresEntraOptions for tunable options.
Duroxide uses Rust's tracing crate. Control verbosity with RUST_LOG:
RUST_LOG=info node app.js # INFO and above
RUST_LOG=duroxide=debug node app.js # DEBUG for duroxide only
RUST_LOG=duroxide::activity=info node app.js # Activity traces onlyThe Client class includes a management API for inspecting and managing orchestration instances:
constclient=newClient(provider);// Event queuesawaitclient.enqueueEvent(instanceId,'queueName',JSON.stringify(data));// Custom status pollingconstchange=awaitclient.waitForStatusChange(instanceId,0,100,30000);// change: { customStatus, customStatusVersion } or null on timeout// Instance managementconstinstances=awaitclient.listAllInstances();constinfo=awaitclient.getInstanceInfo(instanceId);consttree=awaitclient.getInstanceTree(instanceId);awaitclient.deleteInstance(instanceId,false);// Execution history with full event dataconstexecutions=awaitclient.listExecutions(instanceId);constevents=awaitclient.readExecutionHistory(instanceId,executions[0]);for(consteventofevents){console.log(event.kind,event.data);// event.kind: "OrchestrationStarted" | "ActivityScheduled" | "ActivityCompleted" | ...// event.data: JSON string with event-specific content (result, input, error, etc.)}// Metricsconstmetrics=awaitclient.getSystemMetrics();conststats=awaitclient.getOrchestrationStats(instanceId);constdepths=awaitclient.getQueueDepths();getOrchestrationStats() returns null for a missing instance, otherwise:
{historyEventCount: 2,historySizeBytes: 184,queuePendingCount: 0,kvUserKeyCount: 1,kvTotalValueBytes: 11}// Read a KV entry from an orchestration instanceconstvalue=awaitclient.getValue(instanceId,'myKey');// Snapshot every KV entry for an orchestration instanceconstallValues=awaitclient.getKvAllValues(instanceId);// Wait until a KV key is set (with timeout in ms)constreadyValue=awaitclient.waitForValue(instanceId,'myKey',30000);// Inside an orchestration, inspect or prune the local KV snapshotconstsnapshot=ctx.getKvAllValues();constkeys=ctx.getKvAllKeys();constsize=ctx.getKvLength();constremoved=ctx.pruneKvValuesUpdatedBefore(cutoffMs);MAX_KV_KEYS is now 150, and MAX_KV_VALUE_BYTES is now 65536.
- Architecture — how the Rust/JS interop works, yield vs await, limitations
- User Guide — patterns, recipes, and best practices
Requires PostgreSQL (see .env.example):
npm test# e2e tests (25 PG + 1 SQLite smoketest)
npm run test:races # Race/join composition tests (7 tests)
npm run test:admin # Admin API tests (14 tests)
npm run test:scenarios # Scenario tests (6 tests)
npm run test:all # Everything (52 tests)Use GitHub Issues for bug reports and feature requests. Do not report security vulnerabilities through public GitHub issues; follow the instructions in SECURITY.md instead.
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 questions or comments.
Microsoft takes the security of our software products and services seriously. Please do not report security vulnerabilities through public GitHub issues. See SECURITY.md for security reporting instructions.
duroxide-node does not send telemetry to Microsoft. Applications may configure their own logging or metrics exporters; those signals are controlled by the application owner.
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 is subject to those third-party policies.
MIT License - see LICENSE for details.