n8n-io/n8n · error · Error
Tool ${toolCall.toolName} not found
Error message
Tool ${toolCall.toolName} not found What it means
Thrown when a node is correctly configured for a raw XML/SOAP body (sendBody=true, contentType='raw', rawContentType matching an XML media type) but the body field itself is empty or not a string. Without a body the request would be sent empty, so the validator blocks it.
Source
Thrown at packages/@n8n/agents/src/runtime/loop/agent-runtime.ts:377
if (!state) {
throw new StaleResumeError(`No suspended run found for runId: ${this.runId}`);
}
const toolCall = state.pendingToolCalls[options.toolCallId];
if (!toolCall) {
throw new StaleResumeError(`No tool call found for toolCallId: ${options.toolCallId}`);
}
const list = AgentMessageList.deserialize(state.messageList);
this.context.hydrateDeferredToolsFromList(list);
await hydrateFileParts(list.messages(), this.config.fileStore, {
threadId: state.persistence?.threadId,
});
const tool = this.context
.getCurrentTools(state.persistence)
.find((t) => t.name === toolCall.toolName);
if (!tool) throw new Error(`Tool ${toolCall.toolName} not found`);
let resumeData: unknown = data;
let abortScope: AgentAbortScope | undefined;
const resumeSchema = toolCall.suspended ? toolCall.resumeSchema : tool.resumeSchema;
if (!isCancellation(resumeData) && resumeSchema) {
const parseResult = await parseWithSchema(resumeSchema, data, { stripUnknown: true });
if (!parseResult.success) {
throw new Error(`Invalid resume payload: ${parseResult.error}`);
}
resumeData = parseResult.data as JSONValue;
}
try {
// Merge persisted execution options with fresh caller options
const {
runId: _rid,
toolCallId: _tcid,View on GitHub (pinned to 5ac6606e81)
Solutions
- Put the XML envelope string (or an expression like expr('{{ $json.soapBody }}')) into the body field.
- Confirm rawContentType stays an XML media type and contentType stays 'raw'.
- Remove any specifyBody/jsonBody/bodyParameters so only the raw body is used.
Example fix
// before
httpRequest({
name: 'SOAP Call',
sendBody: true,
contentType: 'raw',
rawContentType: 'text/xml',
body: '',
});
// after
httpRequest({
name: 'SOAP Call',
sendBody: true,
contentType: 'raw',
rawContentType: 'text/xml',
body: expr('{{ $json.soapEnvelope }}'),
}); Defensive patterns
Strategy: validation
Validate before calling
const XML_MEDIA = /\b(?:text|application)\/(?:[\w.+-]*\+)?xml\b/i;
function isMissingBody(value: unknown): boolean {
return typeof value !== 'string' || value.trim() === '';
}
// before building:
if (params.sendBody === true && params.contentType === 'raw' && XML_MEDIA.test(String(params.rawContentType ?? '')) && isMissingBody(params.body)) {
throw new Error('Raw XML body configured but body is empty; set body to the envelope or an expression.');
} Prevention
- Always pair a rawContentType with a non-empty body field.
- Use an expression (expr('={{ $json.soapBody }}')) when the envelope comes from upstream data.
- Unit-test SOAP nodes with a sample envelope to catch empty-body regressions.
When it happens
Trigger: params.sendBody === true AND params.contentType === 'raw' AND rawContentType matches the XML media pattern (text/xml, application/xml, application/...+xml) AND isMissingBody(params.body) — i.e. body is not a string or is whitespace-only.
Common situations: Configuring the raw content type but forgetting to fill body; referencing a field that resolves to empty at build time; an AI builder sets up the envelope scaffolding but leaves body blank.
Related errors
- No tool call found for toolCallId: ${options.toolCallId}
- LangSmithTelemetry creates its own tracer — do not use .otlp
- No suspended run found for runId: ${this.runId}
- Invalid resume payload: ${parseResult.error}
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/381a63d2ce627ec4.
Report an issue: GitHub.