Serialization for HIL
When an execution suspends for Human-in-the-Loop (HIL) review, the entire ExecutionMemory is serialized to JSON and stored in Process_SuspendedExecutions.ExecutionMemoryJson. On resume, the JSON is deserialized and execution continues from where it left off with the full memory state restored.
Why Serialization Is Needed
HIL suspension may pause execution for hours or days — far longer than any in-process object can survive. The memory must be durable so that when an approver acts, the execution engine can restore the exact state that was present before suspension.
ExecutionMemorySerializer
// ExecutionMemorySerializer.cs
public class ExecutionMemorySerializer
{
private readonly JsonSerializerOptions _options = new()
{
WriteIndented = false,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
public string Serialize(ExecutionMemory memory)
{
var dto = new ExecutionMemoryDto
{
NodeOutputs = memory.GetAllNodeOutputs(), // Dictionary<string, object?>
GlobalVariables = memory.GetAllGlobalVariables(), // Dictionary<string, object?>
Metadata = memory.Metadata
};
return JsonSerializer.Serialize(dto, _options);
}
public ExecutionMemory Deserialize(string json)
{
var dto = JsonSerializer.Deserialize<ExecutionMemoryDto>(json, _options)
?? throw new InvalidOperationException("Failed to deserialize ExecutionMemory");
var memory = new ExecutionMemory(dto.Metadata);
foreach (var (nodeId, output) in dto.NodeOutputs)
memory.SetNodeOutput(nodeId, output);
foreach (var (key, value) in dto.GlobalVariables)
memory.SetGlobal(key, value);
return memory;
}
}
Serialized JSON Structure
// Example serialized ExecutionMemory stored in Process_SuspendedExecutions
{
"metadata": {
"executionId" : "a3f8...",
"processId" : 42,
"tenantId" : "tenant-001",
"triggeredBy" : "user-007",
"startedAt" : "2026-05-21T09:00:00Z"
},
"nodeOutputs": {
"fetch-customer" : { "customerId": "C-100", "name": "Acme Corp", "tierLevel": "Gold" },
"validate-order" : { "isValid": true, "orderTotal": 1500.00 },
"approval-hil-node" : null // Suspended here — output not yet set
},
"globalVariables": {
"correlationId" : "txn-99887",
"retryCount" : 0
}
}
Database Storage
-- Process_SuspendedExecutions (relevant columns)
CREATE TABLE Process_SuspendedExecutions (
ExecutionId NVARCHAR(64) NOT NULL PRIMARY KEY,
ProcessId INT NOT NULL,
TenantId NVARCHAR(64) NOT NULL,
SuspendedNodeId NVARCHAR(256) NOT NULL,
ExecutionMemoryJson NVARCHAR(MAX) NOT NULL, -- Serialized ExecutionMemory
SuspendedAt DATETIMEOFFSET NOT NULL,
ExpiresAt DATETIMEOFFSET NULL,
-- ... approval-related columns
);
Resume Path
// WorkflowExecutionService.ResumeAsync — called when approver submits decision
public async Task ResumeAsync(string executionId, ResumePayload payload, CancellationToken ct)
{
var suspended = await _repo.GetSuspendedExecutionAsync(executionId, ct);
// Restore ExecutionMemory from the serialized JSON
var memory = _serializer.Deserialize(suspended.ExecutionMemoryJson);
// Write the approver's decision into the restored memory
memory.SetNodeOutput(suspended.SuspendedNodeId, payload.Decision);
// Continue from the next node after the suspended node
await RunFromNodeAsync(suspended.SuspendedNodeId, memory, ct);
}
Serializability Requirement
All values stored in nodeOutputs and globalVariables must be JSON-serializable. If a non-serializable value is in memory at suspension time, the serialization will fail and the suspension operation will throw. This is a critical design constraint for any executor that runs before a HIL node.
| Value Type | Serializable? |
|---|---|
| Anonymous objects, POCOs, primitives | Yes |
JsonElement | Yes |
| Streams, file handles, open connections | No — will throw |
| Delegates, lambdas | No — will throw |
| Circular object references | No — will throw |