Skip to content

refactor(core): Split WorkflowExecute.runNode into smaller methods #17864

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: master
Choose a base branch
from

Conversation

despairblue
Copy link
Contributor

@despairblue despairblue commented Jul 31, 2025

Summary

Review commit by commit.

This splits WorkflowExecute.runNode. This PR should not change any behaviour.

This allows removing the eslint warning about cyclomatic complexity as well as making it clear which part of that method needs access to what bindings.
It also makes the method more readable and descriptive.

This is done to prepare the work of moving AI tool executions into the engine.

Note:
The additional tests were created by claude code, but I checked that they work via code coverage reports and mutation testing (e.g. willfully breaking the implementation to check if the test works)
The tests mock way to much and are mainly here to prove that moving the code around didn't change any logic.
With further refactors the tests will gradually be removed and replaced with handwritten tests that document that intention of the behaviour as well.

Related Linear tickets, Github issues, and Community forum posts

Review / Merge checklist

  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with release/backport (if the PR is an urgent fix that needs to be backported)

@despairblue despairblue force-pushed the refactor-run-node branch 2 times, most recently from 7b3dbf4 to 4c576f0 Compare July 31, 2025 13:33
@n8n-assistant n8n-assistant bot added core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team labels Jul 31, 2025
@despairblue despairblue requested review from ivov and schrothbn July 31, 2025 14:13
@despairblue despairblue marked this pull request as ready for review July 31, 2025 14:13
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cubic analysis

1 issue found across 3 files • Review in cubic

React with 👍 or 👎 to teach cubic. You can also tag @cubic-dev-ai to give feedback, ask questions, or re-run the review.

break;
}
// We always use the data of main input and the first input for execute
let connectionInputData = inputData.main[0] as INodeExecutionData[];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: Prefer Typeguards over Type casting

Avoid using as for type narrowing – replace the cast with a proper type annotation.
Rule "Prefer Typeguards over Type casting" forbids as in this context since it is not a DOM/event/const/generic-constraint assertion.

Prompt for AI agents
Address the following comment on packages/core/src/execution-engine/workflow-execute.ts at line 1129:

<comment>Avoid using `as` for type narrowing – replace the cast with a proper type annotation.
Rule &quot;Prefer Typeguards over Type casting&quot; forbids `as` in this context since it is not a DOM/event/const/generic-constraint assertion.</comment>

<file context>
@@ -1089,72 +1089,68 @@ export class WorkflowExecute {
 		return customOperation;
 	}
 
-	/** Executes the given node */
-	// eslint-disable-next-line complexity
-	async runNode(
-		workflow: Workflow,
-		executionData: IExecuteData,
-		runExecutionData: IRunExecutionData,
</file context>
Suggested change
let connectionInputData = inputData.main[0] as INodeExecutionData[];
let connectionInputData: INodeExecutionData[] = inputData.main[0];

Copy link
Contributor

@schrothbn schrothbn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me as a first refactor, a great improvement to understand what's happening. Also besides the heavy mocking I think the additional test coverage is quite useful.
I'll let Ivan do the final approval so we have another pair of eyes on it.

Copy link
Member

@ivov ivov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick pass, more thorough look with fresh eyes tomorrow.

break;
}
// We always use the data of main input and the first input for execute
let connectionInputData = inputData.main[0] as INodeExecutionData[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do inputData.main.at(0), check it for the empty case, and so allow the typechecker to recognize that connectionInputData is INodeExecutionData[] without overriding TS.

Comment on lines +1116 to +1118
!nodeType.execute &&
!customOperation &&
(nodeType.poll || nodeType.trigger || nodeType.webhook)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this condition a bit easier to read beforehand.

Curious why we need to check that a node type does not have an execute method and does not have a custom node operation in addition to checking that it has a method that identifies it as a trigger, poller or webhook? I'd expect the latter should be enough?


let connectionInputData: INodeExecutionData[] = [];
/**
* Prepares input data for node execution based on node type and workflow settings
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This docline could use some love. What does it mean to "prepare" input data? The args mentioned are already clear from the signature. Let's either improve or remove?

// We always use the data of main input and the first input for execute
let connectionInputData = inputData.main[0] as INodeExecutionData[];

const forceInputNodeExecution = workflow.settings.executionOrder !== 'v1';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const forceInputNodeExecution = workflow.settings.executionOrder !== 'v1';
const legacyExecutionOrder = workflow.settings.executionOrder !== 'v1';

Comment on lines +1143 to +1144
if (connectionInputData.length === 0) {
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing context so this might be intended, but I notice the return value differs?

Comment on lines +1360 to +1362
/**
* Executes a declarative node using RoutingNode
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This docline doesn't add much.

/**
* Executes a declarative node using RoutingNode
*/
private async executeDeclarativeNode(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private async executeDeclarativeNode(
private async executeDeclarativeNodeInTest(

* @param abortSignal - Signal for canceling execution
* @returns Promise resolving to node execution result
*/
async runNode(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a runNode (which orchestrates) and an executeNode (which actually executes). I find the above docline mostly redundant of the code (does it add much?), but it'd be a good opportunity to document this so folks can clearly differentiate between them. Or maybe finding better names that make this clear outright.


const response = await triggerResponse.manualTriggerResponse!;
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
const isDeclarativeNode = nodeType.description.requestDefaults !== undefined;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can move this after the connectionInputData check.

executionData,
abortSignal,
);
} else if (nodeType.poll) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a minimal readability gain in this nightmare file, since all these cases return, can we remove the unnecessary elses and spread out these cases?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core Enhancement outside /nodes-base and /editor-ui n8n team Authored by the n8n team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants