This page explains the internal mechanism that allows AI agent nodes in n8n to pause their execution, invoke connected tool sub-nodes, and resume once the tool results are available. It covers the EngineRequest/EngineResponse protocol, the handleRequest scheduling function, the makeEngineResponse assembler, and the virtual TOOL_EXECUTOR_NODE_NAME node used during partial execution.
For background on the broader workflow execution lifecycle, see Workflow Execution Lifecycle. For partial execution mechanics (DirectedGraph, findSubgraph, cleanRunData), see Partial Execution and Error Handling. For the AI nodes that produce EngineRequest objects (Agent node, LLM nodes), see AI and LangChain Nodes.
Standard n8n nodes consume input and produce output in a single synchronous call. AI agent nodes behave differently: a single agent invocation may require multiple rounds of tool calls before producing a final output. n8n handles this with a pause-and-resume loop inside WorkflowExecute.processRunExecutionData.
When an agent node's execute() method needs to call a tool, it returns an EngineRequest object instead of INodeExecutionData[][]. The execution engine detects this, schedules the named tool nodes, executes them, and then re-invokes the agent's execute() method with an EngineResponse carrying the results. This cycle repeats until the agent returns normal output.
High-level agent tool-call sequence:
Sources: packages/core/src/execution-engine/workflow-execute.ts1-84 packages/core/src/execution-engine/requests-response.ts1-100
The protocol is expressed through three interfaces defined in n8n-workflow:
| Type | Direction | Purpose |
|---|---|---|
EngineRequest | Agent ā Engine | Requests execution of one or more tool nodes |
EngineResponse | Engine ā Agent | Delivers tool execution results back to the agent |
IRunNodeResponse | Any node ā Engine | Normal output path; contains INodeExecutionData[][] |
An EngineRequest carries an actions array. Each entry is an ExecutionNodeAction:
| Field | Type | Description |
|---|---|---|
actionType | 'ExecutionNodeAction' | Discriminant |
nodeName | string | Name of the tool node to execute |
input | IDataObject | Input data to pass to the tool |
type | 'ai_tool' | Connection type used for the input override |
id | string | Unique action identifier (used to match responses) |
metadata | object | Arbitrary agent-side metadata passed through |
An EngineResponse contains:
| Field | Type | Description |
|---|---|---|
actionResponses | ActionResponse[] | One entry per action, in the same order |
metadata | object | Metadata from the original EngineRequest |
Each ActionResponse has action (the original ExecutionNodeAction) and data (the ITaskData from the tool's run).
Sources: packages/core/src/execution-engine/workflow-execute.ts39-44 packages/core/src/execution-engine/requests-response.ts1-30
Code entities involved:
Sources: packages/core/src/execution-engine/workflow-execute.ts1004-1073 packages/core/src/execution-engine/requests-response.ts1-100
isEngineRequestInside executeNode, after calling the node's execute() method, the engine checks whether the return value is an EngineRequest:
packages/core/src/execution-engine/workflow-execute.ts1050-1052
if (isEngineRequest(data)) {
return data;
}
isEngineRequest is a type guard exported from requests-response.ts. If true, executeNode returns the EngineRequest object up to processRunExecutionData instead of the usual IRunNodeResponse.
handleRequesthandleRequest in requests-response.ts receives the EngineRequest and does the following for each action:
runData entry for the tool node via initializeNodeRunData, setting inputOverride with the connection type ai_tool and the action's input payload. The pairedItem includes a sourceOverwrite so lineage tracking works correctly.NodeToBeExecuted array that describes how each tool node should be enqueued.The inputOverride structure added to runData[toolNodeName][runIndex] looks like:
inputOverride: {
ai_tool: [[{
json: <action.input>,
pairedItem: {
input: 0, item: 0,
sourceOverwrite: { previousNode, previousNodeOutput, previousNodeRun }
}
}]]
}
Sources: packages/core/src/execution-engine/requests-response.ts55-100 packages/core/src/execution-engine/__tests__/workflow-execute-process-process-run-execution-data.test.ts483-520
The tool nodes returned by handleRequest are pushed onto nodeExecutionStack. The normal execution loop then processes them: nodeExecuteBefore / execute() / nodeExecuteAfter hooks fire for each tool.
makeEngineResponseOnce all tools from a single EngineRequest have completed, makeEngineResponse in requests-response.ts assembles the EngineResponse by:
actions array from the EngineRequest.runData[toolNodeName] to collect each tool's ITaskData.actionResponses, one entry per action, containing both the original action object and the tool's output data.The engine re-enqueues the agent node and calls executeNode again, this time passing the EngineResponse as the subNodeExecutionResults parameter:
packages/core/src/execution-engine/workflow-execute.ts1041-1043
nodeType instanceof Node
? await nodeType.execute(context, subNodeExecutionResults)
: await nodeType.execute.call(context, subNodeExecutionResults)
The agent uses the tool results to continue reasoning. If it needs more tools, it returns another EngineRequest; otherwise it returns INodeExecutionData[][] and the cycle ends.
Sources: packages/core/src/execution-engine/workflow-execute.ts1004-1073
The agent-tool protocol preserves the invariant that nodeExecuteBefore fires exactly once per node per execution attempt. Specifically, it does not re-fire for the agent when it resumes after tool execution.
Observed hook sequence for trigger ā agent ā tool:
The agent's nodeExecuteAfter fires only when the agent finally produces INodeExecutionData[][].
TOOL_EXECUTOR_NODE_NAME ā The Virtual NodeTOOL_EXECUTOR_NODE_NAME is a constant from @n8n/constants. It represents a virtual node injected dynamically into the workflow graph during partial execution of tool nodes. It is never stored in the workflow definition.
Its purpose is to act as a synthetic destination node when a partial execution targets a tool. Without this node, the partial execution machinery has no place to route the tool's output back to the agent.
The checkReadyForExecution method skips validation for this node specifically, because it has no INode entry in workflow.nodes:
packages/core/src/execution-engine/workflow-execute.ts855-859
if (!node && nodeName === TOOL_EXECUTOR_NODE_NAME) {
// ToolExecutor is added dynamically during test executions and isn't saved in the workflow
continue;
}
Sources: packages/core/src/execution-engine/workflow-execute.ts5 packages/core/src/execution-engine/workflow-execute.ts855-859 packages/core/src/execution-engine/__tests__/workflow-execute.test.ts13
runPartialWorkflow2)When a user runs only part of a workflow and the destination node is a tool (detected via NodeHelpers.isTool), runPartialWorkflow2 applies special graph rewiring before execution:
Graph transformation for tool partial execution:
The code path in runPartialWorkflow2:
packages/core/src/execution-engine/workflow-execute.ts219-231
if (NodeHelpers.isTool(destinationNodeType.description, destination.parameters)) {
graph = rewireGraph(destination, graph, agentRequest);
workflow = graph.toWorkflow({ ...workflow });
const toolExecutorNode = workflow.getNode(TOOL_EXECUTOR_NODE_NAME);
// ...
destination = toolExecutorNode;
destinationNode = { nodeName: toolExecutorNode.name, mode: 'inclusive' };
}
rewireGraph (from partial-execution-utils) modifies the DirectedGraph to:
TOOL_EXECUTOR_NODE_NAME virtual node.ai_tool connection to the virtual node instead of the real agent.agentRequest context to set up the input data.This allows the partial execution to treat the virtual node as the final destination, which is "inclusive" (meaning the tool node itself runs).
Sources: packages/core/src/execution-engine/workflow-execute.ts197-305
requests-response.ts ā Module SummaryAll three protocol functions live in a single module:
packages/core/src/execution-engine/requests-response.ts1-100
| Export | Signature | Role |
|---|---|---|
isEngineRequest | (data: unknown) => data is EngineRequest | Type guard used in executeNode |
handleRequest | (request: EngineRequest, workflow, runData) => NodeToBeExecuted[] | Converts action list to execution stack entries; mutates runData with inputOverride |
makeEngineResponse | (request: EngineRequest, runData) => EngineResponse | Assembles tool results into actionResponses |
Internal helpers used only within this module:
| Function | Purpose |
|---|---|
buildParentOutputData | Constructs INodeExecutionData[][] with pairedItem.sourceOverwrite for lineage |
initializeNodeRunData | Creates the runData slot for a tool node and populates inputOverride |
Sources: packages/core/src/execution-engine/requests-response.ts1-100
An EngineRequest may request multiple tools in parallel within a single round-trip. The actions array can contain more than one entry.
handleRequest processes all of them together and returns a NodeToBeExecuted[] for each. The execution engine enqueues all of them onto the stack before the agent resumes. When makeEngineResponse assembles the EngineResponse, it includes one ActionResponse per action, matched by action.id:
expect(response?.actionResponses).toHaveLength(2);
const action1Response = actionResponses.find((r) => r.action.id === 'action_1');
const action2Response = actionResponses.find((r) => r.action.id === 'action_2');
This is the mechanism that allows an agent to fan out multiple tool calls in one LLM step and receive all results before generating its next response.
Sources: packages/core/src/execution-engine/workflow-execute.ts1-84 packages/core/src/execution-engine/requests-response.ts1-100
Refresh this wiki
This wiki was recently refreshed. Please wait 2 days to refresh again.