Skip to main content

Versioning - Go 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 two primary Versioning methods that you can use:

  • Workflow Branching with GetVersion. This method works by adding branches to your code tied to specific revisions. In most other SDKs, it is called patching.
  • Worker Versioning. The Worker Versioning feature allows you to tag your Workers and programmatically roll them out in deployment versions, so that old Workers can run old code paths and new Workers can run new code paths. If you were using this method experimentally prior to summer 2025, refer to the Worker Versioning Legacy docs.

Workflow Branching

To understand why Workflow Branching is useful, it's helpful to first demonstrate cutting over an entire Workflow.

Workflow cutovers

Since incompatible changes only affect open Workflow Executions of the same type, you can avoid determinism errors by creating a whole new Workflow when making changes. 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, you would duplicate PizzaWorkflow as PizzaWorkflowV2:

func PizzaWorkflow(ctx workflow.Context, order PizzaOrder) (OrderConfirmation, error) {
// this function contains the original code
}

func PizzaWorkflowV2(ctx workflow.Context, order PizzaOrder) (OrderConfirmation, error) {
// this function contains the updated code
}

You can use any name you like for the new function, so long as the first character remains uppercase (this is a requirement for any Workflow Definition, since it must use an exported function). Using some type of version identifier, such as V2 in this example, will make it easier to identify the change.

You would then need to update the Worker configuration, and any other identifier strings, to register both Workflow Types:

w.RegisterWorkflow(pizza.PizzaWorkflow)
w.RegisterWorkflow(pizza.PizzaWorkflowV2)

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 GetVersion method.

Using GetVersion

Branching with GetVersion essentially defines a logical branch for a specific change in a Workflow. If your Workflow is not pinned to a specific deployment or you need to fix a bug in a running workflow, you can branch it.

Consider the following Workflow Definition:

func YourWorkflow(ctx workflow.Context, data string) (string, error) {
ao := workflow.ActivityOptions{
ScheduleToStartTimeout: time.Minute,
StartToCloseTimeout: time.Minute,
}
ctx = workflow.WithActivityOptions(ctx, ao)
var result1 string
err := workflow.ExecuteActivity(ctx, ActivityA, data).Get(ctx, &result1)
if err != nil {
return "", err
}
var result2 string
err = workflow.ExecuteActivity(ctx, ActivityB, result1).Get(ctx, &result2)
return result2, err
}

Suppose you replaced ActivityA with ActivityC and deployed the updated code.

Imagine you want to revise this Workflow by adding another Activity to calculate a file checksum. If an existing Workflow Execution was started by the original version of the Workflow code, where ActivityA was run, and then resumed running on a new Worker where it was replaced with ActivityC, the server side Event History would be out of sync. This would cause the Workflow to fail with a nondeterminism error.

To avoid this problem, use workflow.GetVersion():

var err error
v := workflow.GetVersion(ctx, "Step1", workflow.DefaultVersion, 1)
if v == workflow.DefaultVersion {
err = workflow.ExecuteActivity(ctx, ActivityA, data).Get(ctx, &result1)
} else {
err = workflow.ExecuteActivity(ctx, ActivityC, data).Get(ctx, &result1)
}
if err != nil {
return "", err
}

var result2 string
err = workflow.ExecuteActivity(ctx, ActivityB, result1).Get(ctx, &result2)
return result2, err

When workflow.GetVersion() is run for the new Workflow Execution, it records a marker in the Event History so that all future calls to GetVersion for this change Id — Step 1 in the example — on this Workflow Execution will always return the given version number, which is 1 in the example.

If you make an additional change, such as replacing ActivityC with ActivityD, you need to add some additional code:

v := workflow.GetVersion(ctx, "Step1", workflow.DefaultVersion, 2)
if v == workflow.DefaultVersion {
err = workflow.ExecuteActivity(ctx, ActivityA, data).Get(ctx, &result1)
} else if v == 1 {
err = workflow.ExecuteActivity(ctx, ActivityC, data).Get(ctx, &result1)
} else {
err = workflow.ExecuteActivity(ctx, ActivityD, data).Get(ctx, &result1)
}

Note that we changed maxSupported from 1 to 2. A Workflow that has already passed this GetVersion() call before it was introduced returns DefaultVersion. A Workflow that was run with maxSupported set to 1 returns 1. New Workflows return 2.

After you are sure that all of the Workflow Executions prior to version 1 have completed, you can remove the code for that version:

