diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/IWorkflowActorRoleResolver.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/IWorkflowActorRoleResolver.cs
new file mode 100644
index 000000000..7a9fd0f77
--- /dev/null
+++ b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/IWorkflowActorRoleResolver.cs
@@ -0,0 +1,45 @@
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace StellaOps.Workflow.Abstractions;
+
+///
+/// Resolves the authenticated actor's identity (ID + roles) from the server-side
+/// request context. Implementations may query a database, read JWT claims, or
+/// fall back to client-supplied values.
+///
+public interface IWorkflowActorRoleResolver
+{
+ ///
+ /// Returns the actor ID for the current request.
+ /// Implementations should prefer server-side identity over client-supplied values.
+ ///
+ string ResolveActorId(string? clientActorId);
+
+ ///
+ /// Resolves user roles from a trusted source (database, identity provider).
+ /// Falls back to when no server-side context
+ /// is available (e.g., during signal pump processing).
+ ///
+ Task> ResolveActorRolesAsync(
+ string actorId,
+ IReadOnlyCollection? clientRoles,
+ CancellationToken cancellationToken = default);
+}
+
+///
+/// Default no-op resolver that passes through client-supplied values.
+/// Used when no database-backed role resolution is configured.
+///
+public sealed class NullWorkflowActorRoleResolver : IWorkflowActorRoleResolver
+{
+ public string ResolveActorId(string? clientActorId)
+ => clientActorId ?? string.Empty;
+
+ public Task> ResolveActorRolesAsync(
+ string actorId,
+ IReadOnlyCollection? clientRoles,
+ CancellationToken cancellationToken = default)
+ => Task.FromResult>(clientRoles ?? []);
+}
diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalDefinitionCompiler.Helpers.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalDefinitionCompiler.Helpers.cs
index a39bde064..915e74b1e 100644
--- a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalDefinitionCompiler.Helpers.cs
+++ b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowCanonicalDefinitionCompiler.Helpers.cs
@@ -179,8 +179,10 @@ public static partial class WorkflowCanonicalDefinitionCompiler
},
ResultKey = resultKey,
TimeoutSeconds = timeoutSeconds,
- WhenFailure = failureHandlers?.HasFailureBranch == true ? BuildSequence(failureHandlers.WhenFailure) : null,
- WhenTimeout = failureHandlers?.HasTimeoutBranch == true ? BuildSequence(failureHandlers.WhenTimeout) : null,
+ // Preserve empty handlers (0 steps) as empty sequences — they mean "ignore failure and continue".
+ // Only set to null when no handler was provided at all.
+ WhenFailure = failureHandlers is not null ? BuildSequence(failureHandlers.WhenFailure) : null,
+ WhenTimeout = failureHandlers is not null ? BuildSequence(failureHandlers.WhenTimeout) : null,
};
}
diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowDeclarativeAbstractions.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowDeclarativeAbstractions.cs
index 7d373c17f..8ba98212b 100644
--- a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowDeclarativeAbstractions.cs
+++ b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowDeclarativeAbstractions.cs
@@ -726,6 +726,104 @@ public sealed class WorkflowFlowBuilder
null);
}
+ public WorkflowFlowBuilder Call(
+ string stepName,
+ Address address,
+ WorkflowExpressionDefinition payloadExpression,
+ Action> whenFailure,
+ Action>? whenTimeout = null)
+ {
+ return AddMicroserviceCall(
+ stepName,
+ address.MicroserviceName,
+ address.Command,
+ context => WorkflowCanonicalExpressionRuntime.Evaluate(payloadExpression, context),
+ payloadExpression,
+ null,
+ whenFailure,
+ whenTimeout);
+ }
+
+ public WorkflowFlowBuilder Call(
+ string stepName,
+ Address address,
+ WorkflowExpressionDefinition payloadExpression,
+ WorkflowHandledBranchAction onFailure,
+ WorkflowHandledBranchAction onTimeout = WorkflowHandledBranchAction.None)
+ {
+ return AddMicroserviceCall(
+ stepName,
+ address.MicroserviceName,
+ address.Command,
+ context => WorkflowCanonicalExpressionRuntime.Evaluate(payloadExpression, context),
+ payloadExpression,
+ null,
+ null,
+ null,
+ onFailure,
+ onTimeout);
+ }
+
+ public WorkflowFlowBuilder Call(
+ string stepName,
+ Address address,
+ WorkflowExpressionDefinition payloadExpression,
+ string? resultKey = null)
+ {
+ ArgumentNullException.ThrowIfNull(payloadExpression);
+ return AddMicroserviceCall(
+ stepName,
+ address.MicroserviceName,
+ address.Command,
+ context => WorkflowCanonicalExpressionRuntime.Evaluate(payloadExpression, context),
+ payloadExpression,
+ resultKey ?? stepName,
+ null,
+ null);
+ }
+
+ public WorkflowFlowBuilder Call(
+ string stepName,
+ Address address,
+ WorkflowExpressionDefinition payloadExpression,
+ Action> whenFailure,
+ Action>? whenTimeout = null,
+ string? resultKey = null)
+ {
+ ArgumentNullException.ThrowIfNull(payloadExpression);
+ return AddMicroserviceCall(
+ stepName,
+ address.MicroserviceName,
+ address.Command,
+ context => WorkflowCanonicalExpressionRuntime.Evaluate(payloadExpression, context),
+ payloadExpression,
+ resultKey,
+ whenFailure,
+ whenTimeout);
+ }
+
+ public WorkflowFlowBuilder Call(
+ string stepName,
+ Address address,
+ WorkflowExpressionDefinition payloadExpression,
+ WorkflowHandledBranchAction onFailure,
+ WorkflowHandledBranchAction onTimeout = WorkflowHandledBranchAction.None,
+ string? resultKey = null)
+ {
+ ArgumentNullException.ThrowIfNull(payloadExpression);
+ return AddMicroserviceCall(
+ stepName,
+ address.MicroserviceName,
+ address.Command,
+ context => WorkflowCanonicalExpressionRuntime.Evaluate(payloadExpression, context),
+ payloadExpression,
+ resultKey,
+ null,
+ null,
+ onFailure,
+ onTimeout);
+ }
+
public WorkflowFlowBuilder Call(
string stepName,
Address address,
diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs
index 77486a4f1..e0d8cd899 100644
--- a/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs
+++ b/src/Workflow/__Libraries/StellaOps.Workflow.Abstractions/WorkflowRegistrationAbstractions.cs
@@ -78,6 +78,9 @@ public static class WorkflowRegistrationServiceCollectionExtensions
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
{
PropertyNameCaseInsensitive = true,
+ // Allow floating-point JSON numbers (e.g., 201000256548.0) to deserialize into integer
+ // types (long?, int?). This is needed because the workflow state round-trips through
+ // JSON serialization which may add ".0" to integer values.
NumberHandling = global::System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString,
};
@@ -191,6 +194,9 @@ public static class WorkflowRegistrationServiceCollectionExtensions
return new Dictionary(payload, StringComparer.OrdinalIgnoreCase);
}
+ // Normalize decimal-valued integers (e.g., 201000256548.0 → 201000256548)
+ // before serialization. This is needed because the workflow state may store
+ // integer values as decimals after JSON round-trips.
var normalized = NormalizePayloadNumbers(payload);
var json = JsonSerializer.Serialize(normalized, SerializerOptions);
return JsonSerializer.Deserialize(json, startRequestType, SerializerOptions)
diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowStartContracts.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowStartContracts.cs
index 8c4d3716e..bf9270c83 100644
--- a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowStartContracts.cs
+++ b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowStartContracts.cs
@@ -16,4 +16,6 @@ public sealed record StartWorkflowResponse
public required string WorkflowName { get; init; }
public required string WorkflowVersion { get; init; }
public WorkflowBusinessReference? BusinessReference { get; init; }
+ public IReadOnlyCollection NextTasks { get; init; } = [];
+ public IDictionary WorkflowState { get; init; } = new Dictionary();
}
diff --git a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowTaskContracts.cs b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowTaskContracts.cs
index 0df8f01ac..06e080f81 100644
--- a/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowTaskContracts.cs
+++ b/src/Workflow/__Libraries/StellaOps.Workflow.Contracts/WorkflowTaskContracts.cs
@@ -87,6 +87,9 @@ public sealed record WorkflowTaskCompleteResponse
{
public required string WorkflowTaskId { get; init; }
public required bool Completed { get; init; }
+ public string? WorkflowInstanceId { get; init; }
+ public IReadOnlyCollection NextTasks { get; init; } = [];
+ public IDictionary WorkflowState { get; init; } = new Dictionary();
}
public sealed record WorkflowTaskAssignRequest