Deploying Changed Workflow Code

On this page
Open the guided lab ↗

Note

This is the most volatile page in the course: Worker Versioning's API names and defaults have changed and will change again. Each is marked "(as of 2026-09; see upstream page)"; the concepts are durable, and the upstream guide is the reference of record.

Three features, three maturity levels. Read the table before the mechanisms, because the slide that mixes them up is the one that gets a production run pinned to a Worker that no longer exists.

Feature Status What it is
Worker Versioning (Worker Deployments, Deployment Versions, Current/Ramping) GA — 2026-03-30 per the course notes; the upstream docs snapshot of 2026-09 records the legacy API's removal but prints no GA date (see upstream page) Workers declare a build; the Service routes executions to a build and drains old builds. The legacy pre-2025 "Build ID sets" API is removed from the Server in March 2026.
Pinned / Auto-Upgrade Core concepts of GA Worker Versioning; every Workflow Type declares one Pinned completes on one version; Auto-Upgrade moves to the current version and must stay replay-safe with patching.
Upgrade on Continue-as-New Public Preview — "an experimental SDK-level option" (as of 2026-09; see upstream page) A pinned run learns a new Target Version exists and continues-as-new onto it. Mark it Preview on every slide that shows it.

Two months old

An AgentRun started in July is still open: parked at a checkpoint, woken by signals, continued-as-new twice. Today you deploy a change that inserts a verify_plan Activity before the first tool call. Tomorrow the July execution gets a Workflow Task, and the Worker that picks it up runs today's code.

Replay walks the July history and expects your code to emit the same Commands in the same order. Where the history says ActivityTaskScheduled: execute_tool, the new code schedules verify_plan instead: a non-determinism error, a failed Workflow Task, an execution stuck retrying it. Every fix in this module keeps old histories and new code in agreement, and module 8's replay test tells you whether you need one.

Patching: a branch tied to a marker

A Patch defines a logical branch in a Workflow for a specific change, similar to a feature flag. It applies a code change to new Workflow Executions while avoiding disruptive changes to in-progress Workflow Executions.

Using workflow.patched() inserts a marker into the Event 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. Upstream's precise semantics, condensed:

The July execution has no marker and takes the old branch for the rest of its life; an execution started after the deploy records the marker and takes the new one.

from datetime import timedelta
from temporalio import workflow

with workflow.unsafe.imports_passed_through():
    from agent_runtime.activities import execute_tool, verify_plan


@workflow.defn
class AgentRun:
    @workflow.run
    async def run(self, goal: str) -> str:
        plan = await self._plan(goal)
        if workflow.patched("insert-verify-step"):
            plan = await workflow.execute_activity(
                verify_plan, plan, start_to_close_timeout=timedelta(minutes=2))
        return await self._execute(plan)

Because patched() returns True for every fresh execution, put the newest code at the top of an if/elif chain (if patched("v3") ... elif patched("v2") ... else); reversed, new executions record v2 and never reach v3.

The patch lifecycle

Patching is a three-step process, and the steps are separate deploys.

  1. Patch in any new, updated code using patched(). Run the new patched code alongside old code.
  2. Remove old code and use deprecate_patch() to mark a particular patch as deprecated.
  3. Once there are no longer any open Workflow Executions of the previous version of the code, remove deprecate_patch().

Deprecated patches function similarly to regular patches by adding a marker to the Event History; however, this marker won't cause a replay failure when the Workflow code doesn't produce it. That lets you delete the else branch while step-1 executions are still open.

# Deploy 2: every open execution has the marker or is new; drop the old branch.
@workflow.defn
class AgentRun:
    @workflow.run
    async def run(self, goal: str) -> str:
        workflow.deprecate_patch("insert-verify-step")
        plan = await self._plan(goal)
        plan = await workflow.execute_activity(
            verify_plan, plan, start_to_close_timeout=timedelta(minutes=2))
        return await self._execute(plan)

Deploy 3, once pre-patch executions have left retention: no patched(), no deprecate_patch(), just the verify_plan call.

Patching works everywhere and needs nothing from your deployment system. Its cost is scar tissue: every patch is a branch carried through three deploys, and the replay test is the gate for each step.

Worker Versioning: the deployment does the versioning

Worker Versioning moves the problem out of the code: Workers declare which build they are, executions are routed to a build, and the Service keeps old builds alive until the executions that need them are done. Per current upstream docs it is the recommended default; if you can run versioned worker deployments, prefer it over patching, and fall back to patching only while you work toward a versioned deployment model. The course notes date its GA release to 2026-03-30; the upstream docs snapshot cited above describes the GA feature but gives no GA date. The pre-2025 experimental API is removed from the Server in March 2026; anything about "Build ID sets" is the legacy design.

The concepts, in upstream's terms:

Rolling deploys upgrade Workers in place and are incompatible with Worker Versioning; it is built for blue-green (two versions, instant rollback and ramping) and rainbow (more than two, so pinned executions drain at their own pace) deployments.

Configuring a Worker (as of 2026-09; see upstream page)

The Python shape, per the local docs clone and upstream sample:

from temporalio.common import VersioningBehavior, WorkerDeploymentVersion
from temporalio.worker import Worker, WorkerDeploymentConfig

