Portal Community

What It Actually Is

NodeExecutionResult.OutputData (Dictionary<string, object>) is the only thing an executor's own code is responsible for writing. Every node executor's ExecuteInternalAsync builds this dictionary — field names and shapes are entirely up to the node (e.g. status, count, items).

The Seeding Step You Might Not Expect

Before any executor-specific code runs, a framework-level step called InitializeOutputDataFromInput copies some of InputData into the brand-new OutputData — so every node starts from a consistent baseline instead of each one reimplementing "carry my input forward" independently:

// BaseNodeExecutor.EntryProcess.cs:48-75
private void InitializeOutputDataFromInput(ProcessElementExecutionContext executionContext)
{
    var outputData = executionContext.NodeResult?.OutputData;
    if (outputData == null) return;

    var inputData = executionContext.InputData;
    var mode = settings?.InitialOutputFrom ?? InitialOutputDataMode.ItemsOnly;

    switch (mode)
    {
        case InitialOutputDataMode.InputAsIs:
            foreach (var kvp in inputData) outputData[kvp.Key] = kvp.Value;
            break;

        case InitialOutputDataMode.ItemsOnly:
            if (inputData.TryGetValue("items", out var items))
                outputData["items"] = items;
            break;

        case InitialOutputDataMode.Empty:
        default:
            break;
    }
}
ModeBehaviorDefault?
ItemsOnlyCopies only the items key from InputData into OutputData, by reference (not cloned).✅ Yes
InputAsIsCopies every key from InputData into OutputData, by reference.No
EmptyOutputData starts as a genuinely empty dictionary.No
"By reference" matters Neither mode clones anything. If a node doesn't go on to overwrite items itself, its OutputData's items is the literal same list object as whatever it received on InputData — mutating one mutates the other. Most nodes that touch items (Collection Operation, Data Mapping) always overwrite it themselves via FinalizeOutputItems_WrapListIntoItems, so this seed value is normally short-lived — but a node that reads items without rewriting it will silently pass the upstream reference straight through.

The Standard Envelope Shape

Most nodes' real record output lives under one key, items, in this shape:

{
  "items": [
    { "json": { /* record 1 */ } },
    { "json": { /* record 2 */ } }
  ],
  "status": "success"
}

Whether a node's raw result becomes one item wrapping everything, or one item per record, is controlled by TranslateOutputDataMode (Skip / Regular / SplitIntoArray / CopyFromInput) via the shared WrapJsonIntoItems helper on BaseNodeExecutor. SplitIntoArray is what most list-producing nodes (Collection Operation, Data Mapping) override their default to, so each record becomes its own downstream item instead of the whole array being wrapped as one.

Getting Data Out of OutputData Cleanly

The shared ResolveSourceRecords / ExtractRecordsFromItems helpers on BaseNodeExecutor read either a node's configured inlineDataSource or InputData's items, and normalize either into a flat List<object?> of records (unwrapping each entry's json value). This is the same mechanism both Collection Operation and Data Mapping use, so both nodes accept either an upstream item list or manually-typed inline JSON, without either executor reimplementing the parsing.