Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Deploying changes to orchestrator logic is a key consideration when working with durable orchestration systems. If an orchestration is interrupted and later resumed (for instance, during a host update), the runtime replays the events of the orchestration, ensuring all previous steps execute successfully before taking the next step. If the orchestration code changed between deployments, the steps it takes might no longer be the same. In that case, the system throws a nondeterminism error instead of allowing the orchestration to continue.
Orchestration versioning prevents problems related to nondeterminism, allowing you to work seamlessly with new (or old) orchestrations while maintaining the deterministic execution model that durable orchestrations require.
This built-in feature provides automatic version isolation with minimal configuration. It's backend agnostic, so any app using any of the Durable Functions storage providers, including the Durable Task Scheduler, can use it.
Durable Task SDKs support two styles of versioning, which you can use separately or together:
- Client/context-based conditional versioning—set a version on the client and branch logic in the orchestrator.
- Worker-based versioning—let the worker decide which orchestration versions it can process.
Terminology
This article uses two related but distinct terms:
- Orchestrator function (or simply "orchestrator"): The function code that defines the workflow logic — the template or blueprint for how a workflow should execute.
- Orchestration instance (or simply "orchestration"): A specific running execution of an orchestrator function, with its own state, instance ID, and inputs. Multiple orchestration instances can run concurrently from the same orchestrator function.
Understanding this distinction is crucial for orchestration versioning. The orchestrator function code contains version-aware logic, while orchestration instances are permanently associated with a specific version when created.
How it works
Orchestration versioning operates on these core principles:
- Version association: When an orchestration instance is created, it gets a version permanently associated with it.
- Version-aware execution: Orchestrator code examines the version value associated with the current orchestration instance and branches execution accordingly.
- Backward compatibility: Workers running newer orchestrator versions continue to execute orchestration instances created by older versions.
- Forward protection: The runtime prevents workers running older orchestrator versions from executing orchestrations started by newer versions.
Prerequisites
Before you use orchestration versioning, ensure you have the required package versions for your programming language.
If you're using a non-.NET language (JavaScript, Python, PowerShell, or Java) with extension bundles, your function app must reference Extension Bundle version 4.30.0 or later. Configure the extensionBundle range in host.json so that the minimum version is at least 4.30.0. For example:
{
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.30.0, 5.0.0)"
}
}
For details on choosing and updating bundle versions, see the extension bundle configuration documentation.
In addition to the extension bundle requirement for non-.NET languages, you also need to use the minimum version of the language-specific SDK package listed below. Both the extension bundle and the SDK package are required for orchestration versioning to work correctly.
Use Microsoft.Azure.Functions.Worker.Extensions.DurableTask version 1.14.0 or later.
Setting the default version
To use orchestration versioning, first configure a default version for new orchestration instances.
Add or update the defaultVersion setting in the host.json file in your Azure Functions project:
{
"extensions": {
"durableTask": {
"defaultVersion": "<version>"
}
}
}
The version string can follow any format that suits your versioning strategy:
- Multi-part versioning:
"1.0.0","2.1.0" - Simple numbering:
"1","2" - Date-based:
"2025-01-01" - Custom format:
"v1.0-release"
After you set defaultVersion, all new orchestration instances are permanently associated with that version.
Set the default version in the client builder when configuring your application.
Note
Available in the .NET SDK (Microsoft.DurableTask.Client.AzureManaged) since v1.9.0.
builder.Services.AddDurableTaskClient(builder =>
{
builder.UseDurableTaskScheduler(connectionString);
builder.UseDefaultVersion("1.0.0");
});
The version is a simple string and accepts any value. The SDK tries to convert it to .NET's System.Version. If successful, that library is used for comparison. Otherwise, a simple string comparison is used.
After you set the default version on the client, any orchestration started by this client is permanently associated with that version. The version is also available in the orchestration context, allowing you to use it in conditional statements.
Version comparison rules
When the Strict or CurrentOrOlder strategy is selected (see Version matching), the runtime compares the orchestration instance's version with the defaultVersion value of the worker using the following rules:
- Empty or null versions are treated as equal.
- An empty or null version is considered older than any defined version.
- If both versions are numeric (for example,
"1.0"and"2.0"), they're compared as version numbers, so"2.0"is newer than"1.0". - Otherwise, case-insensitive string comparison is performed.
The following examples illustrate how version comparison works:
| Version A | Version B | Result |
|---|---|---|
"1.0" |
"2.0" |
A is older |
null |
"1.0" |
A is older |
null |
null |
Equal |
"v1-release" |
"v2-release" |
A is older (alphabetical) |
When the Strict or CurrentOrOlder match strategy is selected (see Version matching), version comparison depends on the language:
- .NET: The SDK tries to parse the version as
System.Version. If both parse successfully, comparison usesCompareTo. Otherwise, the SDK uses string comparison. - Python: The SDK uses
packaging.versionfor semantic versioning comparison. - Java: The SDK compares the version as a simple string.
Version-aware orchestrator logic
To implement version-aware logic, use the context parameter to access the current orchestration instance's version and branch execution.
Important
When implementing version-aware logic, it's critical to preserve the exact orchestrator logic for older versions. Any changes to the sequence, order, or signature of activity calls for existing versions can break deterministic replay and cause in-flight orchestrations to fail or produce incorrect results. Keep old version code paths unchanged after deployment.
[Function("MyOrchestrator")]
public static async Task<string> RunOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
if (context.Version == "1.0")
{
// Original logic for version 1.0
...
}
else if (context.Version == "2.0")
{
// New logic for version 2.0
...
}
...
}
[DurableTask]
class HelloCities : TaskOrchestrator<string, List<string>>
{
private readonly string[] Cities = ["Seattle", "Amsterdam", "Hyderabad"];
public override async Task<List<string>> RunAsync(
TaskOrchestrationContext context, string input)
{
List<string> results = [];
foreach (var city in Cities)
{
results.Add(await context.CallSayHelloAsync($"{city} v{context.Version}"));
if (context.CompareVersionTo("2.0.0") >= 0)
{
results.Add(await context.CallSayGoodbyeAsync($"{city} v{context.Version}"));
}
}
return results;
}
}
Note
The context.Version property is read-only and reflects the version permanently associated with the orchestration instance at creation time. This value can't be modified during orchestration execution.
Tip
If you already have in-flight orchestrations created before you specified a default version, context.Version returns null (or a language-dependent equivalent) for those instances. Structure your orchestrator logic to handle both the legacy (null version) and new versioned orchestrations.
Deployment behavior
Here's what to expect when you deploy your updated orchestrator function with the new version logic:
- Worker coexistence: Workers containing the new orchestrator function code start, while some workers with the old code are potentially still active.
- Version assignment for new instances: All new orchestrations and sub-orchestrations created by the new workers get the version from
defaultVersionassigned to them. - New worker compatibility: New workers can process both the newly created orchestrations and the previously existing orchestrations because the version-aware branching logic ensures backward compatibility.
- Old worker restrictions: Old workers can process only the orchestrations with a version equal to or lower than the version specified in their own
defaultVersioninhost.json, because they aren't expected to have orchestrator code compatible with newer versions.
Note
Orchestration versioning doesn't affect worker lifecycle. The Azure Functions platform manages worker setup and decommissioning based on regular rules depending on hosting models.
Example: Replace an activity in the sequence
This example shows how to replace an activity in the middle of a sequence by using orchestration versioning.
Version 1.0
host.json configuration:
{
"extensions": {
"durableTask": {
"defaultVersion": "1.0"
}
}
}
Orchestrator function:
[Function("ProcessOrderOrchestrator")]
public static async Task<string> ProcessOrder(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var orderId = context.GetInput<string>();
await context.CallActivityAsync("ValidateOrder", orderId);
await context.CallActivityAsync("ProcessPayment", orderId);
await context.CallActivityAsync("ShipOrder", orderId);
return "Order processed successfully";
}
Version 2.0 with discount processing
host.json configuration:
{
"extensions": {
"durableTask": {
"defaultVersion": "2.0"
}
}
}
Orchestrator function:
[Function("ProcessOrderOrchestrator")]
public static async Task<string> ProcessOrder(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var orderId = context.GetInput<string>();
await context.CallActivityAsync("ValidateOrder", orderId);
if (TaskOrchestrationVersioningUtils.CompareVersions(context.Version, "1.0") <= 0)
{
// Preserve original logic for existing instances
await context.CallActivityAsync("ProcessPayment", orderId);
}
else
{
// New logic with discount processing
await context.CallActivityAsync("ApplyDiscount", orderId);
await context.CallActivityAsync("ProcessPaymentWithDiscount", orderId);
}
await context.CallActivityAsync("ShipOrder", orderId);
return "Order processed successfully";
}
Version matching
The version matching strategy determines which orchestration instances a worker processes based on version compatibility.
The following table describes the available strategies:
| Strategy | Description |
|---|---|
| None | Version isn't considered when processing work. All work is processed regardless of version. |
| Strict | The orchestration version and the worker version must match exactly. |
| CurrentOrOlder | The orchestration version must be equal to or less than the worker version. This is the default strategy. |
Configuration
{
"extensions": {
"durableTask": {
"defaultVersion": "<version>",
"versionMatchStrategy": "CurrentOrOlder"
}
}
}
None(not recommended): Disables version checking. Any worker processes any orchestration instance.Strict: Processes tasks only from orchestrations with the exact same version asdefaultVersion. Requires careful deployment coordination to avoid orphaned orchestrations.CurrentOrOlder(default): Processes tasks from orchestrations with a version less than or equal todefaultVersion. Enables backward compatibility while preventing older workers from processing newer orchestrations.
Configure the match strategy through the worker builder.
Note
Available in the .NET SDK (Microsoft.DurableTask.Worker.AzureManaged) since v1.9.0.
builder.Services.AddDurableTaskWorker(builder =>
{
builder.AddTasks(r => r.AddAllGeneratedTasks());
builder.UseDurableTaskScheduler(connectionString);
builder.UseVersioning(new DurableTaskWorkerOptions.VersioningOptions
{
Version = "1.0.0",
DefaultVersion = "1.0.0",
MatchStrategy = DurableTaskWorkerOptions.VersionMatchStrategy.Strict,
FailureStrategy = DurableTaskWorkerOptions.VersionFailureStrategy.Reject,
});
});
Version mismatch handling
The version mismatch handling strategy determines what happens when an orchestration instance version doesn't match the worker version.
The following table describes the available strategies:
| Strategy | Description |
|---|---|
| Reject | The orchestration is rejected and returned to the work queue. Another worker can attempt it later. This strategy is the default. |
| Fail | The orchestration is failed and removed from the work queue. |
Configuration
{
"extensions": {
"durableTask": {
"defaultVersion": "<version>",
"versionFailureStrategy": "Reject"
}
}
}
Reject(default): The orchestration instance remains in its current state and can be retried later when a compatible worker becomes available. This strategy is the safest option because it preserves orchestration state.Fail: Immediately terminates the orchestration instance with a failure state. This option might be appropriate when version mismatches indicate serious deployment issues.
When to use each strategy
Reject: Use this strategy when you want the orchestration to retry later or on a different worker. During a Reject failure:
- The orchestration is rejected and returned to the work queue.
- Another worker dequeues the orchestration.
- The dequeued orchestration could land on a different worker or the same one again.
The process repeats until a worker that can handle the orchestration is available. This strategy seamlessly handles rolling deployments where workers are updated progressively.
Fail: Use this strategy when no other version of the worker is expected to process the orchestration. The orchestration fails and enters a terminal state.
Starting orchestrations with specific versions
By default, all new orchestration instances use the current defaultVersion specified in your host.json configuration. However, you might have scenarios where you need to create orchestrations with a specific version that differs from the current default.
When to use specific versions
- Gradual migration: Keep creating orchestrations with an older version even after deploying a newer version.
- Testing scenarios: Test specific version behavior in production.
- Rollback situations: Temporarily revert to creating instances with a previous version.
- Version-specific workflows: Different business processes require different orchestration versions.
[Function("HttpStart")]
public static async Task<HttpResponseData> HttpStart(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
[DurableClient] DurableTaskClient client,
FunctionContext executionContext)
{
var options = new StartOrchestrationOptions
{
Version = "1.0"
};
string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
"ProcessOrderOrchestrator", orderId, options);
// ...
}
You can also start sub-orchestrations with specific versions from within an orchestrator function:
[Function("MainOrchestrator")]
public static async Task<string> RunMainOrchestrator(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var subOptions = new SubOrchestratorOptions
{
Version = "1.0"
};
var result = await context.CallSubOrchestratorAsync<string>(
"ProcessPaymentOrchestrator", orderId, subOptions);
// ...
}
Remove legacy code paths
Over time, you might want to remove legacy code paths from your orchestrator functions to simplify maintenance and reduce technical debt. Remove code carefully to avoid breaking existing orchestration instances.
When it's safe to remove legacy code
- All orchestration instances using the old version completed (succeeded, failed, or ended).
- No new orchestration instances will be created with the old version.
- You checked through monitoring or querying that no instances are running with the legacy version.
- A sufficient time period passed since the old version was last deployed.
Warning
Removing legacy code paths while orchestration instances are still running those versions can cause deterministic replay failures. Always check that no instances are using the legacy version before removing the code.
Best practices
Version management
- Use multi-part versioning: Adopt a consistent versioning scheme, like
major.minor.patch. - Document breaking changes: Clearly document what changes require a new version.
- Plan version lifecycle: Define when to remove legacy code paths.
Code organization
- Separate version logic: Use clear branching or separate methods for different versions.
- Preserve determinism: Don't modify existing version logic once deployed. If changes are absolutely necessary, like critical bug fixes, ensure they maintain deterministic behavior and don't alter the sequence of operations.
- Test thoroughly: Test all version paths, especially during transitions.
Monitoring and observability
- Log version information: Include the version in your logging for easier debugging.
- Monitor version distribution: Track which versions actively run.
- Set up alerts: Monitor for any version-related errors.
Troubleshooting
Common issues
Issue: Orchestration instances created with version 1.0 are failing after deploying version 2.0
- Solution: Make sure the version 1.0 code path in your orchestrator remains exactly the same. Any changes to the execution sequence can break deterministic replay.
Issue: Workers running older orchestrator versions can't run new orchestrations
- Solution: This behavior is expected. The runtime prevents older workers from running orchestrations with newer versions. Make sure all workers are updated to the latest version and that their
defaultVersionsetting inhost.jsonis updated accordingly.
- Solution: This behavior is expected. The runtime prevents older workers from running orchestrations with newer versions. Make sure all workers are updated to the latest version and that their
Issue: Version information isn't available in the orchestrator (
context.Versionorcontext.getVersion()is null, regardless of thedefaultVersionsetting)- Solution: Check the Prerequisites section to make sure your environment meets all the requirements for orchestration versioning.
Issue: Orchestrations of a newer version are making very slow progress or are stuck
- Solution: This problem can have different root causes:
- Insufficient newer workers: Make sure enough workers that contain an equal or higher version in
defaultVersionare deployed and active. - Orchestration routing interference from older workers: Old workers can interfere with the orchestration routing mechanism, making it harder for new workers to pick up orchestrations. This interference can be especially noticeable with certain storage providers (Azure Storage or MSSQL). Normally, the Azure Functions platform makes sure that old workers are disposed of soon after a deployment, so any delay typically isn't significant. Consider using the Durable Task Scheduler for an improved routing mechanism.
- Insufficient newer workers: Make sure enough workers that contain an equal or higher version in
- Solution: This problem can have different root causes: