Skip to content
Pyrula
Pre-release: the packages are not on PyPI yet, so pip install does not resolve. Build from source to try it today.

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= is auto_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 with incompatible_upgrade.
  • With version= is pinned. 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.

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 runSame version onlySame version or newer
On a newer workern/aShadow-replays history; adopts if compatible, else incompatible_upgrade
Use whenA change can’t be replayed safelyChanges 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.put and ctx.memory.delete are skipped while verifying. Writes made another way (a client in deps) do happen.
  • It is bounded by the lower of the agent’s timeout and max_attempt_wall_seconds, capped at 30 seconds. On timeout the run goes back to the queue unfailed; the third timeout ends it with replay_check_timeout. Hand-backs show as the replay_check_deferred outcome (metric) and, on Kafka, a hold with reason replay_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 with incompatible_upgrade.
  • Code that blocks without awaiting can’t be interrupted and stalls the worker’s loop.
  1. Deploy new workers alongside the old ones. New runs get the new version; old workers finish old runs.

  2. Wait for the old version to drain, then retire it:

    from pyrula.workflows.versioning import retire_version, wait_drained
    if wait_drained(store, "charge", old_version, timeout=3600):
    retire_version(store, "charge", old_version) # not retired while runs are in flight (force=True overrides)
  3. 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 WorkerOptions
from pyrula.workflows.versioning import StrandedPolicy
WorkerOptions(stranded_policy=StrandedPolicy.FAIL, stranded_timeout=3600) # seconds

They end with no_compatible_worker.

Replay real histories against the new code in CI with export_run_history and assert_replay_compatible (Deploy-safe versioning).

View this page as Markdown