Durable Functions for Stateful Workflows
Orchestrate long-running workflows using Durable Functions orchestrator patterns (fan-out/fan-in, chaining, monitor), and understand how state is checkpointed.
Durable Functions for Stateful Workflows is a free Cloud & IT Cert Prep lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Cloud & IT Cert Prep learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Durable Functions?
Regular Azure Functions are stateless — each invocation runs independently with no memory of previous calls. Durable Functions extend Azure Functions with the ability to write stateful, long-running workflows using plain async/await code. The Durable Task Framework automatically checkpoints state to Azure Storage after each step, so workflows can survive server restarts, timeouts, or planned maintenance and resume exactly where they left off.
The Three Function Types
Durable Functions introduces three function types. An orchestrator function coordinates the overall workflow — it calls activity functions and waits for results using yield or await without actually blocking a thread. An activity function performs a single unit of work (call an API, write to a database) and is the only place side effects should occur. An entity function maintains small pieces of durable state (counters, flags) across calls.
// Client function (HTTP trigger) — starts the orchestration
module.exports = async function (context, req) {
const client = df.getClient(context);
const orderId = req.body.orderId;
const instanceId = await client.startNew('OrderOrchestrator', undefined, { orderId });
return client.createCheckStatusResponse(context.bindingData.req, instanceId);
};The Chaining Pattern
The chaining pattern runs activity functions sequentially, passing the output of one as the input to the next. The orchestrator awaits each activity in turn. If any activity fails, the workflow stops and can be restarted from the failed step. This is the simplest Durable Functions pattern and is suitable for workflows where each step depends on the result of the previous one, such as order processing pipelines.
// Orchestrator: chaining pattern
const df = require('durable-functions');
module.exports = df.orchestrator(function* (context) {
const orderId = context.df.getInput().orderId;
const validated = yield context.df.callActivity('ValidateOrder', orderId);
const charged = yield context.df.callActivity('ChargePayment', validated);
const shipped = yield context.df.callActivity('ShipOrder', charged);
return { status: 'shipped', trackingId: shipped.trackingId };
});Fan-Out / Fan-In Pattern
The fan-out/fan-in pattern launches multiple activity functions in parallel and waits for all of them to complete before continuing. The orchestrator starts all tasks simultaneously using callActivity without awaiting them, collects the task objects into an array, then yields on Task.all(). This is dramatically faster than sequential processing for independent work items like processing multiple files, calling multiple APIs, or batch operations.
// Orchestrator: fan-out / fan-in
module.exports = df.orchestrator(function* (context) {
const items = context.df.getInput().items;
// Fan-out: start all tasks in parallel
const tasks = items.map(item => context.df.callActivity('ProcessItem', item));
// Fan-in: wait for all tasks to complete
const results = yield context.df.Task.all(tasks);
return results;
});The Monitor Pattern
The monitor pattern polls an external system at intervals until a condition is met — similar to a polling loop but fully durable. The orchestrator calls an activity to check the status, waits a configurable interval using createTimer, then loops. Because the state is checkpointed to storage between each poll, the orchestrator does not consume compute resources while waiting, making it far more efficient than a sleeping timer-based approach.
// Orchestrator: monitor pattern (poll until job completes)
module.exports = df.orchestrator(function* (context) {
const jobId = context.df.getInput().jobId;
const expiry = new Date(context.df.currentUtcDateTime);
expiry.setHours(expiry.getHours() + 24); // 24-hour timeout
while (context.df.currentUtcDateTime < expiry) {
const status = yield context.df.callActivity('GetJobStatus', jobId);
if (status === 'completed') return { jobId, status };
if (status === 'failed') throw new Error('Job failed');
// Wait 30 seconds before next poll
const nextCheck = new Date(context.df.currentUtcDateTime);
nextCheck.setSeconds(nextCheck.getSeconds() + 30);
yield context.df.createTimer(nextCheck);
}
throw new Error('Workflow timed out');
});Human Interaction Pattern
The human interaction pattern pauses an orchestration and waits for an external event — such as a manager approval. The orchestrator yields on waitForExternalEvent, which can wait days or weeks without consuming compute. An external system (email approval link, mobile app, webhook) calls the Durable Functions HTTP API to raise the event, unblocking the orchestration. Combine with a timer for automatic timeout and escalation if no response arrives.
// Orchestrator: wait for human approval with timeout
module.exports = df.orchestrator(function* (context) {
const request = context.df.getInput();
yield context.df.callActivity('SendApprovalEmail', request);
const timeout = df.Task.createTimer(context, new Date(Date.now() + 48 * 3600 * 1000));
const approval = context.df.waitForExternalEvent('ApprovalResponse');
const winner = yield context.df.Task.any([approval, timeout]);
if (winner === approval) {
const approved = winner.result;
return approved ? 'Approved' : 'Rejected';
} else {
return 'Timed out — escalated';
}
});Orchestrator Constraints
Orchestrator functions have important constraints because they may be replayed from history multiple times to rebuild state. They must be deterministic — never use Date.now(), Math.random(), or make direct I/O calls. Instead, use context.df.currentUtcDateTime for timestamps and call activity functions for all I/O. Logging in the orchestrator body will produce duplicate log entries during replay; use activity functions for logging instead.
// WRONG — non-deterministic, will cause replay bugs
module.exports = df.orchestrator(function* (context) {
const now = new Date(); // Don't use Date()
const rand = Math.random(); // Don't use Math.random()
const data = await fetch('/api'); // Don't make HTTP calls directly
});
// CORRECT
module.exports = df.orchestrator(function* (context) {
const now = context.df.currentUtcDateTime; // OK
const data = yield context.df.callActivity('FetchData', null); // OK
});Managing Instances: Status and Termination
Each orchestration run has a unique instance ID you can use to query its status, send events, or terminate it. The Durable Functions HTTP management API provides endpoints for checking status (GET /instances/{id}), sending events (POST /instances/{id}/raiseEvent/{name}), and terminating (POST /instances/{id}/terminate). Use the Durable client binding in your functions to access these operations programmatically.
// Client function: check orchestration status
module.exports = async function (context, req) {
const client = df.getClient(context);
const instanceId = req.params.instanceId;
const status = await client.getStatus(instanceId, true, true, true);
return {
status: 200,
body: {
instanceId,
runtimeStatus: status.runtimeStatus,
customStatus: status.customStatus,
output: status.output
}
};
};Storage Backend and Performance
Durable Functions stores orchestration history, instance state, and inter-function message queues in an Azure Storage Account (or Azure SQL/Netherite backends for higher throughput). Every checkpoint writes to Azure Table Storage and Azure Queue Storage. For high-throughput scenarios (thousands of concurrent orchestrations), the Netherite storage backend uses Azure Event Hubs for dramatically better performance. Monitor the orchestration queue depth to detect bottlenecks.
// host.json: configure the Durable Task storage provider
{
'version': '2.0',
'extensions': {
'durableTask': {
'hubName': 'MyTaskHub',
'storageProvider': {
'type': 'azure',
'connectionStringName': 'AzureWebJobsStorage',
'controlQueueBatchSize': 32,
'maxQueuePollingInterval': '00:00:02'
}
}
}
}Error Handling and Retries
Activity functions can throw exceptions, which propagate back to the orchestrator as TaskFailedException. Use try-catch blocks in the orchestrator to handle failures gracefully. For transient errors, configure automatic retries with backoff using callActivityWithRetry, specifying maximum attempts, first retry interval, and backoff coefficient. This is the recommended pattern for activities that call external APIs or databases.
// Orchestrator: retry an activity with exponential backoff
module.exports = df.orchestrator(function* (context) {
const retryOptions = new df.RetryOptions(
5000, // firstRetryIntervalInMilliseconds
3 // maxNumberOfAttempts
);
retryOptions.backoffCoefficient = 2; // 5s, 10s, 20s
try {
const result = yield context.df.callActivityWithRetry(
'CallExternalAPI',
retryOptions,
context.df.getInput()
);
return result;
} catch (e) {
yield context.df.callActivity('SendFailureAlert', e.message);
throw e;
}
});Durable Entities
Durable Entities (entity functions) implement small pieces of durable state accessible by identity — similar to virtual actors. An entity has an ID and a state that persists between calls. You call operations on an entity from an orchestrator or client, and the entity processes them one at a time (serialised). Common uses include counters, approval state machines, rate limiters, and shopping carts — any scenario needing durable, updatable state without a database.
// Counter entity function
const df = require('durable-functions');
module.exports = df.entity(function (context) {
let count = context.df.getState(() => 0);
const operation = context.df.operationName;
if (operation === 'add') count += context.df.getInput();
if (operation === 'reset') count = 0;
if (operation === 'get') context.df.return(count);
context.df.setState(count);
});
// From orchestrator, increment counter entity
// const entityId = new df.EntityId('Counter', 'myCounter');
// yield context.df.callEntity(entityId, 'add', 1);Quick Check
Test your understanding of Microsoft Azure Fundamentals (AZ-900) concepts from this lesson.
Lesson Recap
In this lesson you learned: Durable Functions enable stateful, long-running workflows by checkpointing orchestrator state to Azure Storage, key patterns include chaining, fan-out/fan-in, monitor, and human interaction, and orchestrators must be deterministic — all I/O and non-deterministic calls must go through activity functions. Next up we explore Azure Logic Apps.
Frequently asked questions
Is the “Durable Functions for Stateful Workflows” lesson free?
Yes — the full text of “Durable Functions for Stateful Workflows” is free to read here on the web, and the Cloud & IT Cert Prep course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Cloud & IT Cert Prep course, upgrade to CoddyKit PRO.
What will I learn in “Durable Functions for Stateful Workflows”?
Orchestrate long-running workflows using Durable Functions orchestrator patterns (fan-out/fan-in, chaining, monitor), and understand how state is checkpointed. You practise Cloud & IT Cert Prep with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Cloud & IT Cert Prep?
No prior experience is required. Cloud & IT Cert Prep on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Durable Functions for Stateful Workflows” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Cloud & IT Cert Prep lesson?
Yes. Every Cloud & IT Cert Prep lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Azure Functions Triggers and Bindings
- Durable Functions for Stateful Workflows
- Azure Logic Apps
- Event Grid and Event-Driven Architecture