Portal Community

Pattern 1 — AI Agent Memory

Conversational AI nodes need to retain message history across workflow triggers. Each trigger is a new execution, so ephemeral output would lose the conversation. Pinned data provides the persistent memory layer.

// ConversationNode.cs — classic AI memory pattern
public override async Task<NodeExecutionResult> ExecuteAsync(
    NodeExecutionContext ctx,
    CancellationToken ct)
{
    // 1. Load history from previous run (null on first run)
    var history = await ctx.ExecutionMemory
        .GetPinnedDataAsync<ChatHistory>(ctx.NodeId, ct)
        ?? new ChatHistory();

    // 2. Append new user message
    var userInput = ctx.ExecutionMemory.GetNodeOutput<string>("user-input-node");
    history.Messages.Add(new Message { Role = "user", Content = userInput });

    // 3. Get AI reply (history provides context)
    var reply = await _llm.CompleteAsync(history.Messages, ct);
    history.Messages.Add(new Message { Role = "assistant", Content = reply });

    // 4. Save updated history — pin for next run
    return NodeExecutionResult.Success(
        output     : reply,
        pinnedData : history  // Persists across runs
    );
}

Pattern 2 — Running Accumulation

A node that aggregates data across multiple invocations — a counter, a running total, a rolling average — uses pinned data to carry the accumulated state forward.

// SalesAggregatorNode.cs — running totals across workflow triggers
public override async Task<NodeExecutionResult> ExecuteAsync(
    NodeExecutionContext ctx,
    CancellationToken ct)
{
    var thisSale = ctx.ExecutionMemory.GetNodeOutput<decimal>("sale-input-node");

    var agg = await ctx.ExecutionMemory
        .GetPinnedDataAsync<SalesAggregate>(ctx.NodeId, ct)
        ?? new SalesAggregate { TotalSales = 0, Count = 0 };

    agg.TotalSales += thisSale;
    agg.Count++;
    agg.LastUpdated = DateTime.UtcNow;

    return NodeExecutionResult.Success(
        output     : new { ThisSale = thisSale, RunningTotal = agg.TotalSales },
        pinnedData : agg
    );
}

Pattern 3 — Short-Lived Cache

A node that calls an external API can cache the response using pinned data with a TTL, avoiding redundant calls when the workflow triggers frequently.

// ExchangeRateNode.cs — caches rate for 1 hour
public override async Task<NodeExecutionResult> ExecuteAsync(
    NodeExecutionContext ctx,
    CancellationToken ct)
{
    var cached = await ctx.ExecutionMemory
        .GetPinnedDataAsync<CachedRate>(ctx.NodeId, ct);

    // Use cache if fresh
    if (cached is not null && cached.FetchedAt > DateTime.UtcNow.AddHours(-1))
    {
        ctx.Observability.Logger.LogInformation("Using cached exchange rate");
        return NodeExecutionResult.Success(cached.Rate);
    }

    // Fetch fresh
    var rate = await _fxApi.GetUsdToEurRateAsync(ct);

    return NodeExecutionResult.Success(
        output     : rate,
        pinnedData : new CachedRate { Rate = rate, FetchedAt = DateTime.UtcNow },
        pinnedDataTtl: TimeSpan.FromHours(2)  // Auto-expire after 2 hours
    );
}

Decision Guide — Do I Need Pinned Data?

QuestionIf Yes
Does the node need data from a previous run?Use pinned data
Is the data only needed within one execution run?Use ephemeral output (GetNodeOutput)
Does the data grow or accumulate over time?Use pinned data with read-modify-write pattern
Is the data needed by multiple future runs but only briefly?Use pinned data with TTL
Is the data large and should be auditable per-run?Write to a database or event log instead
Is it user-specific state shared across many workflows?Use SetGlobal/GetGlobal instead
Pinned data is node-scoped, not user-scoped. Pinned data is keyed to (processId, nodeId), meaning all users running the same workflow share the same pinned data slot for a given node. If you need per-user state, include the userId in the pinned data object structure, or use a different storage mechanism (e.g., a database table with a userId column).