Edit

Migrate your Durable Functions app to version 4 of the Node.js programming model

This guide covers the Durable Functions-specific changes needed when upgrading to the v4 Node.js programming model. To create a new v4 app instead, see the quickstarts for JavaScript and TypeScript.

Important

Complete the general Node.js v4 upgrade guide first. This article covers only the extra Durable Functions-specific changes.

Migration checklist

Use the following checklist to track your progress through each migration step:

Step Section
1. Verify prerequisites Prerequisites
2. Upgrade the npm package Upgrade the durable-functions npm package
3. Replace function.json with code-based registration Register your Durable Functions triggers
4. Register durable client in code Register your Durable Client input binding
5. Update client API calls Update your Durable Client API calls
6. Update callHttp calls Update calls to the callHttp API
7. (TypeScript) Use new exported types Use new exported types for stronger type safety

Prerequisites

Upgrade the durable-functions npm package

The programming model version and the durable-functions package version are different:

Programming model durable-functions package
v3 2.x
v4 3.x

Upgrade to the v3.x package:

npm install durable-functions

Register your Durable Functions triggers

In the v4 model, you no longer declare triggers and bindings in a separate function.json file. Instead, register your Durable Functions triggers directly in code using the df.app namespace. After migrating each function, delete its function.json file.

Function type Registration method
Orchestration df.app.orchestration()
Entity df.app.entity()
Activity df.app.activity()

The following examples show the migration pattern for each function type.

Orchestration

const df = require('durable-functions');

const activityName = 'helloActivity';

df.app.orchestration('durableOrchestrator', function* (context) {
    const outputs = [];
    outputs.push(yield context.df.callActivity(activityName, 'Tokyo'));
    outputs.push(yield context.df.callActivity(activityName, 'Seattle'));
    outputs.push(yield context.df.callActivity(activityName, 'Cairo'));

    return outputs;
});
import * as df from 'durable-functions';
import { OrchestrationContext, OrchestrationHandler } from 'durable-functions';

const activityName = 'hello';

const durableHello1Orchestrator: OrchestrationHandler = function* (context: OrchestrationContext) {
    const outputs = [];
    outputs.push(yield context.df.callActivity(activityName, 'Tokyo'));
    outputs.push(yield context.df.callActivity(activityName, 'Seattle'));
    outputs.push(yield context.df.callActivity(activityName, 'Cairo'));

    return outputs;
};
df.app.orchestration('durableOrchestrator', durableHello1Orchestrator);

Entity

const df = require('durable-functions');

df.app.entity('Counter', (context) => {
    const currentValue = context.df.getState(() => 0);
    switch (context.df.operationName) {
        case 'add':
            const amount = context.df.getInput();
            context.df.setState(currentValue + amount);
            break;
        case 'reset':
            context.df.setState(0);
            break;
        case 'get':
            context.df.return(currentValue);
            break;
    }
});
import * as df from 'durable-functions';
import { EntityContext, EntityHandler } from 'durable-functions';

const counterEntity: EntityHandler<number> = (context: EntityContext<number>) => {
    const currentValue: number = context.df.getState(() => 0);
    switch (context.df.operationName) {
        case 'add':
            const amount: number = context.df.getInput();
            context.df.setState(currentValue + amount);
            break;
        case 'reset':
            context.df.setState(0);
            break;
        case 'get':
            context.df.return(currentValue);
            break;
    }
};
df.app.entity('Counter', counterEntity);

Activity

const df = require('durable-functions');

df.app.activity('hello', {
    handler: (input) => {
        return `Hello, ${input}`;
    },
});
import * as df from 'durable-functions';
import { ActivityHandler } from "durable-functions";

const helloActivity: ActivityHandler = (input: string): string => {
    return `Hello, ${input}`;
};

df.app.activity('hello', { handler: helloActivity });

Register your Durable Client input binding

In the v4 model, registering secondary input bindings like durable clients is also done in code. Use input.durableClient() to register a durable client input binding, then use getClient() to retrieve the client instance. The following example uses an HTTP triggered function.

const { app } = require('@azure/functions');
const df = require('durable-functions');

app.http('durableHttpStart', {
    route: 'orchestrators/{orchestratorName}',
    extraInputs: [df.input.durableClient()],
    handler: async (_request, context) => {
        const client = df.getClient(context);
        // Use client in function body
    },
});
import { app, HttpHandler, HttpRequest, HttpResponse, InvocationContext } from '@azure/functions';
import * as df from 'durable-functions';

const durableHttpStart: HttpHandler = async (request: HttpRequest, context: InvocationContext): Promise<HttpResponse> => {
    const client = df.getClient(context);
    // Use client in function body
};

app.http('durableHttpStart', {
    route: 'orchestrators/{orchestratorName}',
    extraInputs: [df.input.durableClient()],
    handler: durableHttpStart,
});

Update your Durable Client API calls

Several APIs on DurableClient (renamed from DurableOrchestrationClient) now accept a single options object instead of multiple optional arguments. The most commonly affected APIs are startNew and getStatus; if you only use those APIs, you can skip the rest of the table. The following example shows the updated pattern:

