Portal Community

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 TypeSerializable?
Anonymous objects, POCOs, primitivesYes
JsonElementYes
Streams, file handles, open connectionsNo — will throw
Delegates, lambdasNo — will throw
Circular object referencesNo — will throw
Design for serializability if HIL nodes exist downstream. If your executor runs before a HIL node in the workflow, ensure that everything you write to ExecutionMemory (via nodeOutputs or SetGlobal) is JSON-serializable. The serialization failure will occur at suspension time — after your node has already run — which is a hard error.