Versioning and deploys
Every run is stamped with its workflow’s version at submit: your version=, or by default a
12-character hash of the function body. The version decides how the run is claimed after a deploy.
- No
version=isauto_upgrade. After a restart, a worker on the new code replays each in-flight run’s history against the new body and adopts it. A compatible edit finishes the run; an incompatible one (renamed or removed step, changed inputs) ends it withincompatible_upgrade. - With
version=ispinned. Only a worker with the matching version claims the run.
The store ranks each version of a workflow by first sighting. A worker adopts a run when its version ranks the same or higher, so old and new workers coexist in a rolling deploy. An old worker leaves a newer run pending.
After a rollback, runs stamped by the newer code stay pending until a worker on that version
drains them or you cancel them. The worker logs stranded_runs_detected meanwhile.
A version the store has never seen ranks newest. A locally edited body, or an old pod rebuilt so
its implicit hash changed, adopts every stamped run backward, which likely ends in
incompatible_upgrade. Keep explicit versions stable across rebuilds.
The replay check re-runs body code outside steps, so a print or log line there runs again.
Choosing a policy
Section titled “Choosing a policy”from pyrula.workflows import workflow
@workflow(name="charge", version="2026-10-01", version_behavior="auto_upgrade")async def charge(ctx, amount_cents: int) -> str: ...pinned (default with version=) | auto_upgrade (default without) | |
|---|---|---|
| Who claims an old run | Same version only | Same version or newer |
| On a newer worker | n/a | Shadow-replays history; adopts if compatible, else incompatible_upgrade |
| Use when | A change can’t be replayed safely | Changes are additive or guarded with ctx.patched |
Changing the arguments of a recorded call is replay-incompatible: the shadow replay ends the run
with incompatible_upgrade, detail starting replay_input_changed:. See
when a recorded call’s inputs change.
@agent takes the same version= and version_behavior=. The check replays the stored turn
journal against the new body without calling the model or running a tool. Dropping, renaming or
reordering a recorded call ends the turn with incompatible_upgrade. The check runs your body
up to its first new call, so that code runs twice:
ctx.memory.putandctx.memory.deleteare skipped while verifying. Writes made another way (a client indeps) do happen.- It is bounded by the lower of the agent’s
timeoutandmax_attempt_wall_seconds, capped at 30 seconds. On timeout the run goes back to the queue unfailed; the third timeout ends it withreplay_check_timeout. Hand-backs show as thereplay_check_deferredoutcome (metric) and, on Kafka, a hold with reasonreplay_check. - A dependency failure before the first new call (
StoreUnavailable,Transient,ConnectionError,TimeoutError,socket.gaierror) is an ordinary attempt error under the worker’s retry policy. Any other error there is a divergence and ends the run withincompatible_upgrade. - Code that blocks without awaiting can’t be interrupted and stalls the worker’s loop.
Shipping a change to a pinned workflow
Section titled “Shipping a change to a pinned workflow”-
Deploy new workers alongside the old ones. New runs get the new version; old workers finish old runs.
-
Wait for the old version to drain, then retire it:
from pyrula.workflows.versioning import retire_version, wait_drainedif wait_drained(store, "charge", old_version, timeout=3600):retire_version(store, "charge", old_version) # not retired while runs are in flight (force=True overrides) -
Stop the old workers.
Runs a worker can’t serve wait by default. To fail them after a deadline, set a stranded policy:
from pyrula.workflows import WorkerOptionsfrom pyrula.workflows.versioning import StrandedPolicy
WorkerOptions(stranded_policy=StrandedPolicy.FAIL, stranded_timeout=3600) # secondsThey end with no_compatible_worker.
Proving a change is safe
Section titled “Proving a change is safe”Replay real histories against the new code in CI with export_run_history and
assert_replay_compatible (Deploy-safe versioning).