diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalExpressionRuntime.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalExpressionRuntime.cs
index fabb7c215..7e53dbb07 100644
--- a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalExpressionRuntime.cs
+++ b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalExpressionRuntime.cs
@@ -480,10 +480,12 @@ public static class WorkflowCanonicalExpressionRuntime
JsonValueKind.Number when element.TryGetInt64(out var int64Value) => int64Value,
JsonValueKind.Number when element.TryGetDecimal(out var decimalValue) => decimalValue,
JsonValueKind.Number when element.TryGetDouble(out var doubleValue) => doubleValue,
- JsonValueKind.Object => element.EnumerateObject().ToDictionary(
- x => x.Name,
- x => ToRuntimeValue(x.Value),
- StringComparer.OrdinalIgnoreCase),
+ JsonValueKind.Object => element.EnumerateObject()
+ .GroupBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(
+ g => g.Key,
+ g => ToRuntimeValue(g.Last().Value),
+ StringComparer.OrdinalIgnoreCase),
JsonValueKind.Array => element.EnumerateArray().Select(ToRuntimeValue).ToArray(),
_ => element.ToString(),
};
diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowExecutionActorContext.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowExecutionActorContext.cs
new file mode 100644
index 000000000..871ce857f
--- /dev/null
+++ b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowExecutionActorContext.cs
@@ -0,0 +1,35 @@
+using System.Threading;
+
+namespace StellaOps.Workflow.Abstractions;
+
+///
+/// Propagates the authenticated actor ID through async workflow execution chains
+/// (e.g., OnComplete -> Call -> transport) where the original request context
+/// is not available. Implementations set ActorId and CallerUserId during
+/// task-complete/start-workflow processing.
+///
+public sealed class WorkflowExecutionActorContext
+{
+ private static readonly AsyncLocal CurrentActorId = new();
+ private static readonly AsyncLocal CurrentCallerUserId = new();
+
+ ///
+ /// The actor ID from the task operation request (e.g., numeric user account ID).
+ /// Used for role resolution and task assignment.
+ ///
+ public string? ActorId
+ {
+ get => CurrentActorId.Value;
+ set => CurrentActorId.Value = value;
+ }
+
+ ///
+ /// The authenticated caller's user ID extracted from the HTTP request context.
+ /// Used by transport implementations for backend service calls.
+ ///
+ public string? CallerUserId
+ {
+ get => CurrentCallerUserId.Value;
+ set => CurrentCallerUserId.Value = value;
+ }
+}
diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs
index 770d41846..77486a4f1 100644
--- a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs
+++ b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs
@@ -78,6 +78,7 @@ public static class WorkflowRegistrationServiceCollectionExtensions
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true,
+ NumberHandling = global::System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString,
};
public static IServiceCollection AddWorkflowRegistration(
@@ -190,7 +191,8 @@ public static class WorkflowRegistrationServiceCollectionExtensions
return new Dictionary(payload, StringComparer.OrdinalIgnoreCase);
}
- var json = JsonSerializer.Serialize(payload, SerializerOptions);
+ var normalized = NormalizePayloadNumbers(payload);
+ var json = JsonSerializer.Serialize(normalized, SerializerOptions);
return JsonSerializer.Deserialize(json, startRequestType, SerializerOptions)
?? throw new InvalidOperationException(
$"Unable to bind workflow payload to '{startRequestType.FullName}'.");
@@ -291,5 +293,27 @@ public static class WorkflowRegistrationServiceCollectionExtensions
};
}
+ private static IDictionary NormalizePayloadNumbers(IDictionary payload)
+ {
+ var result = new Dictionary(payload.Count, StringComparer.OrdinalIgnoreCase);
+ foreach (var kvp in payload)
+ {
+ result[kvp.Key] = NormalizeValue(kvp.Value);
+ }
+ return result;
+ }
+
+ private static object? NormalizeValue(object? value)
+ {
+ return value switch
+ {
+ decimal d when d == Math.Truncate(d) && d >= long.MinValue && d <= long.MaxValue => (long)d,
+ double d when d == Math.Truncate(d) && d >= long.MinValue && d <= long.MaxValue => (long)d,
+ IDictionary dict => NormalizePayloadNumbers(dict),
+ object[] arr => arr.Select(NormalizeValue).ToArray(),
+ _ => value,
+ };
+ }
+
private sealed record WorkflowBusinessReferencePartProperty(PropertyInfo Property, string PartName);
}