n8n-io/n8n · error · Error

${toolName} was registered without a runSubAgent callback, a

Error message

${toolName} was registered without a runSubAgent callback, and no host runner was provided. Register it on an Agent (for inline delegation) or pass runSubAgent.

What it means

Thrown at runtime inside the delegate tool's handler when options.runSubAgent is falsy. The createDelegateSubAgentTool function creates a tool that needs a runner to actually execute the child agent. When registered on an Agent (via the Agent's delegate method), the Agent's build process injects runSubAgent automatically. If the tool is used standalone without that injection, the handler has no way to execute the child and fails.

Source

Thrown at packages/@n8n/agents/src/runtime/tools/delegate-sub-agent-tool.ts:667

			if (output.status === 'suspended') {
				return await cascadeChildSuspension(ctx, request, output);
			}
			return output;
		}

		const childPathIndexKey = getChildPathIndexKey(ctx);
		const childPathIndex = childPathIndexes.get(childPathIndexKey) ?? 0;

		taskPath = createChildSubAgentTaskPath(input.taskName, childPathIndex);
		childPathIndexes.set(childPathIndexKey, childPathIndex + 1);

		request = createDelegateSubAgentRequest(input, ctx, taskPath, childPathIndex, options.policy);

		startedAt = Date.now();
		emitSubAgentStarted(ctx, request, startedAt);
		const toolName = options.name ?? DELEGATE_SUB_AGENT_TOOL_NAME;
		if (!options.runSubAgent) {
			throw new Error(
				`${toolName} was registered without a runSubAgent callback, and no host runner was provided. Register it on an Agent (for inline delegation) or pass runSubAgent.`,
			);
		}
		const output = await options.runSubAgent(
			request,
			createRunnerHelpers(ctx, request, options.name),
		);
		emitSubAgentCompleted(ctx, request, output, startedAt);
		if (
			output.status === 'suspended' &&
			options.resumeSubAgent !== undefined &&
			isInterruptibleToolContext(ctx)
		) {
			return await cascadeChildSuspension(ctx, request, output);
		}
		return output;
	} catch (error) {
		// When the parent has a signal it is the authority: `isAbortError` also

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Register the delegate tool through the Agent's .delegate() method instead of createDelegateSubAgentTool + .tools().
  2. If using createDelegateSubAgentTool directly, pass a runSubAgent callback that invokes your host's child-agent runner.
  3. Ensure the Agent's build() method runs before the tool is invoked (the build injects runSubAgent).

Example fix

// before
const tool = createDelegateSubAgentTool({});
agent.tools([tool]); // runSubAgent never injected
// after
agent.delegate(); // Agent build injects runSubAgent automatically
Defensive patterns

Strategy: validation

Validate before calling

// Ensure the delegate tool is registered through the Agent, not standalone:
if (!options.runSubAgent && !registeredViaAgent) {
  throw new Error('Delegate tool needs runSubAgent or Agent registration');
}

Prevention

When it happens

Trigger: Creating a delegate tool with createDelegateSubAgentTool() and adding it to an agent via a path that bypasses the Agent's built-in delegate wiring (e.g. constructing the tool manually and pushing it into .tools()). Or passing the tool to a custom runtime that does not set runSubAgent.

Common situations: Building a custom agent class or runtime that does not extend the Agent's delegate registration. Extracting the tool for testing without mocking runSubAgent. Migrating from a manual tool registration pattern to the Agent builder without removing the old manual registration.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/0aefce3726ad9ed4. Report an issue: GitHub.