AZ-204Chapter 97 of 102Objective 1.1

Azure Functions Workflows: Orchestration Patterns

This chapter covers Azure Functions orchestration patterns using Durable Functions, a key compute topic for the AZ-204 exam. You will learn how to implement reliable, stateful workflows that coordinate multiple function executions, handle failures, and manage long-running processes. This area represents about 10-15% of the compute domain questions, typically appearing as scenario-based questions where you must choose the correct pattern (chaining, fan-out/fan-in, async HTTP APIs, etc.) or identify the correct code structure for a durable orchestration. Mastery of these patterns is essential for passing the exam and for building resilient serverless applications.

25 min read
Intermediate
Updated May 31, 2026

The Factory Assembly Line with Quality Control Stations

Imagine a car factory assembly line. The line itself is the orchestration — it defines the sequence of stations (functions) that a car chassis passes through. Each station performs a specific task: welding, painting, installing engine, etc. A central control system (the orchestrator) tracks each car's progress and ensures that the next station is ready. If a station fails (e.g., paint robot breaks), the control system can pause the line, reroute the car to a backup station, or send an alert. This is exactly how Durable Functions orchestration works: the orchestrator function defines the workflow, each activity function is a station, and the orchestration runtime ensures reliable execution, retries on failure, and tracks state. The car's VIN (vehicle identification number) is like the instance ID — uniquely identifying each running orchestration. The control system's log of completed steps is the orchestration history, allowing resumption from the last completed step if the system restarts. Without orchestration, each station would need to know what the previous station did, leading to tight coupling and fragile workflows. The assembly line decouples stations, allowing independent scaling and maintenance.

How It Actually Works

What Are Azure Functions Orchestration Patterns?

Azure Functions is a serverless compute service that lets you run event-driven code without managing infrastructure. However, a single function execution has limits: it must complete within a timeout (default 5 minutes for the Consumption plan, up to 10 minutes for Premium, and unlimited for Dedicated/ASE). For workflows that span hours, days, or involve multiple steps with conditional logic, a single function is insufficient. This is where Durable Functions come in. Durable Functions is an extension of Azure Functions that allows you to define stateful workflows (orchestrations) using code. The orchestrator function coordinates the execution of other functions (activity functions) and manages state, retries, and waiting for external events.

How Durable Functions Work Internally

Durable Functions rely on the Durable Task Framework (DTF), which uses Azure Storage (queues, tables, and blobs) to persist execution state. The core idea is that the orchestrator function is replayed on each execution. When an orchestrator function calls an activity, it yields control, and the runtime persists the call to a history table. On replay, the orchestrator reads the history and skips already-completed activities, ensuring deterministic behavior. This replay mechanism allows the orchestrator to be stateless in memory while maintaining durable state.

Key Components: - Orchestrator function: Defines the workflow using context object. Must be deterministic (no random, no I/O except via activity calls). - Activity function: Performs actual work (e.g., calling an API, processing a file). Can be non-deterministic. - Client function: Starts an orchestration instance (typically an HTTP-triggered function). - Orchestration instance: A single running workflow, identified by an InstanceId.

Storage Resources: - Task Hub: A logical container for storage resources. Named in host.json. All instances share the same task hub. - Control Queue: Contains messages that drive the orchestration (e.g., timer messages, activity responses). - Work-item Queue: Contains messages for activity tasks. - History Table: Stores orchestration history as rows (events). - Instance Table: Stores runtime status of instances. - Blob Leases: Used for partition management.

Execution Flow: 1. Client function starts an orchestration by calling starter.StartNewAsync("OrchestratorFunction", input). 2. The runtime creates an orchestration instance and enqueues an execution message to the control queue. 3. The orchestrator function is triggered. It executes until it calls an activity or waits. At that point, it yields, and the runtime persists the call to history. 4. The activity function is triggered via the work-item queue. It executes and returns a result to the control queue. 5. The orchestrator is replayed: it reads history, sees that the activity has completed, and continues from that point. 6. This continues until the orchestrator completes or is terminated.

Orchestration Patterns

#### 1. Function Chaining

Executes a sequence of activities in a specific order. Each activity's output is passed as input to the next. This is the simplest pattern.

