Flow Studio
Returning Pinned Data
An executor returns pinned data by passing it as the second argument to NodeExecutionResult.Success(output, pinnedData). The first argument is the ephemeral output for this run; the second argument is the object that will be persisted to the database and survive future runs.
NodeExecutionResult.Success Overloads
// NodeExecutionResult.cs
public class NodeExecutionResult
{
// Ephemeral output only — pinned data is NOT written
public static NodeExecutionResult Success(object? output)
=> new() { IsSuccess = true, Output = output, PinnedData = null };
// Ephemeral output + pinned data — BOTH are set
public static NodeExecutionResult Success(object? output, object? pinnedData)
=> new() { IsSuccess = true, Output = output, PinnedData = pinnedData };
public static NodeExecutionResult Failure(string error)
=> new() { IsSuccess = false, ErrorMessage = error };
public bool IsSuccess { get; init; }
public object? Output { get; init; } // Ephemeral — this run only
public object? PinnedData { get; init; } // Durable — persists across runs
public string? ErrorMessage { get; init; }
}
Executor Example — Returning Both
// SummarizationNode.cs — accumulates and pins the latest summary
public override async Task<NodeExecutionResult> ExecuteAsync(
NodeExecutionContext ctx,
CancellationToken ct)
{
var text = ctx.ExecutionMemory.GetNodeOutput<string>("text-source-node");
var summary = await _aiClient.SummarizeAsync(text, ct);
// The summary for THIS run (ephemeral — passed downstream via $output)
var output = new { Summary = summary, GeneratedAt = DateTime.UtcNow };
// The pinned record — persists after this run ends
// Future runs can read this via ctx.ExecutionMemory.GetPinnedData("summarization-node")
var pinnedData = new
{
LatestSummary = summary,
LatestRunAt = DateTime.UtcNow,
TotalRuns = (await ctx.ExecutionMemory.GetPinnedData("summarization-node")
as dynamic)?.TotalRuns + 1 ?? 1
};
return NodeExecutionResult.Success(output, pinnedData);
}
What BaseNodeExecutor Does After Success
The executor returns NodeExecutionResult — it does NOT call the persistence service directly. BaseNodeExecutor inspects the result and handles persistence:
// BaseNodeExecutor.cs (simplified)
protected async Task<NodeExecutionResult> RunAndPersistAsync(
NodeExecutionContext ctx,
CancellationToken ct)
{
var result = await ExecuteAsync(ctx, ct);
if (result.IsSuccess)
{
// 1. Store ephemeral output in-memory for this execution
ctx.ExecutionMemory.SetNodeOutput(ctx.NodeId, result.Output);
// 2. If pinnedData is non-null, persist it to the database
if (result.PinnedData is not null)
{
await _pinnedDataService.SaveAsync(
ctx.ProcessId,
ctx.NodeId,
result.PinnedData,
ct);
}
}
return result;
}
PinnedData Serialization
Pinned data is serialized to JSON before storage. The object you pass as pinnedData must be JSON-serializable. Anonymous types, POCOs, and dictionaries all work.
| Type | Supported? | Notes |
|---|---|---|
Anonymous object new { Key = val } | Yes | Most common pattern |
| POCO class | Yes | Properties serialized by name |
Dictionary<string, object> | Yes | Flexible, no schema needed |
string (raw text) | Yes | Stored as a JSON string literal |
| Non-serializable (e.g., streams, open connections) | No | Will throw at serialization time |
Passing
null as pinnedData does nothing. If you call NodeExecutionResult.Success(output, null), BaseNodeExecutor skips the persistence call entirely. The existing pinned data for that node is not affected — it is neither overwritten nor deleted.