v := workflow.GetVersion(ctx, "Step1", 1, 2)
if v == 1 {
err = workflow.ExecuteActivity(ctx, ActivityC, data).Get(ctx, &result1)
} else {
err = workflow.ExecuteActivity(ctx, ActivityD, data).Get(ctx, &result1)
}

You'll note that minSupported has changed from DefaultVersion to 1. If an older version of the Workflow Execution history is replayed on this code, it fails because the minimum expected version is 1. After you are sure that all of the Workflow Executions for version 1 have completed, you can remove version 1 so that your code looks like the following:

_ := workflow.GetVersion(ctx, "Step1", 2, 2)
err = workflow.ExecuteActivity(ctx, ActivityD, data).Get(ctx, &result1)

Note that we have preserved the call to GetVersion(). There are two reasons to preserve this call:

  1. This ensures that if there is a Workflow Execution still running for an older version, it will fail here and not proceed.
  2. If you need to make additional changes for Step1, such as changing ActivityD to ActivityE, you only need to update maxVersion from 2 to 3 and branch from there.

You need to preserve only the first call to GetVersion() for each changeID. All subsequent calls to GetVersion() with the same change Id are safe to remove. If necessary, you can remove the first GetVersion() call, but you need to ensure the following:

  • All executions with an older version are completed.
  • You can no longer use Step1 for the changeId. If you need to make changes to that same part in the future, such as change from ActivityD to ActivityE, you would need to use a different changeId like Step1-fix2, and start minVersion from DefaultVersion again. The code would look like the following:
v := workflow.GetVersion(ctx, "Step1-fix2", workflow.DefaultVersion, 1)
if v == workflow.DefaultVersion {
err = workflow.ExecuteActivity(ctx, ActivityD, data).Get(ctx, &result1)
} else {
err = workflow.ExecuteActivity(ctx, ActivityE, data).Get(ctx, &result1)
}

You can add multiple calls to GetVersion in a single Workflow. This can become challenging to manage if you have many long-running Workflows, as you will wind up with many code branches over time. To clean these up, you can gradually deprecate older versions.

Deprecating old versions

You can safely remove support for older versions once you are certain that there are no longer any open Workflow Executions based on that version. You can use the following List Filter syntax for this (the 1 near the end of the last line represents the version number):

WorkflowType = "PizzaWorkflow" 
AND ExecutionStatus = "Running"
AND TemporalChangeVersion="ChangedNotificationActivityType-1"

Since Workflow Executions that were started before GetVersion was added to the code won't have the associated Marker in their Event History, you'll need to use a different query to determine if any of those are still running:

WorkflowType = "PizzaWorkflow" 
AND ExecutionStatus = "Running"
AND TemporalChangeVersion IS NULL

If you have found that there are no longer any open executions for the first two versions of the Workflow, for example, then you could remove support for them by changing the code as shown below:

version := GetVersion(ctx, "ChangedNotificationActivityType", 2, 3)
if version == 2 {
err = workflow.ExecuteActivity(ctx, SendTextMessage).Get(ctx, nil)
} else {
err = workflow.ExecuteActivity(ctx, SendTweet).Get(ctx, nil)
}

When workflow.GetVersion() is run for a new Workflow execution, it records a marker in the Workflow history so that all future calls to GetVersion() for this change ID — Step 1 in the example — on this Workflow execution will always return the given version number.

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

Worker Versioning

Temporal's Worker Versioning feature allows you to tag your Workers and programmatically roll them out in deployment versions, so that old Workers can run old code paths and new Workers can run new code paths. This way, you can pin your deployments to specific revisions, often avoiding the need for branching.

Runtime checking

The Temporal Go SDK performs a runtime check to help prevent obvious incompatible changes. Adding, removing, or reordering any of these methods without Versioning triggers the runtime check and results in a nondeterminism error:

  • workflow.ExecuteActivity()
  • workflow.ExecuteChildWorkflow()
  • workflow.NewTimer()
  • workflow.RequestCancelWorkflow()
  • workflow.SideEffect()
  • workflow.SignalExternalWorkflow()
  • workflow.Sleep()

The runtime check does not perform a thorough check. For example, it does not check on the Activity's input arguments or the Timer duration. Each Temporal SDK implements these sanity checks differently, and they are not a complete check for non-deterministic changes. Instead, you should incorporate Replay Testing when making revisions.