Context
The original reference is Azure/azure-functions-durable-extension#3215, which added exception-property enrichment for .NET isolated Durable Functions on top of microsoft/durabletask-dotnet#474 and the TaskFailureDetails.properties protobuf field from microsoft/durabletask-protobuf#45.
Research update (2026-07-28)
| SDK |
Current state |
| .NET |
Complete. IExceptionPropertiesProvider.GetExceptionProperties(Exception) returns IDictionary<string, object?>?; the provider is registered on the worker, invoked recursively for the exception chain, and serialized through map<string, google.protobuf.Value>. The public TaskFailureDetails exposes both InnerFailure and Properties. PR #474 established the behavior; PR #560 later added a sample without changing the contract. |
| Java |
Complete since microsoft/durabletask-java#263 (2026-04-29). Java added ExceptionPropertiesProvider, worker-builder registration, recursive innerFailure, protobuf round-tripping, integration tests, a depth limit, and safe handling when the provider itself throws. |
| JavaScript |
Custom properties are not implemented. The main FailureDetails still exposes only message, error type, and stack trace, and no code reads or writes the generated protobuf properties map. JS does have partial adjacent support: PR #310 added recursive Error.cause serialization, and the entity-specific failure model reads inner failures recursively. This is context, not a dependency for Python. |
| Python |
The generated protobuf gained properties incidentally with PR #110, so no protobuf update is needed. Python now writes recursive innerFailure (PR #99), made FailureDetails a frozen dataclass for JSON-safe history export (PR #172), and added fully-qualified error types plus is_caused_by() (PR #194). However, all public proto-to-model paths still discard innerFailure and properties, and workers cannot populate custom properties. |
The current .NET and Java implementations agree on the portable property model: None/null, strings, booleans, numbers, nested string-keyed mappings, and lists are represented with google.protobuf.Value; unsupported values fall back to their string representation. This path is independent of the payload DataConverter. Protobuf numbers are float64, so integer-like values read back as float. Date/time values are not inferred on read: review of extension PR #3215 rejected prefix-based dt:/dto: type guessing because ordinary customer strings could be irreversibly coerced. Python should require providers to stringify date/time values and treat prefixed values from .NET as opaque strings.
Target behavior
Add an optional Python ExceptionPropertiesProvider protocol with:
def get_exception_properties(
self, exception: Exception
) -> Mapping[str, Any] | None: ...
When configured on a worker, it is called for the top-level exception and each captured inner exception. None or an empty mapping omits the wire map. If the provider raises, log a warning with the exception and continue reporting the task failure without custom properties, matching Java's worker-safety behavior.
Extend the existing frozen FailureDetails dataclass by appending backward-compatible defaulted fields:
inner_failure: FailureDetails | None = None
properties: dict[str, Any] | None = None
Keep stored properties as ordinary JSON-safe dictionaries so dataclasses.asdict() and history export continue to work after PR #172.
Implementation plan
- Add shared failure conversion utilities. In
durabletask/internal/helpers.py, add Python-to-google.protobuf.Value and protobuf-to-Python conversion for null, bool, number (including Decimal, encoded as float64), string, nested mapping, and list values, with string fallback for unsupported outbound values. Do not add date/time type inference or prefix parsing. Extend new_failure_details() to accept an optional provider/logger, preserve the existing cycle/depth protection, and apply the provider recursively.
- Expose the complete public model everywhere. Add
inner_failure and properties to durabletask.task.FailureDetails, then centralize protobuf-to-FailureDetails conversion and reuse it from TaskFailedError, client.parse_orchestration_state(), history._failure_details(), and entity failure conversion. Also teach _failure_details_from_core_dict() to consume legacy Properties and recursive InnerFailure. This covers task failures, orchestration state queries, entity failures, and all failure-bearing history events without separate partial implementations.
- Configure workers. Add
exception_properties_provider to TaskHubGrpcWorker, retain it on the worker, and thread it through _OrchestrationExecutor / _RuntimeOrchestrationContext so user-orchestrator failures are enriched. Use it for activity failures, entity-operation failures, output-serialization failures, and outer worker failure paths that create TaskFailureDetails.
- Preserve provider support in derived workers. Add and forward the same optional parameter from
DurableTaskSchedulerWorker; export the provider protocol from the public package surface. Existing constructors and behavior remain unchanged when no provider is supplied.
- Keep Azure Functions integration non-blocking. The new
azure-functions-durable 2.x package was added after this issue was opened and creates internal workers per invocation, while activity exceptions are reported by the Functions host/worker pipeline. Core read support will make incoming properties visible immediately. A Functions-specific registration/serialization surface should be designed separately rather than blocking core and DTS support in this issue.
- Add coverage. Test every protobuf value kind,
Decimal/float64 conversion, and nested combination; null/empty mappings; unsupported-value string fallback; recursive properties on __cause__/__context__; cycle/depth limits; selective providers; provider exceptions; and no-provider compatibility. Add worker-level tests for orchestration, activity, and entity failures; round-trip tests through TaskFailedError, orchestration state, and history events; a history JSON-serialization regression test; an Azure Managed constructor/forwarding test; and a fixture that consumes .NET-produced properties.
- Document the user-facing API. Add a short worker example showing correlation/domain metadata extraction and update
CHANGELOG.md plus durabletask-azuremanaged/CHANGELOG.md under Unreleased.
Acceptance criteria
- A configured provider can attach portable custom values to orchestration, activity, and entity failures.
- Properties and recursive inner failures survive protobuf round-trips and are visible through
TaskFailedError.details, orchestration-state APIs, and history APIs.
- Existing three-argument
FailureDetails(...) construction and workers without a provider behave unchanged.
- Provider failures never replace or suppress the original task failure.
- No protobuf regeneration or JavaScript implementation is required before merging Python support.
Context
The original reference is Azure/azure-functions-durable-extension#3215, which added exception-property enrichment for .NET isolated Durable Functions on top of microsoft/durabletask-dotnet#474 and the
TaskFailureDetails.propertiesprotobuf field from microsoft/durabletask-protobuf#45.Research update (2026-07-28)
IExceptionPropertiesProvider.GetExceptionProperties(Exception)returnsIDictionary<string, object?>?; the provider is registered on the worker, invoked recursively for the exception chain, and serialized throughmap<string, google.protobuf.Value>. The publicTaskFailureDetailsexposes bothInnerFailureandProperties. PR #474 established the behavior; PR #560 later added a sample without changing the contract.ExceptionPropertiesProvider, worker-builder registration, recursiveinnerFailure, protobuf round-tripping, integration tests, a depth limit, and safe handling when the provider itself throws.FailureDetailsstill exposes only message, error type, and stack trace, and no code reads or writes the generated protobuf properties map. JS does have partial adjacent support: PR #310 added recursiveError.causeserialization, and the entity-specific failure model reads inner failures recursively. This is context, not a dependency for Python.propertiesincidentally with PR #110, so no protobuf update is needed. Python now writes recursiveinnerFailure(PR #99), madeFailureDetailsa frozen dataclass for JSON-safe history export (PR #172), and added fully-qualified error types plusis_caused_by()(PR #194). However, all public proto-to-model paths still discardinnerFailureandproperties, and workers cannot populate custom properties.The current .NET and Java implementations agree on the portable property model:
None/null, strings, booleans, numbers, nested string-keyed mappings, and lists are represented withgoogle.protobuf.Value; unsupported values fall back to their string representation. This path is independent of the payloadDataConverter. Protobuf numbers are float64, so integer-like values read back asfloat. Date/time values are not inferred on read: review of extension PR #3215 rejected prefix-baseddt:/dto:type guessing because ordinary customer strings could be irreversibly coerced. Python should require providers to stringify date/time values and treat prefixed values from .NET as opaque strings.Target behavior
Add an optional Python
ExceptionPropertiesProviderprotocol with:When configured on a worker, it is called for the top-level exception and each captured inner exception.
Noneor an empty mapping omits the wire map. If the provider raises, log a warning with the exception and continue reporting the task failure without custom properties, matching Java's worker-safety behavior.Extend the existing frozen
FailureDetailsdataclass by appending backward-compatible defaulted fields:Keep stored properties as ordinary JSON-safe dictionaries so
dataclasses.asdict()and history export continue to work after PR #172.Implementation plan
durabletask/internal/helpers.py, add Python-to-google.protobuf.Valueand protobuf-to-Python conversion for null, bool, number (includingDecimal, encoded as float64), string, nested mapping, and list values, with string fallback for unsupported outbound values. Do not add date/time type inference or prefix parsing. Extendnew_failure_details()to accept an optional provider/logger, preserve the existing cycle/depth protection, and apply the provider recursively.inner_failureandpropertiestodurabletask.task.FailureDetails, then centralize protobuf-to-FailureDetailsconversion and reuse it fromTaskFailedError,client.parse_orchestration_state(),history._failure_details(), and entity failure conversion. Also teach_failure_details_from_core_dict()to consume legacyPropertiesand recursiveInnerFailure. This covers task failures, orchestration state queries, entity failures, and all failure-bearing history events without separate partial implementations.exception_properties_providertoTaskHubGrpcWorker, retain it on the worker, and thread it through_OrchestrationExecutor/_RuntimeOrchestrationContextso user-orchestrator failures are enriched. Use it for activity failures, entity-operation failures, output-serialization failures, and outer worker failure paths that createTaskFailureDetails.DurableTaskSchedulerWorker; export the provider protocol from the public package surface. Existing constructors and behavior remain unchanged when no provider is supplied.azure-functions-durable2.x package was added after this issue was opened and creates internal workers per invocation, while activity exceptions are reported by the Functions host/worker pipeline. Core read support will make incoming properties visible immediately. A Functions-specific registration/serialization surface should be designed separately rather than blocking core and DTS support in this issue.Decimal/float64 conversion, and nested combination; null/empty mappings; unsupported-value string fallback; recursive properties on__cause__/__context__; cycle/depth limits; selective providers; provider exceptions; and no-provider compatibility. Add worker-level tests for orchestration, activity, and entity failures; round-trip tests throughTaskFailedError, orchestration state, and history events; a history JSON-serialization regression test; an Azure Managed constructor/forwarding test; and a fixture that consumes .NET-produced properties.CHANGELOG.mdplusdurabletask-azuremanaged/CHANGELOG.mdunderUnreleased.Acceptance criteria
TaskFailedError.details, orchestration-state APIs, and history APIs.FailureDetails(...)construction and workers without a provider behave unchanged.