worker = Worker(
    client,
    task_queue="agent-runs",
    workflows=[AgentRun],
    activities=[call_llm, execute_tool, reserve_gpu, release_gpu],
    deployment_config=WorkerDeploymentConfig(          # as of 2026-09; see upstream page
        version=WorkerDeploymentVersion(
            deployment_name="agent-runtime",
            build_id=os.environ["BUILD_ID"],
        ),
        use_worker_versioning=True,
        default_versioning_behavior=VersioningBehavior.UNSPECIFIED,
    ),
)

Each Workflow Type declares its behaviour at registration:

@workflow.defn(versioning_behavior=VersioningBehavior.PINNED)   # as of 2026-09; see upstream page
class AgentRun:
    ...

With default_versioning_behavior left UNSPECIFIED (as of 2026-09; see upstream page) you must annotate every Workflow Type; upstream suggests a migrating fleet default to Auto-Upgrade, the closest to legacy behaviour, until every type is annotated.

Rollout, ramp, rollback

Routing changes through the CLI (or the equivalent API), not by restarting Workers. Deploy v2 Workers alongside v1, check who is polling with temporal worker deployment describe, then:

temporal worker deployment set-ramping-version \
    --deployment-name agent-runtime --build-id "$V2" --percentage 10
temporal worker deployment set-current-version \
    --deployment-name agent-runtime --build-id "$V2"

Rollback is the same command pointed back at v1: ramp to 0, or set-current-version to the previous build. New executions stop landing on the bad version at once; pinned executions already on it keep running there until you move them with temporal workflow update-options --versioning-override-* (as of 2026-09; see upstream page), one execution or every match of a --query. "Reset-with-Move" combines a Reset with that override for a version-incompatible bad build.

Swipe to explore diagram →
Workers deployment v1 draining WorkerWorkerWorker Workers deployment v2 current WorkerWorkerWorker Task Queue agent-runs agent-42 agent-45 agent-47 PINNED → v1 new → v2 pinned, upgrades at continue-as-new continue_as_new Worker Versioning GA · Pinned / Auto-Upgrade Upgrade-on-CaN = Public Preview (as of 2026-09)
Worker Versioning: pinned executions stay on v1, new ones start on v2, a pinned agent upgrades at continue-as-new.

Long-lived agents: pinned plus continue-as-new

Upstream's decision guide has a row that is your agent runtime: a long Workflow using Continue-as-New should be PINNED and upgrade on Continue-as-New, and patching is then never required; its worked example is literally "AI agent / Chatbot". The other long-lived row, a Workflow that needs its full history and cannot use Continue-as-New, is AUTO_UPGRADE plus patching.

By default a pinned execution stays pinned across Continue-as-New: the pinned version is inherited across the chain while the new run's Task Queue belongs to the same Worker Deployment. The upgrade-on-CaN option changes that: each run remains pinned, the Server tells the Workflow when a new Target Version is available, and a run that continues-as-new with the option set starts on the Target Version. Upstream marks this Public Preview, "an experimental SDK-level option" (as of 2026-09; see upstream page) — the third row of the table at the top:

from temporalio import workflow
from temporalio.workflow import ContinueAsNewVersioningBehavior   # as of 2026-09; see upstream page


@workflow.defn(versioning_behavior=VersioningBehavior.PINNED)
class AgentRun:
    @workflow.run
    async def run(self, state: AgentState) -> str:
        while not state.done:
            state = await self._step(state)
            # Refreshed after each Workflow Task; check it where the loop already turns.
            if workflow.info().is_target_worker_deployment_version_changed():   # as of 2026-09
                workflow.continue_as_new(
                    state,
                    initial_versioning_behavior=ContinueAsNewVersioningBehavior.AUTO_UPGRADE,
                )
        return state.context_summary

Two upstream limitations matter for an agent. Moving is lazy: a Workflow must execute a step to receive the target-version-changed information, so an agent parked at a 72-hour checkpoint will not notice until it wakes (send it a Signal). And the new version must accept the old run's Continue-as-New input or fail on its first Workflow Task; keep AgentState additive.

If every v1 Worker is gone before its pinned executions finish, they cannot make progress. Upstream's runbook: stop the rollout, find affected executions with the TemporalWorkerDeploymentVersion and TemporalReportedProblems Search Attributes, then move them by versioning override or Reset-with-Move onto a build that can handle them. Read the runbook first.

Which to use when

Situation Use Why
Short Workflow, done before the next deploy PINNED Never patched; old version drains fast.
Medium Workflow, spans deploys, no CaN AUTO_UPGRADE + patching Moves to each version; must stay replay-safe.
Long-lived agent with Continue-as-New PINNED + upgrade on CaN Upgrades at the checkpoint; no patching.
Long Workflow needing full history, no CaN AUTO_UPGRADE + patching Cannot use the CaN boundary.
Rolling deploys, not blue-green/rainbow Patching Rolling is incompatible with Worker Versioning.
Bug inside an already-pinned execution Patch, then versioning override Pinning cannot change code already running.

Version by default; patch when you must; replay-test every patch. Versioning removes the need for many patches, not for testing.

What the lab will test

Draw a line between old history and new Workflow code. Predict whether patching, routing, or Continue-As-New lets an existing run cross it safely; Lab 9 provides three concrete cases.

Try it

Lab 9 lives in labs/09-deploying-changed-workflow-code/ and has three exercises:

Sources and license

This page adapts material from Temporal's MIT-licensed documentation and samples (© Temporal Technologies Inc.; © Uber Technologies, Inc.). Adapted text is rewritten for this course; the upstream pages are the reference of record and may have changed since the commit linked here. Temporal is a trademark of Temporal Technologies; this course is independent and not endorsed by Temporal.