const client = df.getClient(context)
const status = await client.getStatus('instanceId', {
    showHistory: false,
    showHistoryOutput: false,
    showInput: true
});
const client: DurableClient = df.getClient(context);
const status: DurableOrchestrationStatus = await client.getStatus('instanceId', {
    showHistory: false,
    showHistoryOutput: false,
    showInput: true
});

The following table lists all the affected APIs:

V3 model (durable-functions v2.x) V4 model (durable-functions v3.x)
getStatus(
    instanceId: string,
    showHistory?: boolean,
    showHistoryOutput?: boolean,
    showInput?: boolean
): Promise<DurableOrchestrationStatus>
getStatus(
    instanceId: string, 
    options?: GetStatusOptions
): Promise<DurableOrchestrationStatus>
getStatusBy(
    createdTimeFrom: Date | undefined,
    createdTimeTo: Date | undefined,
    runtimeStatus: OrchestrationRuntimeStatus[]
): Promise<DurableOrchestrationStatus[]>
getStatusBy(
    options: OrchestrationFilter
): Promise<DurableOrchestrationStatus[]>
purgeInstanceHistoryBy(
    createdTimeFrom: Date,
    createdTimeTo?: Date,
    runtimeStatus?: OrchestrationRuntimeStatus[]
): Promise<PurgeHistoryResult>
purgeInstanceHistoryBy(
    options: OrchestrationFilter
): Promise<PurgeHistoryResult>
raiseEvent(
    instanceId: string,
    eventName: string,
    eventData: unknown,
    taskHubName?: string,
    connectionName?: string
): Promise<void>
raiseEvent(
    instanceId: string,
    eventName: string,
    eventData: unknown,
    options?: TaskHubOptions
): Promise<void>
readEntityState<T>(
    entityId: EntityId,
    taskHubName?: string,
    connectionName?: string
): Promise<EntityStateResponse<T>>
readEntityState<T>(
    entityId: EntityId,
    options?: TaskHubOptions
): Promise<EntityStateResponse<T>>
rewind(
    instanceId: string,
    reason: string,
    taskHubName?: string,
    connectionName?: string
): Promise<void>`
rewind(
    instanceId: string, 
    reason: string, 
    options?: TaskHubOptions
): Promise<void>
signalEntity(
    entityId: EntityId,
    operationName?: string,
    operationContent?: unknown,
    taskHubName?: string,
    connectionName?: string
): Promise<void>
signalEntity(
    entityId: EntityId, 
    operationName?: string,
    operationContent?: unknown,
    options?: TaskHubOptions
): Promise<void>
startNew(
    orchestratorFunctionName: string,
    instanceId?: string,
    input?: unknown
): Promise<string>
startNew(
    orchestratorFunctionName: string, 
    options?: StartNewOptions
): Promise<string>;
waitForCompletionOrCreateCheckStatusResponse(
    request: HttpRequest,
    instanceId: string,
    timeoutInMilliseconds?: number,
    retryIntervalInMilliseconds?: number
): Promise<HttpResponse>;
waitForCompletionOrCreateCheckStatusResponse(
    request: HttpRequest,
    instanceId: string,
    waitOptions?: WaitForCompletionOptions
): Promise<HttpResponse>;

Update calls to the callHttp API

In v3.x of durable-functions, the callHttp() API was updated with the following changes:

Change Details
Arguments → options object All arguments are now passed as a single options object, similar to Express.
uriurl Renamed for consistency.
contentbody Renamed for consistency.
asynchronousPatternEnabledenablePolling Renamed for clarity.

If your orchestrations use callHttp, update the calls to the new syntax:

const restartResponse = yield context.df.callHttp({
    method: "POST",
    url: `https://example.com`,
    body: "body",
    enablePolling: false
});
const restartResponse = yield context.df.callHttp({
    method: "POST",
    url: `https://example.com`,
    body: "body",
    enablePolling: false
});

Use new exported types for stronger type safety

The durable-functions package now exposes new types that weren't previously exported. These types allow you to more strongly type your functions and provide stronger type safety for your orchestrations, entities, and activities. They also improve IntelliSense for authoring these functions.

The following list includes some of the new exported types:

  • OrchestrationHandler, and OrchestrationContext for orchestrations
  • EntityHandler and EntityContext for entities
  • ActivityHandler for activities
  • DurableClient class for client functions

Troubleshooting

"The orchestrator can’t execute without an OrchestratorStarted event"

If you see the following error, make sure you're running at least v4.25 of the Azure Functions Runtime or at least v4.0.5382 of Azure Functions Core Tools if running locally.

Exception: The orchestrator can not execute without an OrchestratorStarted event.
Stack: TypeError: The orchestrator can not execute without an OrchestratorStarted event.

Functions not discovered after migration

If your functions aren't appearing after migration, verify that:

  • You deleted or renamed the old function.json files. Leftover function.json files can conflict with code-based registrations.
  • Your df.app.orchestration(), df.app.entity(), and df.app.activity() calls are being executed at startup (for example, in a file imported by your main entry point).

Other issues

For other issues, file a bug report in the azure-functions-durable-js GitHub repo.

Next steps