Portal Community

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.

TypeSupported?Notes
Anonymous object new { Key = val }YesMost common pattern
POCO classYesProperties serialized by name
Dictionary<string, object>YesFlexible, no schema needed
string (raw text)YesStored as a JSON string literal
Non-serializable (e.g., streams, open connections)NoWill 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.