Flow Studio
Use Cases
Pinned data is the right choice whenever a node needs to remember something across workflow invocations. The three primary patterns are: AI agent memory, running accumulation, and short-lived caching.
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?
| Question | If 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).