Workflow Versioning Strategies

Once you have an understanding of the importance of writing deterministic workflow code, you may find yourself grappling with the best way to make changes to that code. Here, I’ll outline a few common strategies and when and how you might employ them.

There are fundamentally three approaches available to you: Worker Versioning, the patch/getVersion APIs, and workflow-name based versioning. Each has its relative merits and demerits. You should prefer to use the first two, as they’re the officially supported methods provided by Temporal (and can be used together).

Let’s take a look at each:

Worker Versioning

See the official docs on Worker Versioning. This should be your go-to strategy unless you have more specific needs. However be aware that there are currently backwards-incompatible changes planned to the API (though semantically you’ll be able to accomplish the same things)

Advantages:

  • Simple, built-in
  • Robust. Changes are, by default isolated from each other in a way that makes mistakes unlikely.
  • Flexible – you can handle both compatible and incompatible changes

Disadvantages:

  • Some operational burden in the form of worker management

Patch & GetVersion APIs

We have functions available to you in the SDK which allow you to branch in your workflow code based on whether or not the workflow is running with newer or older code. They take slightly different forms depending on what language you’re using. See the linked docs for more.

Advantages:

  • Don’t need to change where workflow starters are pointing
  • Allows you to change the yet-to-be-executed behavior of currently open workflows while remaining compatible with existing histories. The behavior of new workflows always takes the “new” path.
  • Can be used with Worker Versioning to make compatible changes

Disadvantages:

  • Conceptually complex
  • Cognitive burden of needing to understand how both the “old” and “new” code paths work
  • If used indefinitely on the same workflow definition, can lead to a mess of branching

Workflow name based Versioning

Very similar to task-queue based versioning, now when you make changes, simply copy your workflow code MyFooWorkflow to MyFooWorkflowV2. You now redeploy all your workers, and change your workflow starters to point at the V2 workflow.

Advantages:

  • Don’t need to keep separate worker fleets pointed at different queues
  • Conceptually simple
  • Easier to patch old versions when necessary without affecting the code for new versions

Disadvantages:

  • Code duplication - you can only delete the old workflow code when you know all workflows of that type/version have finished
  • Still need to update workflow starters/clients

Task Queue based Versioning (Replaced by Worker Versioning)

:warning: Don’t use this! Use Worker Versioning as described above instead! This section is only kept for posterity.

In this approach, after making changes to your workflow code, you will want to deploy your workers pointed at a new task queue than your existing ones. For example if you previously were targeting the task queue foo-v1, you’d now target foo-v2 or similar. You must also update whatever sources are starting new workflows to point at the new task queue name. Leave your old workers running, possibly reducing the number of instances, until there are no more open workflows on foo. Then you can decommission all of them.

Advantages:

  • Conceptually simple
  • Robust. Changes are isolated from each other in a way that makes mistakes unlikely.

Disadvantages

  • Operationally complex (need to keep old workers alive and change what task queue clients point to). This can result in a number of sets of old workers, especially with long running workflows.
  • Can’t be used to fix a bug in currently running/open workflows
11 Likes

It’d be great to see this guide updated now that Worker versioning is available. It seems like it would largely replace Task Queued-based versioning, and even some uses of the Patch & GetVersion APIs.

1 Like

Thanks so much for this post @Spencer_Judge. If we are migrating a whole workflow from one language to another and have a wrapper for that workflow that currently exists in the language we are migrating to, can you use workflow versioning to have the workflow you are migrating just replace the wrapper without causing issues? Eg If you have workflowA which is written in language A and a wrapper workflow in language B called workflowB can you make the translated version of workflowA (which is now in language B) a v2 of workflowB? I want to make sure I understand how workflow versioning could work for helping with migrations like this.

Great point, I’ll edit the post to refer to those docs which are where we’d want people to be looking at this point anyway

Assuming you’re referencing the new Worker Versioning capabilities, yes, you could use it to accomplish what you’re talking about. However you’ll want to take care to ensure that your inputs and outputs to the workflow are (de)serializable by both languages, so that clients can continue to provide the same inputs (and read the workflow results) without requiring changes too.

Thanks so much for updating the docs @Spencer_Judge! Okay so if I’m understanding correctly, the approach when migrating from one language to another where the language you are migrating to is the language that has a wrapper workflow/parent workflow for the workflow you are migrating is that you should just create a new worker and then use versioning to swap over from the old workflow to the new one. If the worker for the old workflow code (where the parent/wrapper lives) is not versioned, then you need to version it so that all workflows using it can be tagged with the build id for that worker/task queue and then create a new worker and version that worker. But what I’m not understanding is that the inputs and outputs will be the same as the current parent workflow (the wrapper that is in the language that we want to migrate our child workflow and its activities to) so I don’t quite see how this helps with that. I read the documentation you linked and am not sure that I see how this is applicable in this case given the fact that the inputs and outputs of the parent should stay the same and what we want to change is that instead of calling a child workflow which is written in another language, we want to have the activity(ies) and workflow for the child replace what is in the parent/wrapper. The only reason the wrapper exists in this case is to deal with the fact that the child workflow was written in one language but needs to be called using another.

You won’t need to version the old code - you can, but it’s not strictly necessary. If you add a Build ID to a Task Queue which previously didn’t use any for versions, the old un-versioned workflows will continue executing on the un-versioned workers, and new runs will start on the newly added default Build ID.

As for the inputs and outputs, it’s good that they’re not changing. All I was getting at is you’re going to want to make sure that the deserialization of those inputs in the new language is compatible with the existing format.

Be aware too that it sounds like you’re going to want to specify the versioning_intent argument for these child workflows that you’re swapping over to the new language. By default, children will try to run on a compatible worker, which in the case of your existing unversioned workers will mean sticking to them. Setting that argument to be DEFAULT (exact names depend on your SDK language) will ensure that the children start on the newly added default Build ID.

Thanks @Spencer_Judge! I was also wondering if we can use patching for changes to an activity. Let’s say I have actvity A and activityA has a signature that looks like this: activityA(string). But now I want to make a change to activityA’s signature and I decide that I want to future proof it so I make it activityA(activityInput) where the parameter is an object. If I currently am running the original version of activityA, can I just use if (patched(‘somePatchName’)) and then call my new activity or do I need to create a new activity for the updated activityA and give it a new taskqueue etc. I am a bit confused about worker versioning in this case and if it is the best way forward as well as the only way forward. I wonder because my client set up is a bit complex so I don’t think the: await client.taskQueue.updateBuildIdCompatibility(taskQueue, {
operation: ‘addNewIdInNewDefaultSet’,
buildId: ‘1.0’,
});
change is trivial because I currently do not use TaskQueueClient