[FunctionName("Chaining")]
public static async Task<object> Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var x = await context.CallActivityAsync<object>("F1", null);
    var y = await context.CallActivityAsync<object>("F2", x);
    var z = await context.CallActivityAsync<object>("F3", y);
    return z;
}

#### 2. Fan-Out/Fan-In

Executes multiple activities in parallel (fan-out) and then aggregates results (fan-in). Useful for batch processing.

[FunctionName("FanOutFanIn")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var tasks = new List<Task<int>>();
    var workBatch = await context.CallActivityAsync<List<string>>("GetWorkBatch", null);
    foreach (var workItem in workBatch)
    {
        tasks.Add(context.CallActivityAsync<int>("ProcessWorkItem", workItem));
    }
    await Task.WhenAll(tasks);
    var sum = tasks.Sum(t => t.Result);
    await context.CallActivityAsync("SendResult", sum);
}

#### 3. Async HTTP APIs

Long-running operations that return an HTTP 202 Accepted response with a status endpoint. The client polls the endpoint to check completion. This pattern solves the problem of HTTP timeouts for long-running processes.

[FunctionName("HttpStart")]
public static async Task<HttpResponseMessage> Run(
    [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestMessage req,
    [DurableClient] IDurableOrchestrationClient starter,
    ILogger log)
{
    string instanceId = await starter.StartNewAsync("MyOrchestration", null);
    return starter.CreateCheckStatusResponse(req, instanceId);
}

The response contains statusQueryGetUri, sendEventPostUri, terminatePostUri, etc. The client can call statusQueryGetUri to get status: Running, Completed, Failed, Terminated.

#### 4. Monitor

Repeatedly checks a condition until it is met. Uses durable timers to avoid infinite loops and to control polling frequency.

[FunctionName("Monitor")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context,
    ILogger log)
{
    int jobId = context.GetInput<int>();
    int pollingInterval = 15; // seconds
    DateTime expiry = context.CurrentUtcDateTime.AddHours(6);

    while (context.CurrentUtcDateTime < expiry)
    {
        var jobStatus = await context.CallActivityAsync<string>("GetJobStatus", jobId);
        if (jobStatus == "Completed")
        {
            await context.CallActivityAsync("SendAlert", $"Job {jobId} completed.");
            return;
        }
        var nextCheck = context.CurrentUtcDateTime.AddSeconds(pollingInterval);
        await context.CreateTimer(nextCheck, CancellationToken.None);
    }
    await context.CallActivityAsync("SendAlert", $"Job {jobId} timed out.");
}

#### 5. Human Interaction

Waits for an external event (e.g., approval) with a timeout. Uses WaitForExternalEvent and CreateTimer for race conditions.

[FunctionName("HumanInteraction")]
public static async Task Run(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    await context.CallActivityAsync("RequestApproval", null);
    using (var timeoutCts = new CancellationTokenSource())
    {
        DateTime due = context.CurrentUtcDateTime.AddHours(72);
        Task durableTimeout = context.CreateTimer(due, timeoutCts.Token);
        Task<bool> approvalEvent = context.WaitForExternalEvent<bool>("ApprovalEvent");
        if (approvalEvent == await Task.WhenAny(approvalEvent, durableTimeout))
        {
            timeoutCts.Cancel();
            if (approvalEvent.Result)
                await context.CallActivityAsync("ProcessApproval", null);
            else
                await context.CallActivityAsync("Escalate", null);
        }
        else
        {
            await context.CallActivityAsync("Escalate", null);
        }
    }
}

Configuration and Defaults

- host.json: Configure durableTask section. - hubName: Task hub name (default: TestHubName for local, AzureFunctionsHub for Azure). - storageProvider: Azure Storage connection string (default: AzureWebJobsStorage). - maxConcurrentActivityFunctions: Max concurrent activity functions (default: 10 for single VM, 10x number of partitions for scale). - maxConcurrentOrchestratorFunctions: Max concurrent orchestrator functions (default: 10 for single VM). - extendedSessionsEnabled: Enable session reuse (default: false). - extendedSessionIdleTimeoutInSeconds: Idle timeout for extended sessions (default: 30).

Timeouts:

Default orchestration timeout: 5 minutes (Consumption plan). Can be extended with context.CreateTimer.

Activity function timeout: 5 minutes (Consumption), 10 minutes (Premium), unlimited (Dedicated).

Orchestration instance history size limit: 50,000 events (default). Can be increased.

Interaction with Related Technologies

Azure Storage: Durable Functions uses Storage queues, tables, and blobs. Ensure storage account is in the same region to avoid latency.

Azure Event Grid: Can be used to trigger orchestrations or to notify external systems of orchestration events.

Azure Logic Apps: Similar orchestration capability but declarative (workflow designer). Logic Apps is better for integrating with many connectors; Durable Functions is better for custom code and complex logic.

Azure Service Bus: Can be used as a trigger for client functions or to send events to orchestrations.

Common Pitfalls

Non-deterministic orchestrator code: Using DateTime.Now, Guid.NewGuid(), or Random inside orchestrator causes replay issues. Use context.CurrentUtcDateTime, context.NewGuid(), and pass random values from activities.

Infinite replay loops: If orchestrator throws an exception before any durable call, it will retry indefinitely. Ensure at least one durable call (activity or timer) before throwing.

Large history: Too many events can slow down replay. Batch activities or use sub-orchestrations.

Instance ID collisions: Use unique instance IDs (e.g., GUID) to avoid overwriting existing instances.

Walk-Through

1

Create Durable Functions App

Start by creating an Azure Functions app with the Durable Functions extension. In Visual Studio, use the 'Durable Functions Orchestration' template. This creates three files: `HttpStart` (HTTP trigger client), `Orchestrator` (orchestrator function), and `SayHello` (activity function). The project includes the `Microsoft.Azure.WebJobs.Extensions.DurableTask` NuGet package. Ensure the `host.json` has the `durableTask` section configured. For local development, the storage emulator is used; for Azure, set `AzureWebJobsStorage` to a real storage account.

2

Define Orchestrator Function

Write the orchestrator function using `IDurableOrchestrationContext`. It must be deterministic: no direct I/O, no random, no DateTime.Now. Use `context.CallActivityAsync` to invoke activities. The orchestrator can use `context.CallSubOrchestratorAsync` to call another orchestrator. The function returns a result or void. The runtime replays the function on each event, so the code must be idempotent. Use `context.CurrentUtcDateTime` for time, `context.NewGuid()` for unique IDs.

3

Define Activity Functions

Activity functions are regular Azure Functions with `[ActivityTrigger]` binding. They can perform any I/O or computation. They receive input from the orchestrator and return output. Activities can be non-deterministic. They are automatically retried on failure based on the `RetryOptions` passed in `CallActivityWithRetryAsync`. Default retry: 1-second interval, 10 attempts, exponential backoff. Activities can be long-running but are limited by the function timeout.

4

Start Orchestration Instance

A client function (e.g., HTTP trigger) uses `IDurableOrchestrationClient` (injected via `[DurableClient]` attribute) to start an instance. Call `starter.StartNewAsync("OrchestratorFunction", input)`. This returns an instance ID. The client can return a `CreateCheckStatusResponse` that includes URIs for status, send event, terminate, and purge. The client can also use `starter.RaiseEventAsync` to send events to running orchestrations.

5

Monitor and Manage Orchestration

Use the status URI to poll instance state. States: `Pending`, `Running`, `Completed`, `ContinuedAsNew`, `Failed`, `Terminated`. The orchestration history is stored in the History table. You can terminate an instance with `starter.TerminateAsync`. Purge history with `starter.PurgeInstanceHistoryAsync`. For long-running orchestrations, use `ContinueAsNew` to reset history and avoid hitting the 50k event limit. This creates a new instance with the same ID.

What This Looks Like on the Job

Enterprise Scenario 3: IoT Device Firmware Update

A smart building company uses Durable Functions to roll out firmware updates to thousands of IoT devices. The orchestration: (1) queries devices to update (activity), (2) fans-out to each device to download firmware (activity), (3) waits for each device to report installation status (external events), (4) aggregates success/failure, (5) sends a report. The monitor pattern is used to check device health after update. If a device fails to respond within 30 minutes, the orchestration marks it as failed and retries later. This system handles 50,000 devices. Key consideration: use ContinueAsNew to keep the orchestration alive for weeks without hitting history limits. Misconfiguration: not using context.CreateTimer for delays; using Thread.Sleep causes replay issues and unnecessary cost.

How AZ-204 Actually Tests This

What AZ-204 Tests on This Topic

The exam objective 'Create Azure Functions' includes sub-objective 'Implement Durable Functions' (approx. 10-15% of compute questions). You will be tested on:

Identifying the correct pattern for a given scenario (e.g., function chaining vs. fan-out/fan-in).

Recognizing non-deterministic code in orchestrator functions (common wrong answer: using DateTime.Now instead of context.CurrentUtcDateTime).

Understanding the role of the [DurableClient] attribute and the IDurableOrchestrationClient methods.

Knowing the default timeouts and limits (5-minute activity timeout on Consumption, 50k event history limit).

Interpreting the HTTP response from CreateCheckStatusResponse and knowing the URIs (status, send event, terminate, purge).

Common Wrong Answers and Why

1.

Using `Thread.Sleep` for delays: Candidates think it works, but it blocks the function and causes replay issues. Correct: context.CreateTimer.

2.

Calling external APIs directly in orchestrator: This introduces non-determinism. Correct: call an activity function that makes the API call.

3.

Choosing Logic Apps over Durable Functions for complex custom logic: Logic Apps is declarative and better for connectors; Durable Functions is for code-heavy workflows.

4.

Assuming orchestrator runs continuously: It is replayed on each event; it does not stay in memory. Candidates may think it holds a thread.

Specific Numbers and Terms

Default activity retry: 1-second interval, 10 attempts, exponential backoff.

History size limit: 50,000 events per instance.

Orchestration timeout: 5 minutes (Consumption), but can be extended via timers.

Task hub name defaults: TestHubName (local), AzureFunctionsHub (Azure).

CreateCheckStatusResponse returns URIs: statusQueryGetUri, sendEventPostUri, terminatePostUri, purgeHistoryGetUri.

Edge Cases

Sub-orchestrations: Use context.CallSubOrchestratorAsync to break large workflows into manageable pieces. The exam may test that sub-orchestrations inherit the same storage.

ContinueAsNew: Resets history but preserves instance ID. Useful for eternal orchestrations.

External events: RaiseEventAsync can only be called from client functions, not from within the orchestration.

Singleton orchestrations: Use starter.StartNewAsync with a known instance ID; check if instance already exists using starter.GetStatusAsync.

How to Eliminate Wrong Answers

If a question mentions long-running, stateful workflow with custom code, eliminate Logic Apps and standard Functions.

If code uses DateTime.Now inside orchestrator, eliminate that answer.

If the pattern requires waiting for multiple parallel tasks and then aggregating, choose fan-out/fan-in.

If the pattern requires waiting for an external event (e.g., approval), choose human interaction pattern.

Key Takeaways

Durable Functions enable stateful orchestration in Azure Functions using code.

Orchestrator functions must be deterministic: use context methods for time, GUID, and I/O.

Five core patterns: function chaining, fan-out/fan-in, async HTTP APIs, monitor, human interaction.

Default activity timeout on Consumption plan is 5 minutes; on Premium it is 10 minutes.

Orchestration history limit is 50,000 events per instance; use ContinueAsNew to reset.

CreateCheckStatusResponse returns URIs for status, send event, terminate, and purge.

Use Task Hub to isolate orchestrations; default hub name is AzureFunctionsHub in Azure.

External events are raised via IDurableOrchestrationClient.RaiseEventAsync from client functions only.

Retry policies can be set on activity calls with CallActivityWithRetryAsync.

Sub-orchestrations help modularize complex workflows and avoid history limits.

Easy to Mix Up

These come up on the exam all the time. Here's how to tell them apart.

Durable Functions

Code-first: write orchestrations in C#, JavaScript, Python, etc.

Supports complex custom logic and algorithms.

Tightly integrated with Azure Functions ecosystem.

More granular control over retries, error handling, and scaling.

Lower cost for high-volume scenarios (Consumption plan).

Logic Apps

Declarative: design workflows visually in Azure portal.

300+ connectors for SaaS and enterprise systems.

Better for integrating disparate systems without writing code.

Built-in managed connectors for enterprise systems (SAP, Oracle).

Higher cost per execution for high-volume scenarios.

Durable Functions (Consumption Plan)

Cold start latency (up to 10 seconds) for orchestrator functions.

5-minute timeout for activity functions.

Scales based on number of messages in queues.

No dedicated instances; shares resources with other functions.

Cost-effective for variable workloads.

Durable Functions (Premium Plan)

No cold starts (always warm instances).

10-minute timeout for activity functions.

Always ready instances; can scale to zero with minimum instance count.

Dedicated instances; predictable performance.

Higher cost but better for latency-sensitive workloads.

Watch Out for These

Mistake

Durable Functions orchestrator runs continuously on a single thread.

Correct

The orchestrator function is replayed on each event. It does not stay in memory. The runtime uses history to reconstruct state, so the code must be deterministic and idempotent.

Mistake

You can use any .NET async API inside an orchestrator function.

Correct

Only Durable Functions SDK methods (CallActivityAsync, CreateTimer, WaitForExternalEvent) are allowed. Direct I/O, random, or DateTime.Now cause non-determinism and replay failures.

Mistake

Activity functions have unlimited execution time.

Correct

Activity functions are subject to the same timeout as regular Azure Functions: 5 minutes on Consumption plan, 10 minutes on Premium, unlimited on Dedicated. Long-running activities should be split or use async patterns.

Mistake

The orchestration instance ID must be a GUID.

Correct

Instance ID can be any string. It is often a business key (order ID, document ID) to enable idempotent start. However, duplicate IDs will overwrite existing instances unless checked.

Mistake

Durable Functions only work with HTTP triggers.

Correct

Client functions can use any trigger: HTTP, Queue, Service Bus, Timer, Event Grid, etc. The client uses IDurableOrchestrationClient to start orchestrations.

Do You Actually Know This?

Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.

Frequently Asked Questions

What is the difference between an orchestrator function and an activity function in Durable Functions?

An orchestrator function defines the workflow and coordinates activities. It must be deterministic and cannot perform I/O directly. An activity function performs the actual work (I/O, computation) and can be non-deterministic. Orchestrators use context.CallActivityAsync to invoke activities. Activity functions are triggered by the Durable Functions runtime and return results to the orchestrator.

How do I handle long-running orchestrations that exceed the 5-minute timeout?

The orchestrator function itself can run indefinitely across replays because it yields control on each durable call. However, each activity function is limited by the function timeout (5 minutes on Consumption). To handle long-running activities, split them into smaller steps or use async patterns. For orchestrations that last days, use durable timers (context.CreateTimer) to create delays without consuming time.

Can I use Durable Functions with triggers other than HTTP?

Yes. The client function that starts the orchestration can use any trigger: HTTP, Queue, Service Bus, Event Grid, Timer, etc. The client injects IDurableOrchestrationClient via [DurableClient] attribute and calls StartNewAsync. For example, a Queue-triggered function can start an orchestration for each message.

What happens if an orchestrator function throws an exception?

If the exception occurs before any durable call (activity, timer, etc.), the function will be replayed indefinitely because no history is saved. To avoid this, ensure at least one durable call is made. If the exception occurs during a durable call, the runtime catches it and can retry the activity based on retry options. If unhandled, the orchestration enters a Failed state, and the history is preserved for debugging.

How do I pass input to an orchestration and activity functions?

Input is passed as a parameter to the orchestrator function via the client's StartNewAsync method. The orchestrator can then pass input to activity functions via CallActivityAsync. Input and output are serialized as JSON. For complex types, ensure they are serializable. The orchestrator can also use context.GetInput<T>() to deserialize.

What is the purpose of the Task Hub in Durable Functions?

A Task Hub is a logical container for storage resources (queues, tables, blobs) that isolates orchestration data. Multiple function apps can share the same storage account if they use different task hubs. The hub name is configured in host.json under durableTask.hubName. Default is 'TestHubName' locally and 'AzureFunctionsHub' in Azure.

How do I send an external event to a running orchestration?

Use the IDurableOrchestrationClient.RaiseEventAsync method from a client function. You need the instance ID and the event name. The orchestration must be waiting for that event via context.WaitForExternalEvent. For example, to send an approval decision: await starter.RaiseEventAsync(instanceId, "ApprovalEvent", true);

Terms Worth Knowing

Ready to put this to the test?

You've just covered Azure Functions Workflows: Orchestration Patterns — now see how well it sticks with free AZ-204 practice questions. Full explanations included, no account needed.

Done with this chapter?