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 three primary Versioning methods that you can use:
- 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.
- 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 Type Versioning
Since incompatible changes only affect open Workflow Executions of the same type, you can avoid this 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:
func PizzaWorkflow(ctx workflow.Context, order PizzaOrder) (OrderConfirmation, error) {
// implementation code omitted for this example
}
then you would change the code as follows:
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 update the Worker configuration 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.
Workflow Branching with 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:
- This ensures that if there is a Workflow Execution still running for an older version, it will fail here and not proceed.
- If you need to make additional changes for
Step1
, such as changing ActivityD to ActivityE, you only need to updatemaxVersion
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 likeStep1-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.