Skip to main content

Versioning - .NET SDK

Since Workflow Executions in Temporal can run for long periods — sometimes months or even years — it's common to need to make changes to a Workflow Definition, even while a particular Workflow Execution is in progress.

The Temporal Platform requires that Workflow code is deterministic. If you make a change to your Workflow code that would cause non-deterministic behavior on Replay, you'll need to use one of our Versioning methods to gracefully update your running Workflows. With Versioning, you can modify your Workflow Definition so that new executions use the updated code, while existing ones continue running the original version. There are three primary Versioning methods supported by Temporal:

  • Workflow Type Versioning. This is the simplest of the three, and acts more like a cutover than true versioning. It is suitable for short-running Workflows.
  • Versioning with Patching. This method works by adding branches to your code tied to specific revisions. It can be used to revise in-progress Workflows.
  • Safe Deploys. Temporal's Safe Deploys feature allows you to tag your Workers and programmatically roll them out in pods tied to specific revisions, so that old Workers can run old code paths and new Workers can run new code paths.

Workflow Type Versioning

Since incompatible changes only affect open Workflow Executions of the same type, you can avoid the problem by changing the Workflow Type for the new version. To do this, you can copy the Workflow Definition function, giving it a different name, and make sure that both names were registered with your Workers.

For example, if you had made an incompatible change to the following Workflow Definition:

[Workflow]
public class SayHelloWorkflow
{
[WorkflowRun]
# implementation code omitted for this example
}

then you would change the code as follows:

[Workflow]
public class SayHelloWorkflow
{
[WorkflowRun]
# this function contains the original code
}

[Workflow]
public class SayHelloWorkflowV2
{
[WorkflowRun]
# this function contains the updated code
}

You can use any name you like for the new function. Using some type of version identifier, such as V2 in this example, will make it easier to identify the change.

You would then update the Worker configuration to register both Workflow Types:

using var worker = new TemporalWorker(
client,
new TemporalWorkerOptions("greeting-tasks")
.AddWorkflow<SayHelloWorkflow>()
.AddWorkflow<SayHelloWorkflowV2>());

The upside of this versioning method is that it is easy to understand at a glance, as it does not really use any Temporal platform features. The downside of this method is that it does not use any Temporal platform features. It requires you to duplicate code and to update any code and commands used to start the Workflow. This can become impractical over time, depending on how you are providing configuration strings to your deployment. This method also does not provide a way to introduce versioning to any still-running Workflows -- it is essentially just a cutover, unlike the Patching method.

Versioning with Patching

Patching essentially defines a logical branch for a specific change in the Workflow, rather than for the entire Workflow Definition itself.

Suppose you have an initial Workflow version called PrePatchActivity:

[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PrePatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });

// ...
}
}

Now, you want to update your code to run PostPatchActivity instead. This represents your desired end state.

[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PostPatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });

// ...
}
}

The problem is that you cannot deploy PostPatchActivity directly until you're certain there are no more running Workflows created using the PrePatchActivity code, otherwise you are likely to cause a nondeterminism error. Instead, you'll need to deploy PostPatchActivity and use the Patched method to determine which version of the code to execute.

Patching is a three step process:

  1. Use Patched to patch in new code and run it alongside the old code.
  2. Remove the old code and apply DeprecatePatch.
  3. Once you're confident that all old Workflows have finished executing, remove DeprecatePatch.

Patching in new code

Using Patched inserts a marker into the Workflow History.

During replay, if a Worker encounters a history with that marker, it will fail the Workflow task when the Workflow code doesn't produce the same patch marker (in this case, my-patch). This ensures you can safely deploy code from PostPatchActivity as a "feature flag" alongside the original version (PrePatchActivity).

[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
if (Workflow.Patched("my-patch"))
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PostPatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
}
else
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PrePatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
}

// ...
}
}

Deprecating patches

After ensuring that all Workflows started with PrePatchActivity code have finished, you can deprecate the patch.

Deprecated patches serve as a bridge between PrePatchActivity and PostPatchActivity. They function similarly to regular patches by adding a marker to the Workflow History. However, this marker won't cause a replay failure when the Workflow code doesn't produce it.

If, during the deployment of PostPatchActivity, there are still live Workers running PrePatchActivity code and these Workers pick up Workflow histories generated by PostPatchActivity, they will safely use the patched branch.

[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
Workflow.DeprecatePatch("my-patch")
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PostPatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });

// ...
}
}

Removing a patch

You can safely deploy PostPatchActivity once all Workflows labeled my-patch or earlier are finished, based on the previously mentioned assertion.

[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PostPatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });

// ...
}
}

Patching allows you to make changes to currently running Workflows. It is a powerful method for introducing compatible changes without introducing non-determinism errors.

Safe Deploys

Temporal's Safe Deploys feature allows you to tag your Workers and programmatically roll them out in pods tied to specific revisions, so that old Workers can run old code paths and new Workers can run new code paths. If you find that patching is adding maintainability concerns to your codebase, consider adopting Safe Deploys.

Changing the order of any commands in your Workflow code that interact directly with the Temporal Service -- such as calling an Activity or creating a Sleep timer -- will cause a non-determinism error unless you've implemented a versioning solution. To test whether a new revision will require versioning, you should incorporate Replay Testing.