Flow Studio
Memory Structure
The ExecutionMemory class contains three dictionaries: nodeOutputs for per-node output data, globalVariables for shared state, and executionMetadata for engine-managed fields. All three are in-memory and live for the duration of the execution.
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
| Dictionary | Thread Safety | Key Type | Value Type |
|---|---|---|---|
_nodeOutputs | Synchronised — single-threaded execution engine; nodes do not run concurrently | string (nodeId) | object? (any JSON-serializable value) |
_globalVariables | Same — no concurrent access during normal execution | string (variable name) | object? |
Metadata | Immutable 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.