Portal Community

ExecutionMemory Class

// ExecutionMemory.cs
public class ExecutionMemory : IExecutionMemory
{
    // Primary data bus — stores output from each node, keyed by nodeId
    private readonly Dictionary<string, object?> _nodeOutputs = new();

    // Cross-node shared state — any node can read or write
    private readonly Dictionary<string, object?> _globalVariables = new();

    // Engine-managed metadata — read-only to executors
    public ExecutionMetadata Metadata { get; init; }

    // --- nodeOutputs ---
    public void   SetNodeOutput(string nodeId, object? value)   => _nodeOutputs[nodeId] = value;
    public object? GetNodeOutput(string nodeId)                  => _nodeOutputs.GetValueOrDefault(nodeId);
    public T?     GetNodeOutput<T>(string nodeId) where T : class
                  => GetNodeOutput(nodeId) as T;

    // --- globalVariables ---
    public void   SetGlobal(string key, object? value)          => _globalVariables[key] = value;
    public object? GetGlobal(string key)                         => _globalVariables.GetValueOrDefault(key);
    public T?     GetGlobal<T>(string key) where T : class      => GetGlobal(key) as T;

    // --- pinned data (delegates to PinnedDataService — async database call) ---
    public Task<object?> GetPinnedDataAsync(string nodeId, CancellationToken ct = default);
    public Task<T?>      GetPinnedDataAsync<T>(string nodeId, CancellationToken ct = default)
                          where T : class;
    public Task           ClearPinnedDataAsync(string nodeId, CancellationToken ct = default);
}

ExecutionMetadata

// ExecutionMetadata — read-only, set by the engine before execution starts
public record ExecutionMetadata
{
    public string  ExecutionId  { get; init; }  // GUID — unique per run
    public int     ProcessId    { get; init; }  // The workflow definition ID
    public string  TenantId     { get; init; }
    public string? TriggeredBy  { get; init; }  // UserId, scheduled job name, or API key label
    public DateTimeOffset StartedAt { get; init; }
}

IExecutionMemory Interface

Executors always receive IExecutionMemory, not the concrete ExecutionMemory class. This enables unit testing with mocks and ensures executors cannot reach internal state.

// IExecutionMemory.cs
public interface IExecutionMemory
{
    // Node output store (ephemeral — this run only)
    void    SetNodeOutput(string nodeId, object? value);
    object? GetNodeOutput(string nodeId);
    T?      GetNodeOutput<T>(string nodeId) where T : class;

    // Previous node shortcut (see reading-upstream.html)
    object? GetPreviousNodeOutput();
    T?      GetPreviousNodeOutput<T>() where T : class;

    // Global variables (ephemeral — this run only)
    void    SetGlobal(string key, object? value);
    object? GetGlobal(string key);
    T?      GetGlobal<T>(string key) where T : class;

    // Pinned data (durable — persists across runs, async database calls)
    Task<object?> GetPinnedDataAsync(string nodeId, CancellationToken ct = default);
    Task<T?>      GetPinnedDataAsync<T>(string nodeId, CancellationToken ct = default) where T : class;
    Task           ClearPinnedDataAsync(string nodeId, CancellationToken ct = default);

    // Metadata
    ExecutionMetadata Metadata { get; }
}

Dictionary Characteristics

DictionaryThread SafetyKey TypeValue Type
_nodeOutputsSynchronised — single-threaded execution engine; nodes do not run concurrentlystring (nodeId)object? (any JSON-serializable value)
_globalVariablesSame — no concurrent access during normal executionstring (variable name)object?
MetadataImmutable record — always safe to read
Nodes do not run concurrently in the current engine. The execution engine processes nodes in topological order — upstream nodes complete before downstream nodes begin. This means _nodeOutputs does not need to be a ConcurrentDictionary. If the engine ever supports parallel branches in the future, this design will need to be revisited.