Repository navigation
Add opt-in typed workflow, child and client values (#184 slice 2) - #255
Conversation
workflow.defn(typed=True) decodes run, signal, query, update and update-validator arguments to their annotations; start_child_workflow (result_type=) and WorkflowHandle.result / Client.get_result (result_type=) decode results. Dataclass and Enum values in client argument payloads, child and continue-as-new arguments, and workflow and update results are sent as their neutral values at encoding time; commands keep the authored value, so the test harness is unchanged. A replay corpus fixture recorded by an untyped workflow replays to the same completion under typed=True. Refs #184. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
General interim review (Security unavailable)HOLD at CI is 21/21 green. Locally: ruff and mypy are clean, the corpus validator passes, and the full suite passes apart from 8 environmental failures that also fail on base
Blocking
Fix before release
Follow-ups (non-blocking)
Cross-SDK: the codec envelope is unchanged, and values are the same neutral Avro maps and scalars as slice 1's interop fixture. No public symbols are exported by accident. |
A handler with nothing to decode stays unwrapped, and a typed wrapper is fingerprinted as the method it wraps, so turning typed=True on no longer changes a recorded wait_condition predicate (fails at 43bc24f with condition_fingerprint_changed). staticmethod and classmethod handlers are refused under typed=True. Docs list per-surface mismatch behaviour and the in-flight dataclass change caveat; the untyped corpus twin annotates its dataclasses so a leak is caught. Refs #184. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the slice 2 entry under Unreleased above the released 2.14.0 section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Predicate fingerprints hash bytecode, which differs between Python versions, so the 3.13-recorded corpus fixture failed on 3.10-3.15 in CI. The test now builds the untyped history with fingerprints of the unwrapped methods (inspect.unwrap) on the running Python; it still fails at 43bc24f with condition_fingerprint_changed and passes here. Refs #184. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
General interim review (Security unavailable): delta re-reviewCLEAR at
Non-blocking follow-ups are unchanged: no sync-client Merge at this exact SHA and post a readback. |
|
Merged at General's clearance of exact head
|



Slice 2 of #184, under the contract General confirmed (#184 (comment)). Slice 1 is #247 and its review follow-up is #252, both merged. With this PR, the issue's acceptance criteria are covered.
What users get
@workflow.defn(name=..., typed=True): the arguments ofrun(afterctx) and of every@signal,@query,@updateand update-validator handler are decoded to their annotations.typed_methodwraps each method withfunctools.wraps. Signatures, handler registries andinspect.unwrapchecks are unchanged, and a generatorrunstays a generator.TypeErrorwhen the workflow is declared.TypedValueErrorwhen the method is called. Forrun, the workflow fails withexception_type=TypedValueError, and the message names the path but never the value.ctx.start_child_workflow(..., result_type=T): the child's result is decoded with the same rules asschedule_activity(result_type=). That covers single, parallel and selection delivery, because the shared_typed_activity_resultnow acceptsStartChildWorkflow. The path ischild workflow result.result_typeis local only.WorkflowHandle.result(result_type=T)/Client.get_result(..., result_type=T): the workflow result is decoded on the client. The annotation is validated before polling, and a mismatch raisesTypedValueErrorwith the pathworkflow result.asdict()with each enum's.value:workflow_input,signal,update,query,activity_input,schedule_input);StartChildWorkflow.__post_init__, asScheduleActivitydoes);CompleteWorkflow/CompleteUpdateresults andContinueAsNewarguments, converted into_server_command.durable_workflow.testing, which returnsCompleteWorkflow.resultdirectly, behaves as before. These values were refused by the codec, so nothing previously accepted changes.Evidence
typed-workflow-values-opt-in(clarification 3). The history was recorded by the untypedUntypedOnboarding: a neutral start input, a child result with an extraaccount_idkey, and an approval signal with an extra key and a missing optionalnote.TypedOnboarding(typed=True) and the untyped workflow replay it to the identicalCompleteWorkflow.TypedOnboardingis registered in the corpusWORKFLOWS.tests/test_typed_workflow_values.py:Tier.GOLD) and update (CompleteUpdate("update-1", "basic")) arguments;runmismatch failing the workflow without leaking the value;result_typemismatch at theyield;result(result_type=)decoding, plus the plain path and a mismatch;main's source the new module fails to import, becauseworkflow.defn(typed=True)doesn't exist there.docs/sdk-reference.md"Typed activity values" now covers the workflow, child and client surfaces. The CHANGELOG has an Unreleased Added entry.Local runs at 43bc24f (lab2, Python 3.13, this branch's source)
ruff check src testsis clean, andmypy src/durable_workflowreports no issues.tests/integration): 4925 passed, 4 skipped.validate-regression-corpus.py --base-ref origin/main:status: pass.Not merged or released. 2.14.0 is a minor release for this API.
🤖 Generated with Claude Code
Review fixes (General's hold on 43bc24f)
Any/unannotated ones) is left unwrapped. A wrapped handler is fingerprinted as the method it wraps (__typed_workflow_method__in_condition_predicate_fingerprint). Turningtyped=Trueon therefore no longer changes a recordedwait_condition(self.is_done)predicate.test_an_untyped_history_with_predicate_fingerprints_replays_under_typed_truebuilds the history an untyped deploy records, with fingerprints of the unwrapped methods. It fails at43bc24fewithcondition_fingerprint_changedondone-1, and passes here.fae44724, so the history is built at run time on whichever Python runs the suite. The existing drift fixture uses a fake fingerprint for the same reason.staticmethodandclassmethodhandlers raiseTypeErrorundertyped=True. Tests cover both.typed=Truekeeps predicate fingerprints, and a caveat on changing a dataclass while runs are in flight (new fields need defaults).UntypedOnboardingnow annotatesCustomer/Approval, so a leak of typed decoding into untyped workflows would be caught.