- Published on
Temporal Worker Versioning GA — The Deploy Problem Replay Created, and the Third Answer After Two Discarded Generations
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Introduction — Durable Execution Bills You at Deploy Time
- The Mechanism by Which Replay Breaks Deploys
- The Answer So Far — Patching, and Its Cost
- Three Years, Three Generations — A Chronicle of Versioning API Deprecation
- The GA'd Model — Deployment, Pinned, Auto-Upgrade
- The Remaining Hole for Very Long Workflows — Upgrade on Continue-as-New
- The Honest Tradeoffs
- The Rest of the Same Release — Where the Engine Is Headed
- So Should You Use It
- Closing
- References
Introduction — Durable Execution Bills You at Deploy Time
A broad comparison of durable execution engines is already covered in May's deep dive. This post is the opposite: one engine, narrow and deep — Temporal announced GA for Worker Versioning on March 30, 2026, and on the OSS server, v1.31.0 (April 29, 2026) spelled out GA for the Worker Deployment API.
"Versioning feature goes GA" might sound like a yawn, but this is not that kind of news. It took two years and nine months from the first preview (June 2023) to GA, and along the way two full generations of API were thrown away. In the very first paragraph of the GA announcement the vendor admits it themselves — deploying new code to long-running workflows has always been one of the hardest problems in durable execution. Why that problem is so hard follows directly from the engine's core design: deterministic replay. Let us start with that mechanism.
The Mechanism by Which Replay Breaks Deploys
The durability of a Temporal workflow works like this. Every time workflow code issues a command — run an activity, set a timer, start a child workflow — the server records it in the event history, and when a worker dies or is replaced, the new worker replays that history from the beginning to restore the code's execution state. For this magic to hold, one premise is required: given the same history, the code must always issue the same commands. The official docs require this explicitly: workflow code must be deterministic, non-deterministic work like API calls or DB queries must be pushed out into activities, and Temporal will retry activities for you.
But that premise collapses the moment the code changes.
v1 code: run A → timer → run B (history already recorded in this order)
v2 code: run A → run C → timer → run B (new activity C inserted in the middle)
When a v2 worker replays an execution started on v1:
the next event in the history is "timer started"
but the code issues a "schedule activity C" command
→ non-determinism error, workflow task failure
If you only have short workflows, this is barely a problem. They all finish before the deploy. But the entire reason Temporal exists is workflows that run for days, weeks, years. Which means the assumption "there are no workflows running at deploy time" is structurally false, and replay, the very source of durability, becomes the landmine in your deploy. This is not a Temporal bug — it is a constraint shared by the whole family of replay-based durable execution models.
The Answer So Far — Patching, and Its Cost
The traditional answer to this problem is patching. To borrow the docs' phrasing, it is similar to a feature flag: you plant a logical branch inside the code that asks "did this execution start before this change?"
from datetime import timedelta
from temporalio import workflow
@workflow.defn
class MyWorkflow:
@workflow.run
async def run(self) -> None:
if workflow.patched("use-new-activity"):
# executions that started after this patch
self._result = await workflow.execute_activity(
post_patch_activity,
schedule_to_close_timeout=timedelta(minutes=5),
)
else:
# executions that started before the patch and are still running — keep the old path
self._result = await workflow.execute_activity(
pre_patch_activity,
schedule_to_close_timeout=timedelta(minutes=5),
)
It works. The problem is accumulation. The GA announcement puts a finger on it exactly — because workflows can run for days, weeks, or years, patches pile up as real code complexity and cognitive burden. To delete a branch you have to confirm that every execution on the old path has finished and then walk it through a deprecation procedure — which is also manual. In short, patching is the approach that absorbs the deploy problem into conditionals inside workflow code, and in a codebase that changes often, those conditionals are technical debt.
Worker Versioning flips the approach — instead of planting version branches inside the code, you version whole workers by deploy unit and leave running workflows on the worker version that started them. The idea is simple. Pinning down the API for this simple idea took three years.
Three Years, Three Generations — A Chronicle of Versioning API Deprecation
Follow the release notes in chronological order and the difficulty of this feature is right there on the page.
2023-06-23 v1.21.0 V1 preview: grouping Build IDs into "version sets"
UpdateWorkerBuildIdCompatibility / GetWorkerTaskReachability
2024-05-31 v1.24.0 V1's version set concept declared deprecated
→ replaced by V2 "versioning rules" (assignment rules), experimental
2024-12 (pre-release) a third, Deployment-based design appears
2025-06-27 v1.28.0 Worker Deployment API promoted to public preview
V1 and V2 APIs deprecated together
the 2024-12 pre-release APIs go unsupported
2026-03-30 blog GA officially announced (across all SDKs)
2026-04-29 v1.31.0 OSS server spells out GA, V1 and V2 sunset
→ removal announced for the next version, v1.32.0
Taken one at a time, here is how it went. v1.21.0 (June 2023) was the first preview — you tagged workers with a Build ID and registered which builds were compatible with each other into a "version set" via UpdateWorkerBuildIdCompatibility. v1.24.0 (May 2024) wrote in its release notes that the version set concept itself was deprecated and replaced by more flexible "versioning rules". And the notes for v1.28.0 (June 2025) mark even the Deployment-based APIs that had shipped as the "December 2024 pre-release" (DescribeDeployment and friends) as unsupported, while today's Worker Deployment API rises to public preview. That is when the V1 and V2 APIs were deprecated all at once, and users who had been using versioning on v1.27.x even got a breaking-change notice telling them to delete all existing Deployments before upgrading.
Then v1.31.0 (April 2026) declared nine APIs GA — SetWorkerDeploymentCurrentVersion, SetWorkerDeploymentRampingVersion, DescribeWorkerDeployment and others — and announced that the V1 and V2 APIs will be removed in the next server version, v1.32.0 (as of this writing the latest server is v1.31.2, so the removal has not yet been carried out). What is interesting is that experimental APIs like CreateWorkerDeployment were newly added at the same time as GA — meaning the surface is still moving.
Two things can be read out of this. First, designing an API for "safe deploys" on top of a replay model is hard enough that one vendor tore it up twice. Second, a practical warning — if you are self-hosted and have built deploy automation on top of the V1 (build ID compatibility) or V2 (versioning rules) APIs, migration is not optional; it is work with a deadline named v1.32.0.
The GA'd Model — Deployment, Pinned, Auto-Upgrade
Here is the structure of the third design, the one that survived (per the concepts doc).
A Worker Deployment is a logical grouping at the service level (it has a name), and a Worker Deployment Version is one iteration of that service, identified by deployment name plus Build ID. Workers report their own version to the server when they start polling. The worker-side configuration looks like this (the Python example from the production guide).
from temporalio.common import WorkerDeploymentVersion, VersioningBehavior
from temporalio.worker import Worker, WorkerDeploymentConfig
worker = Worker(
client,
task_queue="mytaskqueue",
workflows=workflows,
activities=activities,
deployment_config=WorkerDeploymentConfig(
version=WorkerDeploymentVersion(
deployment_name="llm_srv", build_id=my_env.build_id
),
use_worker_versioning=True,
default_versioning_behavior=VersioningBehavior.UNSPECIFIED,
),
)
The core is the two Versioning Behavior options you declare per workflow type.
@workflow.defn(versioning_behavior=VersioningBehavior.PINNED)
class OrderWorkflow:
...
- Pinned — runs to completion on the version it started on. In the docs' own guarantee wording, it is guaranteed to complete on a single Worker Deployment Version. Since the code cannot change mid-execution, that workflow needs no patching. If you really must move one, you do it by hand with
temporal workflow update-options. - Auto-Upgrade — automatically follows along when the current version changes. In exchange, as the docs make explicit, it moves across multiple versions, so you must manually maintain replay safety with patching. Versioning does not replace patching; it exempts you from it for pinned only.
Routing runs on three concepts. The Current Version, where new workflows go; the Ramping Version, which gets a slice of traffic first (0–100%, with groups split by workflow ID); and the Target Version, the one each execution will move onto next. Like a canary you send 5% to the new version, and if nothing breaks you promote it to current; if something does, you roll current back to the previous version — and because the old version's workers are still polling, the rollback is immediate.
A version's lifecycle flows Inactive → Active → Draining (taken down from current/ramping but pinned workflows remain) → Drained (all remaining pinned ones have finished). The inheritance rules are documented too — a pinned parent's children inherit the parent version (if it is a task queue in the same deployment), auto-upgrade inherits nothing, and cron never inherits. Continue-as-New carries the pinned version across the chain.
The version requirements are not trivial. Per the docs: server v1.29.1+, CLI v1.4.1+, and SDKs at Go v1.35.0 / Python 1.11 / Java 1.29 / TypeScript 1.12 / .NET 1.7.0 / Ruby 0.5.0 or above.
The Remaining Hole for Very Long Workflows — Upgrade on Continue-as-New
One case is left that neither pinned nor auto-upgrade handles cleanly. The example the GA announcement gives directly: workflows that effectively never end, like entity workflows running for months, or AI agents that sleep for weeks between interactions. Leave them pinned and they are bound to that version forever; leave them auto-upgrade and you are back in patching hell.
Upgrade on Continue-as-New, released as public preview alongside GA, targets that hole. Many long-lived workflows already use Continue-as-New as a checkpoint, so the pitch is to use that boundary as the version upgrade point — each run executes pinned (no patching needed within a run), and when a new version becomes current/ramping, it is detected via GetTargetWorkerDeploymentVersionChanged (refreshed at the end of every workflow task) and the workflow switches to the new version at the next Continue-as-New. Each run finishes within one version, but the logical workflow crosses several. v1.31.0 also added the behavior of continuing onto a ramping version. Bear in mind, though, that this part is still public preview.
The Honest Tradeoffs
Do not let the GA stamp fool you into reading this as a free lunch. The bill looks like this.
First, pinned means rainbow deployments, and rainbow deployments are infrastructure cost. As long as pinned workflows remain, you have to keep that version's workers running. The docs themselves say that in a rainbow deployment two or more active versions run at the same time. A months-long pinned workflow means a months-long worker fleet, and since each deploy adds one more version, your operational surface grows accordingly. On Kubernetes, the Temporal Worker Controller, now GA, manages per-version fleets for you — but the compute cost itself does not disappear.
Second, auto-upgrade does not free you from patching. Exactly as the doc wording quoted above says. Even with versioning on, code changes to auto-upgrade workflows still demand replay-safety review and patch markers. The conclusion "it is GA now, so I can delete all my patched calls" is wrong.
Third, versions cannot pile up forever. Per the operational knobs in the v1.28.0 release notes, the default cap on versions per deployment is 100, and the notes warn that raising it beyond a few hundred is not safe (in contrast to the default of 100 deployments per namespace, which they say is safe to raise substantially). If pinned executions that never finish draining accumulate, version hygiene — emptying and deleting old versions — becomes a new operational job. Drainage status refresh is also on a 3-minute cycle by default, so it is not real time.
Fourth, adoption is a deploy-pipeline integration project. Half of versioning is not the server, it is your CD — stamping a Build ID on every build, and adding steps to your pipeline that call SetWorkerDeploymentRampingVersion and SetWorkerDeploymentCurrentVersion after deploy. You also have to satisfy the minimum version matrix above (server, CLI, and SDK alike).
Fifth, there are clear cases where you should not use it. If all your workflows are short enough to finish inside the deploy interval — if you can drain the task queue naturally and then deploy — an unversioned queue is enough. If workflow code changes are rare, a handful of patched calls is cheaper than operating per-version worker fleets. The break-even point for this feature is when "workflows that do not finish between deploys × frequency of code change" is large enough.
The Rest of the Same Release — Where the Engine Is Headed
For context, here is what else shipped in v1.31.0. Serverless Workers, where the server watches the task queue and directly invokes/scales AWS Lambda workers, landed as a pre-release (with a new server component called the Worker Controller Instance, disabled by default); the CHASM framework, which generalizes the server's internal state machines beyond workflows, was enabled by default (the applications on top of it are still off); Standalone Activities, which run activities on their own without a workflow, went to public preview; and task queue priority/fairness went GA. Nexus is now always on, with no feature flag. From workflow replay machine to general-purpose durable execution platform — the direction the release notes point in is consistent.
So Should You Use It
Compressed into three questions, the criteria are these.
- Do you actually have workflows that do not finish between deploys? If not, it ends here — live unversioned.
- Have you actually hit non-determinism errors at deploy time, or do patched branches keep drawing fire in code review? Then a pinned default converts that pain into a deploy topology problem.
- Do you have the infrastructure headroom to run per-version worker fleets? If you cannot absorb the compute cost of rainbow deployments and the version hygiene work, you may be better off slicing workflows shorter and tightening the Continue-as-New cycle.
There is only one misconception to guard against — versioning solves the deploy problem, not the retry problem. Temporal still retries activities, and the idempotency of their side effects (payments, email, tickets) is still on you. That story is covered in detail in Agent Production Failure Taxonomy and Idempotency and The Truth About Exactly-Once Semantics. Especially if your team is trying to wrap an AI agent that sleeps for weeks in a durable workflow, remember both that this GA's Upgrade on Continue-as-New targets exactly that scenario — and that it is still a preview.
Closing
To sum up. Deterministic replay is the source of durable execution, but the price is billed at deploy time — because the moment history and code diverge, you get a non-determinism error. Until now that price was absorbed by patch branches inside workflow code, and Worker Versioning GA moved it into deploy topology — per-version worker fleets and routing rules. The constraint did not disappear; it moved. Your code gets cleaner and your infrastructure gets thicker.
And the fact that it took one vendor three years and two discarded generations of API to arrive at this answer is probably the most honest testimony there is about how intrinsically hard safe deploys are on top of a replay model. Whether or not you adopt it, if you are choosing a durable execution engine, put "how does this engine let me change code while workflows are running?" on your first list of questions.
References
- Announcing GA for Worker Versioning and Public Preview for Upgrade on Continue-as-New — Temporal blog (2026-03-30)
- Temporal Server v1.31.0 release notes — Worker Versioning GA, V1/V2 sunset
- Temporal Server v1.28.0 release notes — public preview promotion and deprecation of the old APIs
- Temporal Server v1.24.0 release notes — version sets deprecated, versioning rules introduced
- Temporal Server v1.21.0 release notes — the first preview of Worker Versioning
- Worker Versioning concepts doc — Pinned/Auto-Upgrade, routing, drainage, inheritance rules
- Worker Versioning production guide — minimum versions, SDK configuration examples
- Python SDK Versioning docs — determinism requirements and patching
- Durable Execution & Workflow Engines 2026 — Temporal, Inngest, Trigger.dev, Hatchet, Restate, DBOS, Step Functions Deep Dive (related post)