All notable changes to the durable-workflow Python SDK are documented here.
The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.
- Read-only acknowledged heartbeat checkpoint accessors distinguish unsupported claims from an explicitly empty checkpoint. Received details, contract version and acknowledgement time remain immutable across heartbeat outcomes. This source addition still requires package publication and installed replacement qualification. (durable-workflow/.github#218)
ActivityRetryPolicy.exponential(...)creates generated activity retry schedules with a finite 60-second default maximum. It rejectsNoneor nonfinite maxima and retains the existing wire shape and interval rounding. The legacy constructor, explicit backoff lists and policies already recorded in history keep their original values. (durable-workflow/.github#219)
- The query client guide, SDK reference and API docstring now document the Server receipt returned by
Client.query_workflow(),WorkflowHandle.query()and the synchronous client. Readreceipt["result"]for the decoded handler value, including a successfulNone. Runtime behavior is unchanged. (#316, #317) - Query workers return valid Avro values containing bytes without also trying to serialize those bytes as plain JSON. The authoritative
result_envelopeis unchanged; JSON-compatible results retain their optional plain projection. (#319)
- Replay refuses explicit
parallel_group_policyhistories before constructing application state until a qualified policy consumer is enabled. Absent-policy histories retain their wait, failure-selection and clock behavior. Explicit propagation is being prepared behind disabled worker admission. (#307)
- Experimental timeout-cleanup parser/exception preparation (workflow#863, workflow#1129, server#1201). This patch advertises no timeout-cleanup capability and does not qualify producer delivery or enable timeout cleanup.
CancellationContext.from_dict()reads the context's optional top-levelcause: absent or"cancellation"is an ordinary cancellation,"timeout"marks a timeout-cleanup request (the run passed its work deadline and is now in cleanup), and any other value raisesValueError. The context carries no other timeout fields: its existingcleanup_deadline_atis the cleanup deadline, and the timeout kind and work deadline stay on theWorkflowTimeoutCleanupStartedevent.causeandis_timeoutare readable on the context, andto_dict()writescauseback exactly as received, including an explicit"cancellation", leaving ordinary contexts byte-identical. A timeout request is raised at the authored call as the new experimentalWorkflowTimeoutCleanupRequested, a subclass ofWorkflowCancelled, so existingexcept WorkflowCancelled:handlers, such as saga compensation, run on timeout too. It staysBaseException-rooted and is not anasyncio.CancelledError. Testcause, the cross-SDK discriminator, to tell the two apart.causeis never inherited: a child cancelled under a timeout request, scoped or not, receives an ordinary cancellation withoutcause.
-
Clientrefuses suppliedtoken,control_tokenandworker_tokenvalues with edge whitespace or control characters before choosing a role or opening an HTTP client. The error names only the option. Values are never trimmed, and empty values retain their existing fallback behavior. (#304) -
Replay supports histories in which a run's own close settled the work it never awaited (server#1193, workflow#1117/#1121, Workflow 2.28.0). When a run completes, fails or continues as new, each still-open activity, such as a
select()loser, gets anActivityCancelledwithcancellation_reason: run_closedjust before the terminal event. Replay, query and update replay now skip that cancellation and leave the activity open, as the workflow saw it. It never resolves a selection member, never supplies replay time, and is never taken as a list group's first positional failure. Before this fix, a close's cancellation of a lower-index member could replace the failure the workflow handled. Replay also accepts the newabandoned_activitieskey onWorkflowCompleted,WorkflowFailedandWorkflowContinuedAsNew, which lists abandon-policy activities left running at close. -
Activity workers treat a
run_closedrefusal as final. A completion refused withrecorded: false, reason: run_closedis split on itsactivity_status, as in the PHP worker. When it iscompletedorfailed, the activity was an abandon-policy activity that kept running after its run closed and its own outcome closed it: the task counts as completed and logs at info level. Otherwise the close cancelled the activity: its outcome is dropped, the task is reported as aborted (never completed), and a warning is logged. A failure or cancellation report refused withrun_closedis not reported again and logs at info level. A claim refused withrun_closedcounts as an answered poll, with no failure backoff. A heartbeat refused withrun_closedraisesActivityLeaseAuthorityLostand publishes no outcome, as before.
-
Client.update_outcome()andClient.update_outcome_receipt()read an accepted update's outcome later by its update id withGET /api/workflows/{workflow_id}/updates/{update_id}, and never submit the update again (durable-workflow/.github#212, server#1131).update_outcome()waits for the outcome by default and returns the decoded result, includingNone, with optionalresult_typedecoding. A still-pending update raises the newUpdateStillPending, which carriesworkflow_id,run_id,update_name,update_id,request_idandupdate_statusand tells the caller to read again by update id; it is never returned asNone. A failed update raisesUpdateFailed, a rejected oneUpdateRejected, and an unknown or pruned id the newUpdateNotFound(a subclass ofUpdateFailed).update_outcome_receipt()returns the raw receipt and answers at once by default. Both need a Server that advertisescapabilities.update_outcome_reads(native 3.6.0 and later); on an older Server they raiseRuntimeCapabilityUnsupportedafter discovery, without reading or resubmitting anything.WorkflowHandleand the synchronous client and handle have the same methods. -
Update outcome reads check the request and answer. An empty,
.or..workflow or update id, an unknownwait_foror a non-positivewait_timeout_secondsraisesValueErrorbefore any request, including discovery, so..can never resolve onto the workflow's describe route. A successful answer must name the requestedupdate_id, carry a stringupdate_statusand, when it echoes aworkflow_id, match it.update_outcome()returns a value only for a completed receipt. Any other answer raises the newUnrecognizedUpdateOutcomewithworkflow_id,update_idandbody, and never returns a value. Withwait_timeout_seconds, the HTTP timeout becomes that wait plus 30 seconds whenever that is longer than the client'stimeout, so a long Server-side wait is not cut off.
- The
Client.update_workflow()andWorkflowHandle.update()docstrings now say what they always did: they return the Server receipt, not the decoded result, and the Server answers atwait_for="accepted"when it is omitted. A receipt withupdate_status: "accepted"is a pending update even though itsresultisNone.
- A workflow-task completion the Server refuses as too large (Server 3.6.0's typed
413 payload_too_largewithlimit_kind: workflow_task_completion_bytes, over 16 payload limits, 32 MiB by default) now raisesWorkflowTaskCompletionTooLargewith the Server'slimitand the encoded requestsize(durable-workflow/.github#228). The worker no longer reports this as a workflow-task failure, which made the Server retry the same oversized completion or block replay. It never resends the refused completion: it completes the same task once with a non-retryablefail_workflowcommand whose message starts withStructural limit exceeded: workflow_task_completion_bytes:, so the run fails with thestructural_limitfailure category and guidance to split the fan-out, use child workflows or continue-as-new. If that follow-up is rejected too, the worker logs it and never calls/fail. Other 413 responses and 5xx retries are unchanged.
- Workers with a sticky cache (
sticky_cache_capacity > 0) advertisesticky_history_suffix(#300, server#1017). On Server 3.5.0 or later, a sticky hit receives only the history after the sequence the worker cached, instead of the full history on every workflow task. A suffix is used only when it continues the cached history exactly; otherwise the worker drops the entry and refetches full history.
Worker.run_until()now records the schedule-to-start histogram for activity tasks, asrun()already did (#188).
- Local activities no longer retry with zero delay once their backoff list runs out. Each later retry repeats the final backoff entry, as Server-scheduled activities do (durable-workflow/.github#219).
-
Worker schedule-to-start histogram
durable_workflow_worker_task_schedule_to_start_secondsfor workflow and activity dispatches, tagged only withtask_kindandtask_queue. It requires Server'sclaimed_task_ready_agecapability and a valid integerready_age_ms, records Server-computed milliseconds as seconds, preserves real zero and omits missing/invalid samples. The shared metrics contract is pinned at revision 2. Published-Server and installed-package acceptance remains pending in #188. -
Condition wait opens carry a deterministic
python:condition-wait:{ordinal}occurrence ID for each authored call, including selection members. False-wake reopens preserve the first open's ID and remaining timeout; legacy chains without an ID keep that absence even when later physical opens have an ID. Separate calls and grace waits receive new ordinals. These identities enable unresolved-open deduplication on supporting runtimes. (#259) -
Opt-in typed workflow values (#184).
@workflow.defn(..., typed=True)decodes the arguments ofrun(afterctx) and of every signal, query, update and update-validator handler to their annotations.ctx.start_child_workflow(..., result_type=T)decodes a child's result, andWorkflowHandle.result(result_type=T)/Client.get_result(..., result_type=T)decode a workflow's result on the client. Dataclass andEnumvalues in client workflow, signal, update, query and schedule arguments, child and continue-as-new arguments, and workflow and update results are sent as their neutral values. A typedTypedValueErrornames the path but never the value. Untyped workflows receive plain values as before, so in-flight histories replay unchanged. -
Typed run-activity visibility through
Client.get_run_activitiesandWorkflowHandle.get_activities, with activity/attempt metadata, exact run/namespace binding, explicit unavailable fields and preserved diagnostic authority. Standalone activity listing is unchanged. Shared delivery remains tracked in durable-workflow/.github#235. -
ActivityInfo.activity_execution_idexposes the received logical activity identity without task/attempt/session fallbacks. Prepared local callbacks receive their validated admission ID; inline local callbacks and older claims without an ID exposeNone. These IDs are public idempotency metadata, not live-claim or external-effect authority, and replace no credentials, heartbeat/current-attempt checks, per-effect suffixes or outcome reconciliation. Shared identity acceptance remains tracked in durable-workflow/.github#211. -
Lazy
Client.iter_workflows,Client.iter_historyandWorkflowHandle.iter_historyfollow opaque page tokens while retaining only the current page and bounded cursor bookkeeping. They preserve run/filter/namespace identity, follow empty continued pages, stop on early close, and surface HTTP, cancellation and invalid/cyclic cursor errors. Existing one-page methods keep their contracts. (durable-workflow/.github#247)
-
Replay consumes an already-open duplicate of a condition wait when the older physical wait is satisfied with a true predicate. A wait opened after that satisfaction remains a separate authored call with its own timeout. (#256)
-
Worker batch encoding converts dataclass and Enum workflow results, update results and continue-as-new arguments to neutral Avro values, matching individual command encoding. Typed results previously failed during real Worker submission. (#184)
-
wait_conditionpredicate fingerprints no longer change across Python versions or worker processes. Every earlier release hashed the predicate's bytecode and thereprof its constants. So upgrading Python (for example 3.12 to 3.13) failed replay of every in-flight run at its first wait, and a predicate containing a nested function (a generator expression, a nested lambda, or a comprehension on 3.10 and 3.11) got a new fingerprint in every process, so a restart or cache loss failed replay withcondition_fingerprint_changed. New waits record a v2 fingerprint (dwpy2:sha256:...) computed from the predicate's module, qualified name and a canonical form of its source AST, with no bytecode, addresses, paths or line numbers. Without source (for exampleevalor a REPL) no fingerprint is sent, and the wait is identified by its key and call order. A v1 value recorded by an earlier release still matches when this process computes the same v1; otherwise it is accepted as unverifiable instead of failing replay, so drift in those predicates is no longer detected. A reopen sends the fingerprint its first open recorded, so a wait opened before the upgrade keeps its v1 value, and a keyless wait links a v1 value only within its own chain. A keyless wait without source links its false-wake reopens within its chain, as before. Rollback and mixed fleets: a run that opened a wait under this release records a v2 fingerprint, which 2.14.0 and earlier refuse on replay. Don't roll a task queue back below this release once such waits exist, and upgrade every worker on a queue together. (#258) -
Same-line
wait_conditionlambdas whose bodies contain list, dict or set comprehensions share one combined fingerprint on every Python version. Put those predicates on separate lines to detect a change from one candidate to another. (#258)
Upgrade from 2.13.2 and earlier. Every earlier release fails replay when a condition wait has an orphaned duplicate open whose timer fires later, which Server 3.0.x can record (#253 below).
- Replay no longer fails when a condition wait has an orphaned duplicate open whose timer fires later. Server 3.0.x can record a second open of the same wait while the first is still open (server#1015). The first open's timeout used to end the logical wait in replay, while history continued with the reopened wait, so replay raised
NonDeterministicReplayError. A wait now counts as reopened when the next open of the same wait was recorded before the earlier wait's own end. A wait that timed out before the next open is still finished, and the next open is a separate call. Every earlier release has this gap; it is not a 2.13.2 regression. (#253) - Replay now refuses a recorded condition-wait reopen that records a longer timeout than the wait's first open, or that adds or drops the timeout, with a
NonDeterministicReplayErrorwhose detail startscondition_wait_reopen_timeout_exceeded. Earlier versions linked such a reopen silently. Reopens with the full timeout (written before 2.13.2) or with the time remaining (2.13.2 and later) still replay. This matches sdk-php#271. (#248) - Typed-value refusals never repeat user data. A map entry is named by its position (
scores[entry 1]), not its key, and a dataclass whose__post_init__raisesValueErroror a payload nested too deeply fails with a non-retryableTypedValueErrorthat does not carry the original message. (#252)
- Opt-in typed activity values (#184).
@activity.defn(..., typed=True)decodes an activity's positional arguments to its parameter annotations, andctx.schedule_activity(..., result_type=T)decodes the completed result in single calls, parallel groups and selections. Supported types are dataclasses,Enum(includingIntEnumandStrEnum),Optional[T],list[T],dict[str, T]and scalars; other annotations raiseTypeErrorup front. Undeclared keys are ignored and missing fields take their defaults. A mismatch raises the newTypedValueError, which names the field path but never the value. It is non-retryable in a typed activity and is raised at theyieldforresult_type. Dataclass andEnumactivity arguments and results are now sent as their neutral values, the same bytes asdataclasses.asdict()with each enum's.value; these values were refused before. Untyped activities and calls receive plain values as before, so in-flight histories replay unchanged.
- A
wait_conditiontimeout now counts from when the wait first opens. Before, a signal that left the predicate false reopened the wait with the fulltimeoutagain, so every such signal pushed the timeout later: a 300-second wait signalled after 2 minutes timed out after 7. The reopen now carries the seconds left before the first open's deadline, rounded up and never below 0, measured at the latest recorded history time. Histories without recorded times keep the authored timeout, and histories written by earlier versions still replay. (#245) - Replay now enforces the recorded search attributes (Workflow protocol 1.20 replay identity). A workflow whose
upsert_search_attributesdiffers from itsSearchAttributesUpsertedhistory fails replay with aNonDeterministicReplayErrorwhose detail startssearch_attribute_value_mismatch, as memo replay already does for memos. Values are compared as Core'sUpsertSearchAttributesCallnormalizes them (trimmed strings and list items, a blank string as null) and with their types (1,1.0,Trueand"1"differ). An authored tuple is checked as the JSON list it is sent as. Values outside that portable domain, on either side, are refused rather than coerced: non-string or non-portable keys, objects, sets, bytes, maps, non-string list items, non-finite floats and out-of-range integers. ASearchAttributesUpsertedevent withoutattributesis refused rather than skipped, with a detail startingsearch_attribute_update_missing. One without a workflow sequence is refused withdurable_command_sequence_missing(ordurable_command_sequence_invalidfor zero, negative, string, float or bool) rather than given one, and a second upsert at the same sequence withduplicate_search_attribute_update. An upsert that shares its workflow sequence with any other durable command record is refused withdurable_command_sequence_collision. Every lifecycle row that carries a command's sequence claims it, including activity heartbeat, retry and cancellation-acknowledged rows, child cancellation rows, service operation scheduling, cancellation scope openings, continue-as-new, and cooperative and scope cancellation deliveries, which claim every sequence in theirsequence_span(absent or null means 1, as in Core; any other value that is not a positive integer is refused withdurable_command_sequence_invalid); events that only point at a step, such as a signal receipt, an update or a cancellation request, claim no sequence. When the history recordsattribute_types, it must type exactly the update's non-null keys, and each type must be a known type that can hold the value; anything else, including a malformed type value, fails withsearch_attribute_type_mismatch. The SDK's upsert declares no types and the Server records a key's namespace-registered type ahead of the inferred one, so a string may be recorded askeyword,stringordatetimeand an integer asintorfloat; search-attribute type identity is therefore not pinned for Python. History withoutattribute_typeskeeps value-only identity. Replay diagnostics name reasons, keys, JSON types, sequences and event types, never a recorded or authored value. (#235)
- 2.13.1 claim withdrawn (#237). The 2.13.1 entry said "The cancellation clock prefers a reused step's
reused_recorded_at." As an observable result, that is incorrect. The test behind it was not a history Workflow 2.19 can write: it had no redriveWorkflowStartedand delivered the cancellation before the reused step. Redrive writes the successor's start and its reused prefix together, so a successor cancellation always follows the prefix andremaining()is computed from that delivery. A reused event'sreused_recorded_attherefore cannot reach the budget, and the preference changes no observable result. Replay behavior is unchanged. The 2.13.1 start-clock change (ctx.now()fromreplayed_started_at) is unaffected. Valid successor fixtures replace the evidence, and the 2.13.1 fixtureredrive-start-replayed-clock, which has only a partial redrive envelope, is registered as non-qualifying. - Redrive successor evidence is checked against Core's full cancellation shape (#239). The test check for a successor's own cancellation now requires what Workflow 28893f97 writes: a root
durable-workflow.cancellation-context/v1context whose lineage is exactly the request's own address, a nullparent_request_id,requested_atno later than the request,command_typerequest_cancellationon the request, and a single-callcall_kindon the delivery that matches the recorded call. A delivery to a call resolved before the request is refused, and so is any event recorded at an unscheduled step the delivery consumed. The unreleased #236 fixtureredrive-reused-upserts-cancellation-clockhas nocommand_type, so it is registered as non-qualifying andredrive-successor-reused-upserts-cancellation-clockreplaces it. Tests and fixtures only; replay behavior is unchanged.
- Replay of Workflow 2.19 redrive reuse is qualified for
MemoUpsertedand forSearchAttributesUpsertedvalue identity. Neither event is a replay clock source:ctx.now()stays the persisted start time, which on a redriven run is the failed run's start, and the cancellation budget does not move on an upsert. Search-attribute type identity is not qualified. The SDK's upsert declares noattribute_types, and the Server records namespace-registered types the SDK cannot see, so recorded types are not compared with authored ones. They are only checked to be well formed and able to hold the recorded value. Signal waits (SignalWaitOpened,SignalReceived,SignalApplied) are scoped out for Python, because the SDK cannot author a Core signal wait. Reused signal-wait events replay for history validity only: a cold replay applies the recorded signal once, takes no time from these events and opens no wait again. This does not qualify Python workers for Core signal waits. (#235)
- A run created by redrive now replays from the failed run's start time.
ctx.now()andctx.uuid7()readWorkflowStarted.replayed_started_atwhenrecovery_kindisredrive, as Core does, so workflow code before the failed step sees the same clock as the failed run. An invalid value is refused rather than ignored. The cancellation clock prefers a reused step'sreused_recorded_at. (#232)
Worker.status()returns a read-onlyWorkerStatussnapshot for health endpoints and orchestrator probes. Readiness requires this worker's own acknowledged registration, running pollers and a successful latest Server exchange per role; liveness reports event-loop progress, exited pollers and stalled requests. Snapshots perform no network I/O and can be read from another thread. AddsWorkerStateandWorkerRoleStatus, a runnable probe example and a Kubernetes guide. Existing worker behavior is unchanged.
- History-bundle verification and bundle simulation now fail a
retention-pruned export (
workflow.details_state: "pruned") with thehistory.details_prunederror, matching PHP Core, instead of reporting it asok. Retained bundles and older exports without the marker are unchanged.
- Declare minimum stable Server releases per major, including native Server 3.x, while preserving the legacy 2.x declaration. Include the compatibility record in installed wheels and source distributions and verify it against release source. Worker protocol and capability negotiation remain unchanged.
- Report exceptions with empty or whitespace-only messages using meaningful fallback text, so workflow and activity failures reach callers instead of being rejected as invalid failure reports. Preserve the exception type, stack trace and task identity in ordinary and supervised activity workers.
- Apply the same blank-message handling to invocable activities, queries and update validators while keeping nonblank diagnostic text unchanged.
- Preserve the committed condition reopen when another signal arrives during a signal's completion. Replacement workers replay both signals exactly once without skipping durable waits or weakening definition-drift checks.
- Expose immutable received retry policy and original attempt metadata through
ActivityInfo.retry_info, with explicit unknown, legacy-null and unlimited states and a last-configured-attempt check. - Expose nullable original workflow/run IDs in remote, inline and prepared activity contexts without substituting task or activity-attempt IDs.
- Add an optional
run_idto async and sync signal calls, with canonical activity guidance for stable business-event keys and delivery reconciliation.
- Preserve the bound run when
WorkflowHandle.signalsends a signal, instead of silently targeting whichever run is current.
- Bound each worker poll HTTP attempt with an absolute deadline, including responses that keep sending progress. Preserve cancellation, retry budgets and the original poll identity.
- Accept negotiated poll-progress responses alongside ordinary JSON, applying their original status and retry headers before normal error handling.
- Warn when a connection failure or gateway error cuts a poll short, naming the observed duration and configured wait.
- Keep the bounded
Worker.run_untilhelper polling after a matching typed backend-lock refusal. Backoff stays inside the original caller deadline, releases reserved capacity and stops promptly on cancellation. Malformed, foreign and fatal refusals retain their existing error behavior.
- Bound failure messages, stacks and diagnostic payloads before reporting them. The complete encoded request defaults to 1 MiB, including JSON escaping and markers, and respects lower configured or discovered limits. Preserve failure type, retry policy and claim identity; refuse an identity that cannot fit.
- Keep worker deregistration inside the remaining shutdown deadline. When the runtime advertises generation-fenced receipts, retry matching transient or lost responses with the original registration token. Without advertised fencing, use one legacy attempt. Exhausted or uncertain shutdown keeps registration removal unconfirmed instead of reporting success.
- Activity and child mock failures in
WorkflowEnvironmentnow reach authored workflow handlers through replay. Explicit retry budgets invoke each attempt without sleeping; per-call counts and a configurable cap keep tests bounded. - Synchronous callable child workflow mocks alongside canned child results.
- Nexus operation mocks in
WorkflowEnvironment, keyed by endpoint, service and operation. Register canned responses or synchronous callables; workflow code receives completion, acceptance orNexusOperationFailedthrough replay. - Explicit verified
tls_contextconfiguration for async and synchronous clients, including cloned query clients and runtime payload transfers. Applications can add a private CA while retaining certifi's public roots.
-
Name exception classes and static routes in poll warnings, including message-less transport failures, while preserving credential-safe summaries. Record local request cancellation as
CancelledErrorinstead ofpending, including cancellation during transport retry backoff. -
Verify schema-2 history exports, including the lossless Avro memo authority and its JSON projection. Retain schema-1 support, reject malformed or unknown versions and preserve integrity-only promotion limits. Match the published canonical JSON encoding for Unicode and finite floats in checksums/signatures.
-
Bound client metric routes to reviewed operation templates, including update-validation tasks, worker sessions, streams and Nexus operations. Unknown paths emit
unknown; static poll routes retain/poll. Variable identifiers and query strings do not create metric series. -
Honor explicitly configured and inherited metric recorders even when they evaluate to false, rather than silently replacing them with the no-op.
-
Back off workflow, activity and query polling independently after repeated HTTP or transport failures, with jitter and a 30-second cap. Successful polls reset the delay, and repeated authentication warnings are rate-limited.
- Require a patched certifi trust bundle that removes the affected emSign root. Installing this version upgrades older certifi resolutions too. Normal TLS and hostname verification and explicit private certificate authorities remain unchanged.
- Read-only
WorkflowContext.workflow_idandrun_idproperties expose the original task-bound identity during workflow execution and cold replay. Continue-as-new successors keep the workflow ID and receive their own run ID. IDs in business arguments remain separate, including numeric strings.
- Capability-gated activity runtime lease renewal, including automatic Worker maintenance during loading, callbacks and publication. Preserve the original task, attempt, owner and business deadlines without recording application progress or adding a protocol floor. Older runtimes retain their behavior.
- A bounded
Client.renew_activity_task_leasemethod using the connected runtime's affirmative discovery and the Client's actual transport credentials.
-
Stop settled activity maintenance independently of cancellation, including callbacks that catch cancellation. Late outcomes remain fenced, and a running synchronous callback keeps its executor slot until it physically exits.
-
Stop default async and synchronous activities when a heartbeat explicitly refuses continued execution. Preserve the Server reason separately from cancellation and fence completion/failure even when user code catches the refusal. The worker frees its slot after the callback exits.
-
Reject canonical version markers missing their change ID or supported-range bounds. Consistent historical duplicate ranges may still differ when each contains the frozen version.
-
Share one frozen numeric version across
get_version,patchedanddeprecate_patchfor the same change ID, while validating each call's range and preserving retained duplicate-marker sequences.
- Optional execution telemetry and standard logging correlation for workflow, query and activity scopes. The telemetry extra installs only the API; applications own providers, exporters, sampling and shutdown.
- Bounded W3C propagation gated by the Server's actual observability capability. Recover run parents and validated dispatch/command/outcome links only after original poll identities and existing authority fences. Baggage defaults empty and is never carried on links.
- Isolated async/thread contexts and fresh provider factories for supervised callbacks. Cold replay recovers the run parent without re-emitting completed activity attempts. Application request IDs and business values are preserved.
- Preserve workflow entry argument-binding refusals before application code runs, with truthful Worker capability discovery for compatible deployment routing. Workflow body exceptions remain ordinary workflow failures.
- Run synchronous remote activities outside the asyncio event loop so other workflow tasks, polls and heartbeats can progress.
- Reject unsupported coroutine signal/update handlers and generator handlers before registration or replay, including inherited and wrapped handlers. Guard returned values without removing the Worker's existing asynchronous query and update-validator support. Local synchronous query/validation helpers refuse and close returned coroutines.
- Raise
WorkflowAlreadyStartedfor the current Server'sinstance_already_startedduplicate-start reason, retaining support for the historicalduplicate_not_allowedreason. - Correct start-policy documentation to
failanduse-existing, including active versus closed instances and preservation of original input/deadlines. Start admission and workflow lifecycle behavior are unchanged.
- Cooperatively cancel a completion-waiting Nexus service call at its original
durable boundary when the runtime advertises the owned
workflow_runbinding. Preserve the original request, claim-release receipt and cleanup deadline.
- Commit workflow-authored Nexus admission with the original leased workflow task. Pending calls wait for durable service history, and cold replay retains the original call instead of invoking the operation from the Worker.
- Preserve typed terminal results, failures, external arguments and recorded legacy side-effect outcomes across Nexus replay.
- Select the legacy patch branch when an existing unmarked history has already reached its next durable operation, preserving pending activities without scheduling them again.
- Reuse one version decision per change ID and validate each call's supported range. Repeated patch calls now record one marker on new histories.
- Replay consistent duplicate markers written by earlier Python SDKs without rewriting their histories or shifting committed cancellation boundaries. Conflicting decisions remain explicit replay errors.
- Opt-in sticky execution with bounded entries, encoded history bytes and TTL. Retain durable history and validate prefixes before reusing Server-issued page cursors. Eviction, expiry, replacement and mismatches use cold replay.
- Cache counters and a runnable sticky execution example. Server 2.5.10 exposes
the replay modes needed for warm history reuse. Workers keep the cache disabled
unless
sticky_cache_capacityis positive.
- Preserve accepted update handler failures as
UpdateFailed, including the failure message, HTTP status, Server response and workflow, run, update and failure identities. ExistingInvalidArgumentcatches remain compatible.
- Acknowledge replay waiting on an already pending timer, activity or child through the existing waiting-for-history protocol. Empty command lists no longer produce rejected workflow task completions.
- Reject message-stream completion metadata without commands before sending a waiting acknowledgement, preventing stream progress from being discarded.
- Keep already scheduled scalar timers, remote activities and child workflows pending during signal wakeups and cold worker replay. Replay no longer emits another scheduling command for the same recorded operation, including shielded cancellation cleanup timers.
- Reject changed timer delays when history records the original delay, while preserving compatibility with older histories that omit it.
- Honor the configured Client HTTP timeout for ordinary control, discovery and worker requests. Explicit long-poll timeouts retain their existing grace.
- Preserve the configured worker poll window after Server discovery for workflow, activity and query tasks. HTTP grace remains separate, so short polls release Server admission slots when requested.
- Request bounded workflow history pages in Python workers so long valid histories no longer require an unbounded Server poll response. A worker now stops rather than replaying partial history if a later page fetch fails.
- Keep up to 1,024 verified external payload references in the default worker replay cache while retaining the 16 MiB byte limit. This avoids repeated downloads for histories with many distinct external payloads.
- Return completed workflow results and typed failures when their terminal event is beyond the first Server history page. Clients and workflow handles can request further pages with the Server's opaque page token.
- Retry workflow-task heartbeat after a compatible Server reports temporary database loss with an unknown outcome. The worker checks the exact task, lease owner, and attempt before resending the same request, including on path-prefixed managed runtime URLs. Server 2.4.11 adds this response; older Servers retain their existing behavior.
- Workers recognize Server's typed long-poll capacity response for workflow, activity, and query polls. They release the poll reservation and wait for the advertised retry delay without duplicate transport retries or an error warning. Unrelated or malformed rate-limit responses keep their existing error path.
- Async and sync clients and selected workflow handles can redrive a failed run by source run ID, with an optional idempotency request ID. This requires a Server that exposes the run-targeted redrive operation.
- Cold replay reuses completed activities copied into a redrive successor without scheduling them again. An uncaught recorded activity failure carries its failed-step identity; handled or translated failures do not claim one.
- On supporting Servers, retry a draining refusal for a completion payload with its exact activity, workflow, or query lease and immutable payload slot. Client uploads, unknown capabilities, and hard storage fences remain blocked.
- Preserve computed outcomes during late upload pressure. Existing storage retries stay interruptible and do not execute handlers again; Server upload allowances, namespace quotas, and stale-lease errors remain authoritative.
- Workers now retry retryable storage-admission refusals without re-executing handlers or changing task identities. Completed outcomes and prepared payload uploads survive acknowledgement retries; successful uploads are not repeated.
- Registration, polling, heartbeat, and completion retries remain interruptible during shutdown. Authentication and stale-lease failures remain terminal.
- Persisted execution and run deadlines now raise the exported
WorkflowTimedOutexception instead of returningNonefrom workflow results. This terminal runtime outcome is distinct from the caller's pollingTimeoutError; selected history and other terminal exceptions are unchanged.
- Cold replay now treats an intervening durable step as the authoritative boundary for later signals and updates, even when they retain an older condition sequence. Post-condition activities can therefore complete before those receivers advance the workflow without rebinding them to a closed wait.
The Python SDK 2.0.0 promotes the qualified release candidate to the stable
2.0 line. This Avro-only SDK release is supported with Server 2.0.0; JSON
remains HTTP transport and is not a workflow payload codec.
Earlier SDK versions remain historical releases and are not alternate supported
2.0 baselines; no prerelease compatibility shim is provided.
- Worker registration now explicitly refuses local activities, worker sessions, and sticky execution at the portable worker-affinity protocol floor instead of advertising an unimplemented routing contract.
WorkflowContext.select()starts independent durable operations and returns the first persisted winner with stable member identity. Non-winning handles remain available to await or cancel explicitly across replay and restart.WorkflowContext.message_stream()now consumes named repeated input in ordered bounded batches. Runtime-owned cursor and wait metadata survives replay, worker replacement, server restart, duplicates, and continue-as-new without exposing the reserved transport signal to workflow declarations.WorkflowContext.upsert_memo()now authors validated memo patches, replaysMemoUpsertedidentities, carries opaque payload envelopes, and fails before completion when runtime capability discovery does not advertise memo updates.- Workflow Streams are now a first-party client and workflow-authoring surface with list, describe, resumable at-least-once subscription, idempotent append, close, and error operations. Run-scoped stream items support runtime-mediated external payload references, and replay-safe workflow commands derive stable per-item idempotency keys from the durable command identity.
- List-yield parallel composition now accepts nested activity, child-workflow, and timer groups, schedules every durable leaf through the existing command protocol, restores the nested input shape on replay, and keeps stable full group paths across restart and duplicate terminal delivery.
WorkflowContext.saga()provides deterministic reverse-order activity compensation for failures and cooperative cancellation.SagaCompensationFailedpreserves both the initiating and compensation failures as structured diagnostics.- Clients and workers now externalize large Avro payloads through the authenticated namespace runtime using only the runtime URL, namespace, and role credential. Opaque references are resolved before decode with bounded caching, size/SHA-256 verification, typed retryable and terminal failures, and no SDK delete authority over runtime-owned objects. Direct local, S3, GCS, and Azure adapters remain explicit self-hosted integrations and are never inferred from the runtime's backing driver.
- List-based activity, child-workflow, and timer fan-out now emits the stable language-neutral parallel-group identity and path metadata consumed by service-mode and embedded replay implementations.
- Declared update validators now run through durable, synchronous pre-accept worker tasks. Capability discovery fails closed, validation replay does not invoke the handler or commit workflow state, and typed failures preserve the Server's retryability and fencing outcomes.
- The optimized
fastavropayload path now uses the fixed recursivedurable_workflow.protocol.Valueschema with explicit named branches and Avro single-object framing. Cross-language fixtures cover every branch, schema evolution, nested values, and stable policy failures. - A repeatable benchmark reports JSON, legacy-wrapper, and fixed-schema raw and HTTP-envelope sizes plus adapter/encode/decode latency over the shared checked-in corpus, with an enforced production-path regression budget calibrated with explicit headroom.
- Schedule listing now exposes server-side status, workflow-type, visibility-
query, page-size, and continuation-token filters on async and sync clients.
ScheduleListErrorpreserves typed filter and cursor refusal evidence. durable-workflow-python-conformance --composenow accepts nested host runner evidence tables, resolved artifact/source aliases, booleanpassedresult cells, nested protocol trace planes, and no-PHP audit check aliases when composing full published-artifact Python conformance results.durable-workflow-python-conformance --composenow normalizes runbook-style scenario and capability identifiers such asserver-upandresult-returned, plus common status, trace-plane, and no-PHP audit aliases, before evaluating host evidence.durable-workflow-python-conformance --composenow accepts runner-native host evidence aliases such asscenarioEvidence,capabilityResults,officialCli,firstUserFlow,traces, andlanguageNeutralityAudit, so full published-artifact runs are not marked uncovered just because the runner records observations under natural collection names.durable-workflow-python-conformancenow exposes a host-evidence composition contract and--composemode so published-artifact runners can turn raw observations into a complete Python parity result document before evaluation.durable-workflow-python-conformancenow exposes the Python SDK published-artifact parity contract and evaluates host result documents, rejecting smoke-only evidence unless the official CLI path, cold first-user setup, protocol traces, no-PHP-assumption audit, concrete artifact versions, and complete capability table are all present.Client.delete_namespace()now exercises the namespace lifecycle cleanup control-plane surface and returns the server's per-table cleanup counts onNamespaceDescription.deleted.
- The README and PyPI project description now require Server worker protocol
>=1.19,<2.0. Package and API-reference release validation derive that interval from the shipped SDK protocol and reject semantic guidance drift. - Cold replay now classifies an already-satisfied condition before binding the next durable history row. Signals and updates accepted across worker replacement can therefore advance through the transient wait without consuming the following activity or a later condition wait.
- Yielded continue-as-new commands now preserve pending memo and search- attribute upserts in the same order as returned continuations, while replayed metadata updates remain consumed exactly once.
- High-level workers now explicitly advertise memo-upsert and typed-search-
attribute command support when registering with Server. Worker requests use
protocol 1.19, while direct
Client.register_worker()callers retain full control of their explicit capability list. - Completed parallel activity, child-workflow, timer, and mixed groups now replay results and select failures by their durable yielded positions rather than terminal-event recording order. Chronological signals, updates, and condition-wait interactions retain their history order.
- Worker workflow, activity, query, update, and update-validation tasks now
reject every missing, null, or otherwise non-exact root payload codec before
decoding envelopes or invoking application code. Customer-owned Avro values
and metadata remain free to use
codecandpayload_codeckeys. - Explicit JSON and unknown payload-codec tags now fail closed before null, absent, empty-argument, or already-decoded replay shortcuts. Untagged no-payload sentinels and Avro-encoded null values retain their intended behavior.
- The Python API reference now describes query decorators and query-state replay as parts of the shipped server-routed worker query path while keeping its Server capability negotiation requirements explicit.
- Workflow and update-validation discovery now use one Server-multiplexed long poll per workflow reservation. A worker with one workflow slot can discover either task kind promptly without leasing excess replay work, increasing idle request load, or hiding poll reservations from heartbeat slot reporting.
- Package publication now treats exact-version PyPI JSON as the authoritative integrity surface. The rendered project-page audit remains visible evidence, but a client challenge or presentation mismatch no longer blocks creation of the matching source prerelease after immutable artifacts have been verified.
- PyPI qualification now keeps exact-version metadata and exact and documented prerelease installs release-blocking without requiring the project root or a bare install to select a release candidate over historical final releases. Stable 2.0 qualification retains strict root and default-install checks while preserving non-yanked 0.x history.
- Package metadata now identifies the Python SDK as a release candidate in the Durable Workflow 2.0 train and describes the shipped worker-routed query path without overstating pre-accept update-validator support.
- Query calls and update wait stages now fail before dispatch with typed, actionable errors when Server discovery is missing, unavailable, or explicitly lacks the requested capability.
- Worker deregistration treats an already-absent registration as an idempotent success, allowing cleanup-first published-artifact conformance runs to continue without hiding authorization or other deregistration failures.
- API-reference deployment now installs its exact rendered command from public PyPI before handing a candidate to GitHub Pages. Package publication invokes the same fail-safe deployment after the release is installable, while a delayed or failed publication leaves the prior reference live.
- The published API reference now renders its exact prerelease installation command and qualified Server pairing from the package release manifest.
- Worker shutdown now quiesces inline
run_until()work and fully stops the query-poller thread before deregistration. A worker that cannot stop every execution path by its shutdown deadline reports a failure and keeps its registration instead of allowing stale completions after lease recovery. - Workers configured with only
worker_tokencan now authenticate cluster discovery and complete registration, polling, heartbeat, and graceful deregistration without receiving a control credential. Actual worker and control operations remain fail-closed across credential roles. - Successful worker registrations are now removed through the worker protocol after pollers and in-flight tasks drain. Shutdown is idempotent, surfaces authentication and protocol failures, and retains cleanup context without replacing a primary worker-loop failure.
- Scoped worker and control tokens are no longer substituted across protocol planes. Missing role-appropriate credentials fail before transport, while the shared token continues to authorize both planes.
- The MkDocs search overlay no longer widens the document beyond responsive viewports, keeping search and navigation usable without horizontal panning.
- Client construction now rejects base URLs with a terminal SDK-owned
/apisuffix, query, or fragment while preserving self-hosted and managed-runtime path prefixes. The quickstart also uses unique durable workflow identities and documents typed duplicate-start handling. - Current condition-timeout history uses explicit timer and condition identity without adjacency inference, while the metadata-poor historical shape keeps a narrowly bounded replay fallback. Interleaved updates and signals no longer shift the ordinary activity cursor.
- Worker registration now advertises each workflow's declared signal, query, and update contract with typed parameter metadata, allowing the server to admit declared Python workflow updates while preserving typed refusals.
- Explicit release recovery rejects terminally superseded plans before and after publication preflight while keeping completed-plan verification idempotent.
- Synchronous clients now keep their async HTTP connection pool, returned handles, and cleanup on one client-owned event loop for the client's lifetime.
- Worker-routed queries now replay signal-woken condition waits that reopen the same logical wait until all recorded signals have been applied, so long-lived queryable workflows report the latest signal-mutated state.
- Python conformance CLI evidence now accepts terminal stdout/JSON captured on nested CLI command records while still rejecting generic scenario outputs as proof of the public CLI result path.
- Python conformance source-policy evidence now accepts the explicit
local_product_source_checkouts_used=falsepublished-artifact runner alias. durable-workflow-python-conformance --composenow accepts actual CLI terminal-result evidence fromworkflow:start --wait,workflow:describe, andworkflow:show-run --followinstead of only generic result aliases.durable-workflow-replay-conformancenow declaresoutcome: passwhen every required Python replay shard scenario passes, so full replay evidence is no longer reported as non-passing.- Worker query tasks now treat null or empty compact history payload fields as missing when a history export carries the durable activity result or signal arguments, so cold replay after worker restart does not answer queries from an unmodified workflow instance.
- Worker query tasks now replay from the bundled history export when the inline task history is empty or truncated, so cold query replay reconstructs activity-derived state instead of answering from a fresh workflow instance.
- Python parent workflows now decode successful child workflow completions from
the server's documented
ChildRunCompleted.outputhistory payload, while still accepting the olderresultalias. This prevents completed child workflow returns from replaying asNone. - Condition-wait replay now binds signals that arrive during a leased
workflow task to the next recorded wait when the server history records
those signals before the task's
ConditionWaitOpenedrow. This avoids applying rapid signal batches to the previous wait and prevents replay from completing with a later wait/timer history step left unconsumed. - Condition-wait replay now prefers the recorded event-order wait binding over
a signal's stored
workflow_sequencewhen several signals arrive before a reopened wait. This handles server histories where later signals are accepted while the previous wait is still open but must replay against the next physical wait. - Condition-wait replay now lets a true predicate finish the current wait
before any following same-key wait is considered stale terminal history.
This keeps query and signal replay aligned with histories that include
unresolved
ConditionWaitOpenedplus condition-timeoutTimerScheduledrows after a replayed false reopen, while preserving pending sequential same-key waits and resolved reopens that must remain replay history. - Workflow workers now report unhandled workflow-task execution errors back to the server instead of leaving the leased task pending until the lease or CLI wait times out. This lets the server observe and retry or fail the task promptly when command serialization or interceptor code raises after a task has been claimed.
- Workflow-task completion now retries transient transport failures and server throttling/5xx rejections before preserving emitted commands or reporting a definite task failure, reducing stuck waiting runs when a signal-satisfied wait completes immediately after replay-driven query activity.
- Ambiguous workflow-task completion failures no longer get reported back as durable task failures after commands have been produced. Definite server rejections are still treated as failed workflow tasks even when the best-effort failure report cannot be sent, but transport/ownership ambiguity preserves the emitted commands so replay-driven signal completion is not converted into a stuck failed task.
- Repeated condition-wait openings for the same logical wait now replay through every matching signal before deciding whether the wait is still pending, so long-running signal/query workflows do not get stuck on the first signal.
- Signal and update receivers recorded while a condition wait is open now replay at that specific wait, so later signal-driven waits are not satisfied or consumed too early when no activity or timer result separates them.
- Signal and update receivers recorded after an activity result now replay after
the workflow consumes that activity result, so receiver-mutated state is not
overwritten by deterministic post-activity setup before a
wait_condition. - Python workflow replay now throws terminal
ActivityFailedhistory events into the generator as a typedActivityFailedexception, including the recorded activity and failure metadata. Activity-only saga workflows can catch the exception and schedule compensation instead of replaying the original activity command again. - Worker heartbeat
process_metricsnow report instantaneous values instead of process-lifetime aggregates.cpu_percentis the share of wall time the worker spent on CPU during the interval since the previous heartbeat (previously the lifetime average, which converged to a fixed value within minutes and stopped tracking live load), andmemory_bytesis the current resident set size sampled from/proc/self/statmon Linux (previouslyru_maxrss, which is the process-lifetime high-water mark and never decreased after a startup spike). Platforms without/procno longer reportmemory_bytesrather than reporting a misleading peak. The heartbeat protocol shape is unchanged; the server records whatever the worker sends, so the Worker Status surface starts showing accurate live numbers as soon as workers upgrade.
- Release-plan recovery now consumes immutable, exact-version release-note preparation authority before publishing a newly recorded plan.
tests/test_client.pynow closes theschedule.historypolyglot parity slice.test_get_schedule_history_matches_polyglot_fixtureasserts the full decoded payload envelope per event (id,recorded_at,payload, plus the workflow attribution fields) so the Python parity check covers the same wire content the CLI parity check covers when it asserts the printed JSON envelope matches the shared fixture'sresponse_body. A companiontest_iter_schedule_history_walks_polyglot_fixtureexercisesClient.iter_schedule_historyagainst the shared fixture to lock in the cursor-advance semantics across pages.
- Stable 2.0 remains gated on published-artifact qualification. Server-routed Python queries and synchronous pre-accept update validation are implemented; validator-bearing workers fail closed when discovery cannot prove that the Server enforces the acceptance boundary.
durable-workflow-replay-verifyanddurable-workflow-history-bundle-verifyconsole scripts plus matching Python APIs (verify_replay,verify_golden_history,simulate_bundles,verify_history_bundle,aggregate_verdicts,promotion_decision_for*). These produce verdicts andpromotion_decisionvalues that match the platform replay contract, replay cross-runtime golden histories against registered workflow classes, and integrity-check exported history bundles for promotion gates.InvocableActivityHandler,handle_invocable_http_request,handle_invocable_lambda_event, andlambda_invocable_activity_handlerfor activity-grade external execution from HTTP servers and serverless runtimes. The carrier shares the external-task input/result envelope with first-party workers and rejects workflow-task inputs.CONFORMANCE.md— per-repo platform conformance claim listing the fixtures this SDK serves, the targets it claims (official_sdk,worker_protocol_implementation), and the release gate that blocks PyPI publication when conformance regresses.docs/reference/invocable.mdcovering the invocable adapter for the generated API reference site.
- Worker registration enforces the
worker_protocol.version: "1.1"feature floor advertised by/api/cluster/infoand fails closed with a clear error when the server's advertised feature set is missing capabilities the SDK relies on. - Workers and the serializer reject unsupported payload codecs at encode and decode time instead of silently passing the raw bytes through; misconfigured deployments surface a typed error instead of a downstream decode failure.
- Built-package smoke (
scripts/smoke-built-package.py) now verifies installed wheel and source distribution metadata, the PEP 561py.typedmarker, every name in__all__, that reference modules resolve fromsite-packagesrather than the source checkout, and that the README quickstart still replays. The PyPI/TestPyPI publish workflow runs the smoke before uploading artifacts.
Client.get_schedule_history(schedule_id, *, limit=None, after_sequence=None)returns oneScheduleHistoryPageof the schedule's audit stream, andClient.iter_schedule_history(...)is an async iterator that walks every remainingScheduleHistoryEventwith paging hidden.ScheduleHandleexposes the same surface ashandle.history(...)andhandle.iter_history(...). History remains available for deleted schedules so post-mortem review still works after a schedule is removed.
WorkflowEnvironmentnow drivescontinue_as_newchains end-to-end. Each link's input, workflow type, history, and terminal command are exposed through theruns/run_countproperties, signals can be queued for a specific link viaenv.signal(..., run=N), and chains that switch workflow types use the newenv.register_workflow(cls)registration. Chain length is bounded bycontinue_as_new_limit(default50); exceeding the limit raisesRuntimeErrorso tests catch runaway continuations.
Client.set_namespace_external_storage(and its sync facade) now takes the namespace asname, matchingdescribe_namespace,create_namespace, andupdate_namespace. The 0.4.0 spellingnamespace=is still accepted as a deprecated keyword alias that emits aDeprecationWarning; it will be removed in a future release. Passing bothnameandnamespaceraisesTypeError.
- Workflow control-plane parity across the async and sync clients for list, describe, cancel, terminate, history, history export, and run visibility, plus a public history replayer and released golden replay fixtures.
- Task-queue, worker, and namespace control-plane coverage for build-id rollout visibility, drain/resume mutation, worker build-id reporting, namespace controls, activity-task operations, schedule visibility/mutation, and search attribute management.
- External payload storage support for reference envelopes, object-store drivers, expiry metadata, retention/delete helpers, storage policy parity, and verified-byte caching.
- Bridge webhook client support, invocable activity carrier support, replay-safe UUIDv7 and patch-marker helpers, worker interceptors, payload codec batching, and explicit Avro payload adapters.
- PyPI/TestPyPI publish builds now run the installed-package smoke before uploading artifacts, so release candidates verify the wheel and source distribution import from site-packages and replay the README quickstart.
- Polyglot parity coverage now spans CLI/Python shared control-plane fixtures, including workflow maintenance, task queues, storage drivers, and system maintenance endpoints, reducing drift between released SDK behavior and other Durable Workflow surfaces.
- Breaking (pre-1.0):
WorkflowCancelledandActivityCancellednow inherit fromBaseException(notDurableWorkflowError/Exception), so a genericexcept Exception:block in activity code or result handlers no longer silently swallows cancellation. Callers that relied on catching cancellation viaexcept Exception:orexcept DurableWorkflowError:must now either catch the class by name (e.g.except (ActivityCancelled, WorkflowCancelled):) or catchBaseException. Mirrors the standard-library precedent set byasyncio.CancelledErrorandKeyboardInterrupt.
- Plane-scoped SDK bearer tokens:
Client(..., control_token=..., worker_token=...)and the sync wrapper now support least-privilege server deployments where operator/admin credentials are separate from worker credentials. The existingtoken=argument remains the shared fallback. Worker.run_until(workflow_id=..., timeout=...)for examples, smoke tests, and single-workflow scripts that need to run a worker until one workflow reaches a terminal state.- A Docker Compose order-processing example under
examples/order_processingthat starts a local server and runs a multi-activity Python workflow end-to-end. ctx.wait_condition(...)durable primitive with replayer support, for workflows that pause until a signal- or update-driven predicate holds.@workflow.signal,@workflow.query, and@workflow.updatedecorators with in-workflow dispatch: signals apply during replay, queries execute against a replayed workflow instance, and updates run on a worker with acceptance + application recorded in history.ctx.sleep(seconds)sugar overStartTimerfor readability.- In-process
WorkflowEnvironmenttesting harness that boots a worker and client against a fake server for unit-style tests without Docker. - Activity retry policy support:
ActivityRetryPolicy(...)onctx.schedule_activity(...)serializes retry bounds onto the server-side command. - SDK metrics hooks (
MetricsRecorder/PrometheusMetricsRecorder) for worker-side operational telemetry.
- Worker compatibility checks now use
/api/cluster/infoprotocol manifests as the authority instead of the top-level server app version. SDK 0.3.x requirescontrol_plane.version: "2",control_plane.request_contractschemadurable-workflow.v2.control-plane-request.contractversion1, andworker_protocol.version: "1.0". Missing, unknown, or undiscoverable compatibility states fail closed. Client.get_result()now decodesWorkflowCompletedoutput with the event or workflow payload codec instead of assuming JSON.- History-event decoding in
client.pyandworkflow.pynow requires the server's canonical PascalCaseevent_typevalues (WorkflowCompleted,ActivityCompleted,TimerFired, etc.). The prior snake_case fallback and theoutput-or-resultkey fallback onWorkflowCompletedhave been removed; unknown event-type shapes are ignored instead of silently tolerated. (#432)
- Runtime server version compatibility check at worker registration. On
Worker.run(), the SDK now calls/api/cluster/infoand refuses to register against a server whose major version falls outside the set the SDK knows how to talk to. This prevents a 0.2.x worker from silently attempting to drive a future breaking-release server. (#302) Client.get_cluster_info()— fetches the server version and declared capability manifest from/api/cluster/info.- Avro payload codec support as a core runtime dependency.
serializer.encode(),serializer.decode(), andserializer.envelope()now accept acodec=argument, anddecode_envelope()honors the inner codec tag. The Worker decodes Avro-coded activity arguments and echoes the inbound codec on itscomplete_activity_taskresult. Wire format is the Durable Workflow generic-wrapper (base64 of0x00+ Avro binary of a{json: string, version: int}record), byte-compatible with the PHPWorkflow\Serializers\Avroserializer. (#362)
- Avro is now the default codec for new payloads produced by the client, serializer helpers, schedules, workflow commands, and activity results. JSON payloads remain supported for compatibility with existing history.
- Replayed activity results now decode using the event payload codec.
Initial PyPI release. HTTP+JSON worker and client for the Durable Workflow server, covering workflow authoring, activity execution, signal and update commands, and the worker protocol over long-poll HTTP.