Schedules hotfix for DTS dashboard#169
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates the durabletask.scheduled persisted schedule entity JSON shape to match what the Durable Task Scheduler (DTS) dashboard expects when deserializing state as .NET types.
Changes:
- Serialize
ScheduleConfigurationusing PascalCase property names and formatintervalas a .NETTimeSpanstring. - Serialize
ScheduleState.statusas the .NET enum ordinal (int) and accept legacy string/snake_case formats when reading. - Add regression tests for the .NET-compatible JSON shape and update the changelog with the user-visible fix.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| tests/durabletask/scheduled/test_models.py | Adds tests validating .NET-compatible JSON serialization and legacy deserialization support. |
| durabletask/scheduled/schedule_status.py | Adds helpers to convert schedule status to/from .NET ordinal/string representations. |
| durabletask/scheduled/models.py | Implements PascalCase JSON shape, TimeSpan interval formatting/parsing, and more tolerant deserialization. |
| CHANGELOG.md | Documents the dashboard compatibility fix under ## Unreleased. |
Persist OrchestrationInput as a JSON-encoded string so the DTS dashboard can deserialize it (it types the field as string?). Also address Copilot review feedback: restrict _from_iso Z-normalization to a trailing Z, use PEP 604 unions in ScheduleStatus.from_dotnet, and preserve the generated execution_token when absent from persisted state.
berndverst
left a comment
There was a problem hiding this comment.
Reviewed the persisted-state realignment against the .NET wire format — this looks correct and well-tested. Approving.
Verified the cross-language claims:
- .NET
ScheduleStatusenum order confirmed againstmicrosoft/durabletask-dotnet(src/ScheduledTasks/Models/ScheduleStatus.cs):Uninitialized=0, Active=1, Paused=2, matching_STATUS_TO_ORDINALexactly. TimeSpanformat/parse is correct, including the day component (1.02:03:04) and 7-digit fractional ticks (00:00:01.5000000).OrchestrationInputas a JSON-encoded string matches .NET'sstring?typing (consistent with the quotedStartObject-as-string error); PascalCase keys satisfySystem.Text.Jsoncase-insensitive matching.- Client read path (
get_typed_state→ScheduleState.from_json) and the legacy snake_case reads round-trip.durabletask-azuremanagedinherits the core types with no serialization override, so nothing to update there.
CI is green across all Python versions and the review feedback (trailing-Z normalization, PEP 604 union, execution-token preservation) is all addressed. Tests are thorough.
Non-blocking concerns to keep in mind:
-
No forward compatibility. New workers persist the .NET-shaped state; an old (pre-PR) worker reading that state breaks hard —
ScheduleStatus(1)raisesValueErroron the str-enum, and the missinginterval_secondsraisesKeyError. In a mixed-version fleet during rollout, schedules touched by an upgraded worker would fail on older workers still in the pool. Acceptable for a hotfix, but worth upgrading the fleet together and/or noting in the release notes. -
Minor: schedule
orchestration_inputis encoded/decoded via a hardcoded defaultJsonDataConverter, bypassing any custom worker-configured converter. Not a regression (inputs already degraded to JSON-native across a persistence round-trip, and the actual orchestration start still uses the worker's converter), but schedule inputs must be plain-JSON-serializable by the default converter. Possible follow-up if custom-typed schedule inputs are a scenario you want to support.
|
Could it be that the Dashboard needs to be more flexible instead? This isn't really a fix required in the Python SDK is it? |

Summary
Schedules created with
durabletask.scheduledwere failing to load in the Durable Task Scheduler (DTS) dashboard. The dashboard backend reads the raw schedule entity state and deserializes it into .NET types withSystem.Text.Json(Web defaults), but the Python SDK persisted the state in a Python-native shape.This PR aligns the persisted schedule entity state (
ScheduleState/ScheduleConfiguration) with the .NET SDK wire format so the dashboard can read it. In-memory representations are unchanged; only the persisted JSON shape is adjusted (with tolerant reads for round-tripping).Fixes
statusas a numeric enum, not a string.System.Text.Json(Web defaults) has no string-enum converter, so it expects the ordinal.ScheduleStatusis now serialized as its .NET ordinal (Uninitialized=0,Active=1,Paused=2). Fixes the originalcould not be converted to ... ScheduleStatus. Path: $.statuserror.intervalas a .NETTimeSpanstring, notinterval_seconds. The interval is now formatted/parsed as[-][d.]hh:mm:ss[.fffffff](e.g.01:00:00) to match the .NETTimeSpanJSON converter.OrchestrationInputas a JSON-encoded string, not a raw object. .NET types the field asstring?; Python held it asAny. A dict/object input serialized to a JSON object and was rejected (Cannot get the value of a token type 'StartObject' as a string. Path: $.ScheduleConfiguration.OrchestrationInput). It is now serialized to a JSON string on persist and decoded back on read; the in-memory value stays a raw object so the orchestration still starts with the correct input.ScheduleStateandScheduleConfigurationnow emit .NET property names (Status,ExecutionToken,ScheduleConfiguration,ScheduleId,Interval, etc.) so case-insensitive matching in the backend succeeds (snake_case underscores previously broke the match).Backward compatibility
from_jsonon both types tolerates the legacy snake_case/string shapes (numeric or string status,interval_seconds, raw input) so states persisted by earlier Python workers still round-trip.Review feedback addressed
_from_isoonly normalizes a trailingZ(no longer replaces everyZin the string).ScheduleStatus.from_dotnetuses a PEP 604 union (int | str | None); removed the now-unusedtyping.Unionimport.ScheduleState.from_jsonpreserves the__init__-generatedexecution_tokenwhen the field is absent, instead of overwriting it withNone(which would make everyrun_schedulesignal look stale and silently stop the schedule).Testing
TimeSpaninterval, JSON-string input), legacy-format deserialization, and execution-token preservation.tests/durabletask/scheduled/tests pass;flake8clean.orchestration_input(the exact shape that previously failed) and confirmed the dashboard'sGET /api/v1/taskhubs/schedulesendpoint deserializes and lists it without error.