fix(core): Only record sub-node error metadata for nodes that get a run (#37256)

This commit is contained in:
Veljko Terzic
2026-08-31 06:40:43 +00:00
committed by GitHub
parent 6718392786
commit 8fcabd6595
3 changed files with 636 additions and 3 deletions
@@ -0,0 +1,624 @@
import { Container } from '@n8n/di';
import { createDeferredPromise } from '@n8n/utils/promise/deferred-promise';
import type {
IConnections,
IExecuteFunctions,
INode,
INodeParameters,
INodeType,
INodeTypeData,
IRun,
ISourceData,
ISupplyDataFunctions,
ITaskMetadata,
ITaskSubRunMetadata,
NodeConnectionType,
SupplyData,
} from 'n8n-workflow';
import { NodeConnectionTypes, UnexpectedError, Workflow } from 'n8n-workflow';
import { mock } from 'vitest-mock-extended';
import { ErrorReporter } from '@/errors/error-reporter';
import * as Helpers from '@test/helpers';
import { WorkflowExecute } from '../workflow-execute';
/**
* When an AI sub-node fails to start, the engine records the failure on the sub-node itself
* and also notes it against the parent — filed under the run index the parent is expected to
* get next (`runData[parent].length`). At the end of the execution `moveNodeMetadata` merges
* each note onto the matching run, and reports any note that has no run to merge onto.
*
* That expected index is right for a top-level parent: the engine appends its error run a
* moment later. A sub-node parent never gets one, so the note is left pointing at nothing —
* which is what CAT-3665 is about.
*
* The real engine drives every test. The only authored parts are the node types below, whose
* behaviour comes from node parameters rather than mocks.
*/
const triggerNodeType: INodeType = {
description: {
displayName: 'Test Trigger',
name: 'testTrigger',
group: ['trigger'],
version: 1,
description: '',
defaults: { name: 'Test Trigger' },
inputs: [],
outputs: [NodeConnectionTypes.Main],
properties: [],
},
async execute() {
return [[{ json: {} }]];
},
};
const agentNodeType: INodeType = {
description: {
displayName: 'Test Agent',
name: 'testAgent',
group: ['transform'],
version: 1,
description: '',
defaults: { name: 'Test Agent' },
inputs: [
NodeConnectionTypes.Main,
{ type: NodeConnectionTypes.AiLanguageModel, required: true, maxConnections: 1 },
{ type: NodeConnectionTypes.AiTool },
],
outputs: [NodeConnectionTypes.Main],
properties: [
{ displayName: 'Tool Invocations', name: 'toolInvocations', type: 'number', default: 0 },
],
},
async execute(this: IExecuteFunctions) {
await this.getInputConnectionData(NodeConnectionTypes.AiLanguageModel, 0);
const invocations = this.getNodeParameter('toolInvocations', 0, 0) as number;
if (invocations > 0) {
const tools = (await this.getInputConnectionData(NodeConnectionTypes.AiTool, 0)) as Array<{
invoke: (args: object) => Promise<unknown>;
}>;
for (let i = 0; i < invocations; i++) {
for (const tool of tools) {
try {
await tool.invoke({});
} catch {
// A real agent treats a failed tool call as a result and carries on
}
}
}
}
return [[{ json: { done: true } }]];
},
};
const modelNodeType: INodeType = {
description: {
displayName: 'Test Model',
name: 'testModel',
group: ['transform'],
version: 1,
description: '',
defaults: { name: 'Test Model' },
inputs: [],
outputs: [NodeConnectionTypes.AiLanguageModel],
properties: [
{ displayName: 'Fail Supply Data', name: 'failSupplyData', type: 'boolean', default: false },
],
},
// Covers the "supplyData throws" slice (missing credentials, bad config). Real chat models
// make no network call here, so bad-key/quota errors instead surface on invoke.
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
if (this.getNodeParameter('failSupplyData', itemIndex, false) === true) {
throw new Error('Invalid API key');
}
return { response: {} };
},
};
/** No `supplyData`, so the engine wraps it with the real `createNodeAsTool`. */
const toolPlainNodeType: INodeType = {
description: {
displayName: 'Test Tool',
name: 'testToolPlain',
group: ['transform'],
version: 1,
description: 'A test tool',
defaults: { name: 'Test Tool' },
inputs: [{ type: NodeConnectionTypes.AiLanguageModel, required: true, maxConnections: 1 }],
outputs: [NodeConnectionTypes.AiTool],
properties: [],
},
async execute(this: IExecuteFunctions) {
await this.getInputConnectionData(NodeConnectionTypes.AiLanguageModel, 0);
return [[{ json: { response: 'ok' } }]];
},
};
/** Mirrors `ToolVectorStore`: its setup eagerly fetches its own sub-nodes. */
const toolWithSetupNodeType: INodeType = {
description: {
displayName: 'Test Tool With Setup',
name: 'testToolWithSetup',
group: ['transform'],
version: 1,
description: 'A test tool that sets up its own sub-nodes',
defaults: { name: 'Test Tool With Setup' },
inputs: [
{ type: NodeConnectionTypes.AiLanguageModel, maxConnections: 1 },
{ type: NodeConnectionTypes.AiTool },
],
outputs: [NodeConnectionTypes.AiTool],
properties: [],
},
async supplyData(this: ISupplyDataFunctions): Promise<SupplyData> {
await this.getInputConnectionData(NodeConnectionTypes.AiLanguageModel, 0);
await this.getInputConnectionData(NodeConnectionTypes.AiTool, 0);
// Never reached in this suite: every case using it fails during the setup above
return { response: {} };
},
};
/** A tool that invokes a tool of its own, so both nesting levels get real runs. */
const toolWithNestedToolNodeType: INodeType = {
description: {
displayName: 'Test Nesting Tool',
name: 'testToolWithNestedTool',
group: ['transform'],
version: 1,
description: 'A test tool that invokes another tool',
defaults: { name: 'Test Nesting Tool' },
inputs: [{ type: NodeConnectionTypes.AiTool }],
outputs: [NodeConnectionTypes.AiTool],
properties: [],
},
async execute(this: IExecuteFunctions) {
const tools = (await this.getInputConnectionData(NodeConnectionTypes.AiTool, 0)) as Array<{
invoke: (args: object) => Promise<unknown>;
}>;
// No catch: the inner failure must surface on this tool's own run too
for (const tool of tools) await tool.invoke({});
return [[{ json: { response: 'ok' } }]];
},
};
/** Two main outputs into the same node, so that node gets two runs. */
const branchNodeType: INodeType = {
description: {
displayName: 'Test Branch',
name: 'testBranch',
group: ['transform'],
version: 1,
description: '',
defaults: { name: 'Test Branch' },
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
properties: [],
},
async execute() {
return [[{ json: {} }], [{ json: {} }]];
},
};
const nodeTypeData: INodeTypeData = {
'test.trigger': { type: triggerNodeType, sourcePath: '' },
'test.agent': { type: agentNodeType, sourcePath: '' },
'test.branch': { type: branchNodeType, sourcePath: '' },
'test.model': { type: modelNodeType, sourcePath: '' },
'test.toolPlain': { type: toolPlainNodeType, sourcePath: '' },
'test.toolWithSetup': { type: toolWithSetupNodeType, sourcePath: '' },
'test.toolWithNestedTool': { type: toolWithNestedToolNodeType, sourcePath: '' },
};
function node(name: string, type: string, parameters: INodeParameters = {}): INode {
return { id: name, name, type, typeVersion: 1, position: [0, 0], parameters };
}
/** `A --main--> B` style connection, keyed by source node as the engine expects. */
function connect(
from: string,
to: string,
type: NodeConnectionType = NodeConnectionTypes.Main,
): IConnections {
return { [from]: { [type]: [[{ node: to, type, index: 0 }]] } };
}
async function runWorkflow(nodes: INode[], connections: IConnections): Promise<IRun> {
const workflow = new Workflow({
id: 'test',
nodes,
connections,
active: false,
nodeTypes: Helpers.NodeTypes(nodeTypeData),
});
const waitPromise = createDeferredPromise<IRun>();
const additionalData = Helpers.WorkflowExecuteAdditionalData(waitPromise);
// The fake trigger has no `trigger`/`poll` method, so name the start node explicitly
await new WorkflowExecute(additionalData, 'manual').run({ workflow, startNode: nodes[0] });
return await waitPromise.promise;
}
type RunOutcome = { status?: string; error?: string };
type RunSummary = RunOutcome & { subRun?: unknown };
/**
* The saved execution as an API consumer sees it: `GET /rest|api/v1/executions/:id?includeData=true`
* returns exactly this `runData`, `metadata.subRun` included.
*
* `subRun` is written by a background promise nobody awaits, which runs after the
* `nodeExecuteAfter` hook and currently wins the race against `moveNodeMetadata`. A slow
* async handler on that hook would strand it unmerged instead (see CAT-4268).
*/
function savedExecution(result: IRun): Record<string, RunSummary[]> {
return Object.fromEntries(
Object.entries(result.data.resultData.runData).map(([name, runs]) => [
name,
Array.from(runs, (run) => ({
status: run?.executionStatus,
error: run?.error?.message,
subRun: run?.metadata?.subRun,
})),
]),
);
}
/**
* The orphan reports ("Taskdata missing at the end of an execution") the engine emitted to
* its Sentry sink. Matched by class, not message text, so rewording the message cannot
* silently empty this list. Filtering is necessary: the engine also reports every
* `BaseError` a node throws, relying on `beforeSend`/`shouldReport` to drop it — a filter
* the bare mock bypasses — so "never called" would be wrong.
*/
function orphanReports(): string[] {
return vi
.mocked(errorReporter.error)
.mock.calls.map((call) => call[0])
.filter((error): error is UnexpectedError => error instanceof UnexpectedError)
.map((error) => error.message);
}
/** The notes still waiting to be merged. */
function stagedMetadata(result: IRun): Record<string, ITaskMetadata[]> {
return result.data.executionData?.metadata ?? {};
}
/**
* The runs a node actually recorded, in order, ignoring which index they landed at. A failing
* sub-node's record goes to its *parent's* expected index, so the engine leaves holes in the
* array — an older bug this fix deliberately leaves alone, so nothing here pins it (CAT-4268).
*/
function runsOf(result: IRun, nodeName: string): RunOutcome[] {
const runs = result.data.resultData.runData[nodeName] ?? [];
return runs
.filter((run) => run !== undefined)
.map((run) => ({ status: run.executionStatus, error: run.error?.message }));
}
/** The sources of those same runs, in the same order. */
function sourcesOf(result: IRun, nodeName: string): Array<Array<ISourceData | null>> {
const runs = result.data.resultData.runData[nodeName] ?? [];
return runs.filter((run) => run !== undefined).map((run) => run.source);
}
/**
* The parent → child links recorded on a run. `addOutputData` also writes an entry naming the
* run's own node, which is noise for this fix (CAT-4268), so only cross-node entries are kept.
*/
function crossNodeSubRun(result: IRun, nodeName: string, runIndex = 0): ITaskSubRunMetadata[] {
const subRun = result.data.resultData.runData[nodeName]?.[runIndex]?.metadata?.subRun ?? [];
return subRun.filter((entry) => entry.node !== nodeName);
}
let errorReporter: ErrorReporter;
beforeEach(() => {
errorReporter = mock<ErrorReporter>();
Container.set(ErrorReporter, errorReporter);
});
afterEach(() => {
Container.reset();
});
describe('sub-node error metadata', () => {
describe('current behaviour (pinned)', () => {
test('stages the subRun entry on a top-level parent when its model fails', async () => {
const result = await runWorkflow(
[
node('Trigger', 'test.trigger'),
node('Agent', 'test.agent'),
node('Model', 'test.model', { failSupplyData: true }),
],
{
...connect('Trigger', 'Agent'),
...connect('Model', 'Agent', NodeConnectionTypes.AiLanguageModel),
},
);
expect(result.status).toBe('error');
expect(result.data.resultData.error?.message).toBe('Error in sub-node Model');
expect(savedExecution(result)).toEqual({
Trigger: [{ status: 'success', error: undefined, subRun: undefined }],
// Right target: the engine appends Agent's error run at exactly this index a moment later
Agent: [
{
status: 'error',
error: 'Error in sub-node Model',
subRun: [{ node: 'Model', runIndex: 0 }],
},
],
Model: [{ status: 'error', error: 'Invalid API key', subRun: undefined }],
});
expect(orphanReports()).toEqual([]);
});
test('merges the subRun entry onto a tool run when the invocation succeeds', async () => {
const result = await runWorkflow(
[
node('Trigger', 'test.trigger'),
node('Agent', 'test.agent', { toolInvocations: 1 }),
node('Model', 'test.model'),
node('Tool', 'test.toolPlain'),
node('Tool Model', 'test.model'),
],
{
...connect('Trigger', 'Agent'),
...connect('Model', 'Agent', NodeConnectionTypes.AiLanguageModel),
...connect('Tool', 'Agent', NodeConnectionTypes.AiTool),
...connect('Tool Model', 'Tool', NodeConnectionTypes.AiLanguageModel),
},
);
expect(result.status).toBe('success');
expect(result.data.resultData.error).toBeUndefined();
expect(savedExecution(result)).toEqual({
Trigger: [{ status: 'success', error: undefined, subRun: undefined }],
Agent: [{ status: 'success', error: undefined, subRun: undefined }],
// Pins current behaviour, which looks wrong: `addOutputData` passes its own node
// name as the subRun target, so a tool's run lists itself as its own sub-run.
// Unrelated to this fix and invisible today — nothing reads `subRun`.
Tool: [{ status: 'success', error: undefined, subRun: [{ node: 'Tool', runIndex: 0 }] }],
});
expect(orphanReports()).toEqual([]);
});
test('stages the subRun entry at a top-level parent run index above zero', async () => {
const result = await runWorkflow(
[
node('Trigger', 'test.trigger'),
node('Branch', 'test.branch'),
node('Agent', 'test.agent'),
// Succeeds on the agent's first run, fails on its second
node('Model', 'test.model', { failSupplyData: '={{ $runIndex >= 1 }}' }),
],
{
...connect('Trigger', 'Branch'),
Branch: {
main: [
[{ node: 'Agent', type: NodeConnectionTypes.Main, index: 0 }],
[{ node: 'Agent', type: NodeConnectionTypes.Main, index: 0 }],
],
},
...connect('Model', 'Agent', NodeConnectionTypes.AiLanguageModel),
},
);
expect(result.status).toBe('error');
const saved = savedExecution(result);
expect(Object.keys(saved).sort()).toEqual(['Agent', 'Branch', 'Model', 'Trigger']);
expect(saved.Trigger).toEqual([{ status: 'success', error: undefined, subRun: undefined }]);
expect(saved.Branch).toEqual([{ status: 'success', error: undefined, subRun: undefined }]);
// The target follows the parent's real run count — here run 1, not 0. The entry's own
// `runIndex` reuses that same number, which CAT-4268 may change.
expect(saved.Agent).toEqual([
{ status: 'success', error: undefined, subRun: undefined },
{
status: 'error',
error: 'Error in sub-node Model',
subRun: [{ node: 'Model', runIndex: 1 }],
},
]);
expect(runsOf(result, 'Model')).toEqual([{ status: 'error', error: 'Invalid API key' }]);
expect(orphanReports()).toEqual([]);
});
});
describe('sub-node parents (nothing to attach a subRun entry to)', () => {
test('stages nothing when a tool fails while setting up its own sub-node', async () => {
const result = await runWorkflow(
[
node('Trigger', 'test.trigger'),
node('Agent', 'test.agent', { toolInvocations: 1 }),
node('Model', 'test.model'),
node('Tool', 'test.toolWithSetup'),
node('Tool Model', 'test.model', { failSupplyData: true }),
],
{
...connect('Trigger', 'Agent'),
...connect('Model', 'Agent', NodeConnectionTypes.AiLanguageModel),
...connect('Tool', 'Agent', NodeConnectionTypes.AiTool),
...connect('Tool Model', 'Tool', NodeConnectionTypes.AiLanguageModel),
},
);
// The user-facing failure is unchanged: it names the node that actually broke
expect(result.status).toBe('error');
expect(result.data.resultData.error?.message).toBe('Error in sub-node Tool Model');
expect(runsOf(result, 'Tool Model')).toEqual([{ status: 'error', error: 'Invalid API key' }]);
// `Tool` never gets a run of its own, so there must be nothing noted against it
expect(runsOf(result, 'Tool')).toEqual([]);
expect(stagedMetadata(result)).toEqual({});
expect(sourcesOf(result, 'Tool Model')).toEqual([
[{ previousNode: 'Tool', previousNodeRun: 0 }],
]);
expect(orphanReports()).toEqual([]);
});
test('stages nothing when a tool fails during an invocation', async () => {
const result = await runWorkflow(
[
node('Trigger', 'test.trigger'),
node('Agent', 'test.agent', { toolInvocations: 1 }),
node('Model', 'test.model'),
node('Tool', 'test.toolPlain'),
node('Tool Model', 'test.model', { failSupplyData: true }),
],
{
...connect('Trigger', 'Agent'),
...connect('Model', 'Agent', NodeConnectionTypes.AiLanguageModel),
...connect('Tool', 'Agent', NodeConnectionTypes.AiTool),
...connect('Tool Model', 'Tool', NodeConnectionTypes.AiLanguageModel),
},
);
// The agent absorbs the failed tool call, so the execution itself succeeds
expect(result.status).toBe('success');
expect(runsOf(result, 'Tool')).toEqual([
{ status: 'error', error: expect.stringContaining('Error in sub-node Tool Model') },
]);
expect(runsOf(result, 'Tool Model')).toEqual([{ status: 'error', error: 'Invalid API key' }]);
// The tool is mid-run at index 0, so the note the old code filed at index 1 had
// nothing to merge onto and was reported
expect(orphanReports()).toEqual([]);
});
test('stages nothing when the same tool fails on repeated invocations', async () => {
const result = await runWorkflow(
[
node('Trigger', 'test.trigger'),
node('Agent', 'test.agent', { toolInvocations: 2 }),
node('Model', 'test.model'),
node('Tool', 'test.toolPlain'),
node('Tool Model', 'test.model', { failSupplyData: true }),
],
{
...connect('Trigger', 'Agent'),
...connect('Model', 'Agent', NodeConnectionTypes.AiLanguageModel),
...connect('Tool', 'Agent', NodeConnectionTypes.AiTool),
...connect('Tool Model', 'Tool', NodeConnectionTypes.AiLanguageModel),
},
);
expect(result.status).toBe('success');
expect(runsOf(result, 'Tool')).toEqual([
{ status: 'error', error: expect.stringContaining('Error in sub-node Tool Model') },
{ status: 'error', error: expect.stringContaining('Error in sub-node Tool Model') },
]);
expect(runsOf(result, 'Tool Model')).toEqual([
{ status: 'error', error: 'Invalid API key' },
{ status: 'error', error: 'Invalid API key' },
]);
// Before the fix, run 1 also carried the note left by the first invocation, so it
// listed a sub-node failure belonging to a different attempt
expect(crossNodeSubRun(result, 'Tool', 1)).toEqual([]);
expect(orphanReports()).toEqual([]);
});
test('stages nothing when the failure is three levels deep', async () => {
const result = await runWorkflow(
[
node('Trigger', 'test.trigger'),
node('Agent', 'test.agent', { toolInvocations: 1 }),
node('Model', 'test.model'),
node('Store', 'test.toolWithSetup'),
node('Inner Store', 'test.toolWithSetup'),
node('Inner Model', 'test.model', { failSupplyData: true }),
],
{
...connect('Trigger', 'Agent'),
...connect('Model', 'Agent', NodeConnectionTypes.AiLanguageModel),
...connect('Store', 'Agent', NodeConnectionTypes.AiTool),
...connect('Inner Store', 'Store', NodeConnectionTypes.AiTool),
...connect('Inner Model', 'Inner Store', NodeConnectionTypes.AiLanguageModel),
},
);
expect(result.status).toBe('error');
expect(result.data.resultData.error?.message).toBe('Error in sub-node Inner Model');
expect(runsOf(result, 'Inner Model')).toEqual([
{ status: 'error', error: 'Invalid API key' },
]);
// Neither store gets a run, and only the innermost failure writes a note — the
// outer levels rethrow the error before reaching that code
expect(runsOf(result, 'Store')).toEqual([]);
expect(runsOf(result, 'Inner Store')).toEqual([]);
expect(stagedMetadata(result)).toEqual({});
expect(sourcesOf(result, 'Inner Model')).toEqual([
[{ previousNode: 'Inner Store', previousNodeRun: 0 }],
]);
expect(orphanReports()).toEqual([]);
});
test('stages nothing for the failing tool and leaves its sibling intact', async () => {
const result = await runWorkflow(
[
node('Trigger', 'test.trigger'),
node('Agent', 'test.agent', { toolInvocations: 1 }),
node('Model', 'test.model'),
node('Tool', 'test.toolPlain'),
node('Tool Model', 'test.model', { failSupplyData: true }),
node('Other Tool', 'test.toolPlain'),
node('Other Tool Model', 'test.model'),
],
{
...connect('Trigger', 'Agent'),
...connect('Model', 'Agent', NodeConnectionTypes.AiLanguageModel),
...connect('Tool', 'Agent', NodeConnectionTypes.AiTool),
...connect('Other Tool', 'Agent', NodeConnectionTypes.AiTool),
...connect('Tool Model', 'Tool', NodeConnectionTypes.AiLanguageModel),
...connect('Other Tool Model', 'Other Tool', NodeConnectionTypes.AiLanguageModel),
},
);
expect(result.status).toBe('success');
expect(runsOf(result, 'Tool')).toEqual([
{ status: 'error', error: expect.stringContaining('Error in sub-node Tool Model') },
]);
expect(runsOf(result, 'Tool Model')).toEqual([{ status: 'error', error: 'Invalid API key' }]);
// The healthy sibling is untouched by its neighbour's failure
expect(runsOf(result, 'Other Tool')).toEqual([{ status: 'success' }]);
expect(crossNodeSubRun(result, 'Other Tool')).toEqual([]);
expect(orphanReports()).toEqual([]);
});
test('stages nothing when a tool invokes a tool whose model fails', async () => {
const result = await runWorkflow(
[
node('Trigger', 'test.trigger'),
node('Agent', 'test.agent', { toolInvocations: 1 }),
node('Model', 'test.model'),
node('Outer Tool', 'test.toolWithNestedTool'),
node('Inner Tool', 'test.toolPlain'),
node('Inner Model', 'test.model', { failSupplyData: true }),
],
{
...connect('Trigger', 'Agent'),
...connect('Model', 'Agent', NodeConnectionTypes.AiLanguageModel),
...connect('Outer Tool', 'Agent', NodeConnectionTypes.AiTool),
...connect('Inner Tool', 'Outer Tool', NodeConnectionTypes.AiTool),
...connect('Inner Model', 'Inner Tool', NodeConnectionTypes.AiLanguageModel),
},
);
expect(result.status).toBe('success');
// Both tools are invoked, so both have a real run of their own
expect(runsOf(result, 'Outer Tool')).toEqual([
{ status: 'error', error: expect.stringContaining('Error in sub-node Inner Model') },
]);
expect(runsOf(result, 'Inner Tool')).toEqual([
{ status: 'error', error: expect.stringContaining('Error in sub-node Inner Model') },
]);
expect(runsOf(result, 'Inner Model')).toEqual([
{ status: 'error', error: 'Invalid API key' },
]);
expect(sourcesOf(result, 'Inner Model')).toEqual([
[{ previousNode: 'Inner Tool', previousNodeRun: 0 }],
]);
expect(orphanReports()).toEqual([]);
});
});
});
@@ -260,7 +260,8 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData
type: 'input' | 'output',
data: INodeExecutionData[][] | ExecutionBaseError,
connectionType: AINodeConnectionType,
sourceNodeName: string,
/** Node whose run the `subRun` entry attaches to; `undefined` when there is none. */
sourceNodeName: string | undefined,
currentNodeRunIndex: number,
metadata?: ITaskMetadata,
sourceNodeRunIndex?: number,
@@ -361,6 +362,8 @@ export class SupplyDataContext extends BaseExecuteContext implements ISupplyData
this.runExecutionData,
]);
if (sourceNodeName === undefined) return;
if (get(runExecutionData, 'executionData.metadata', undefined) === undefined) {
runExecutionData.executionData!.metadata = {};
}
@@ -540,12 +540,18 @@ export async function getInputConnectionData(
currentNodeRunIndex = runExecutionData.resultData.runData[parentNode.name].length;
}
// The `subRun` entry is merged onto `runData[<name>][currentNodeRunIndex]` at the
// end of the execution, so it has to name a node that gets a record there. A
// top-level parent gets one appended right after this failure; a sub-node parent
// never does.
const subRunTarget = this instanceof SupplyDataContext ? undefined : parentNode.name;
// Display the error on the node which is causing it
await context.addExecutionDataFunctions(
'input',
error,
connectionType,
parentNode.name,
subRunTarget,
currentNodeRunIndex,
);
@@ -553,7 +559,7 @@ export async function getInputConnectionData(
'output',
error,
connectionType,
parentNode.name,
subRunTarget,
currentNodeRunIndex,
);