Long Workflow Needed? Create a Durable Function

Published on:

CloudTrips needs a workflow that can perform work, wait, and continue without keeping one function execution alive. Durable Functions extends Azure Functions with persisted workflow state, checkpoints, retries, and recovery.

HTTP starter → starts and returns an instance ID
Orchestrator → defines the workflow order
Activity     → performs one unit of real work
Durable timer → persists the wait without occupying a worker

An ordinary function should finish one execution quickly. A durable orchestration can wait for minutes, days, or longer because Azure records its history in storage and reconstructs its state when it resumes.

Create the Function App

Search for Function App, select Create, choose Flex Consumption, and select Select. Enter:

Subscription: CloudTrips TEST
Resource group: rg-cloudtrips-durable-test-weu
Function App name: func-cloudtrips-durable-dmytro-test-weu
Region: West Europe
Runtime stack: Node.js
Version: 22 LTS
Instance size: 2048 MB

Use the newest Node.js LTS version offered by the portal if 22 LTS is no longer listed. On Storage, create the default host storage account. Durable Functions uses storage to persist orchestration instances, history, messages, and timers.

Keep Application Insights enabled under Monitoring. Under Authentication, select Managed identity for all resources. Accept the remaining defaults and select Review + create > Create.

The app name must be globally unique. Flex Consumption scales to zero but the Storage account and Application Insights can still incur small charges.

Create Function App page showing the standalone Durable Functions host on Flex Consumption

Grant the Durable Storage Roles

Selecting managed identity avoids a storage key, but the identity still needs data-plane permission to create and use the Durable task hub. Open the Function App’s Settings > Identity page, select User assigned, and note the managed identity selected for the app.

Open the Function App’s host Storage account, select Access control (IAM), and assign all three roles to that managed identity:

Storage Blob Data Contributor
Storage Queue Data Contributor
Storage Table Data Contributor

Durable Functions uses blobs for leases and large messages, queues to schedule orchestrator and activity work, and tables for instance state and execution history. Subscription Owner or Contributor is not a substitute because those are management-plane roles and do not grant Storage data access.

If a role already exists for the identity, do not add a duplicate. Wait several minutes for RBAC propagation before starting the first orchestration.

Storage account IAM showing the Blob, Queue, and Table Data Contributor roles assigned to the Function App managed identity

Without the Table role, the HTTP starter can fail with DurableTaskStorageException while calling Table.CreateIfNotExistsAsync.

Create the Local Project

Create the project folder:

mkdir -p cloudtrips-durable/src/functions
cd cloudtrips-durable

Create and validate host.json:

cat > host.json <<'EOF'
{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.*, 5.0.0)"
  }
}
EOF

jq . host.json

The extension bundle supplies the Durable Functions runtime bindings. jq must print valid JSON; do not deploy an empty host.json.

Create package.json:

cat > package.json <<'EOF'
{
  "name": "cloudtrips-durable",
  "version": "1.0.0",
  "main": "src/functions/*.js",
  "scripts": {
    "start": "func start"
  },
  "dependencies": {
    "@azure/functions": "^4.7.2",
    "durable-functions": "^3.3.0"
  }
}
EOF

jq . package.json

The Node.js v4 programming model registers functions in code. The durable-functions 3.x package adds the orchestrator, activity, and durable client APIs.

Add the Durable Workflow

Create src/functions/tripWorkflow.js:

cat > src/functions/tripWorkflow.js <<'EOF'
const { app } = require('@azure/functions');
const df = require('durable-functions');

df.app.activity('recordTripStage', {
  handler: (stage, context) => {
    context.log(`Processing stage: ${stage}`);
    return `${stage} completed`;
  }
});

df.app.orchestration('tripOrchestrator', function* (context) {
  const results = [];

  results.push(
    yield context.df.callActivity('recordTripStage', 'Reservation requested')
  );

  const resumeAt = new Date(context.df.currentUtcDateTime);
  resumeAt.setMinutes(resumeAt.getMinutes() + 2);
  yield context.df.createTimer(resumeAt);

  results.push(
    yield context.df.callActivity('recordTripStage', 'Reservation confirmed')
  );

  return results;
});

app.http('startTripWorkflow', {
  methods: ['POST'],
  authLevel: 'anonymous',
  route: 'workflows/trips',
  extraInputs: [df.input.durableClient()],
  handler: async (request, context) => {
    const client = df.getClient(context);
    const instanceId = await client.startNew('tripOrchestrator');

    context.log(`Started orchestration ${instanceId}`);
    return client.createCheckStatusResponse(request, instanceId);
  }
});
EOF

The orchestrator is a generator function because Durable Functions pauses at each yielded durable task and can later replay the workflow history. Keep orchestrator code deterministic: use context.df.currentUtcDateTime instead of new Date() for workflow decisions, and perform network, database, or random work inside activities.

The two-minute durable timer does not hold a worker or bill continuously for an idle execution. Azure checkpoints the orchestration, wakes it after the timer, and schedules the second activity. Two minutes leaves enough time to query the workflow while it is still Running.

Deploy the Workflow

Package the project contents:

zip -r cloudtrips-durable.zip host.json package.json src

Deploy with a remote dependency build:

az functionapp deployment source config-zip \
  --resource-group rg-cloudtrips-durable-test-weu \
  --name func-cloudtrips-durable-dmytro-test-weu \
  --src cloudtrips-durable.zip \
  --build-remote true

Wait for deployment to complete, then open Functions. The list should show:

startTripWorkflow
tripOrchestrator
recordTripStage

Function App showing the HTTP starter, orchestrator, and activity functions

Start an Orchestration

Open startTripWorkflow, select Get Function URL, and copy the URL. Start a workflow and save Azure’s management response:

curl --fail --show-error \
  --request POST \
  '<FUNCTION_URL>' \
  | tee durable-start.json

The HTTP starter returns 202 Accepted with a unique id and management URLs. It does not keep the original HTTP request open for the entire workflow.

Extract the status URL:

jq -r '.statusQueryGetUri' durable-start.json

Copy the complete URL printed by this command. It contains the instance ID, task-hub details, and authorization code required by the management endpoint.

Terminal showing the orchestration instance ID and status management URLs

Poll the Persisted Status

Query the returned status URL:

curl --fail --show-error \
  '<PASTE_THE_COMPLETE_statusQueryGetUri_HERE>' \
  | jq '{instanceId, runtimeStatus, output}'

Replace the entire placeholder, including the angle brackets, with the full URL copied from statusQueryGetUri. Do not shorten the URL or remove its query string.

During the durable timer, runtimeStatus should be Running and output is null. After approximately two minutes, repeat the command. Expected result:

{
  "instanceId": "<unique-instance-id>",
  "runtimeStatus": "Completed",
  "output": [
    "Reservation requested completed",
    "Reservation confirmed completed"
  ]
}

The instance ID lets Azure correlate every checkpoint and resume the correct workflow even after a host restart. Open the orchestrator’s Monitor view to inspect the completed invocation and Application Insights telemetry.

Completed Durable Functions orchestration showing both activity outputs

Clean Up

Delete the isolated resource group:

az group delete \
  --name rg-cloudtrips-durable-test-weu \
  --yes

Confirm that it is gone:

az group exists --name rg-cloudtrips-durable-test-weu

Expected result: false.