n8n-io/n8n · error · StaleResumeError

No tool call found for toolCallId: ${options.toolCallId}

Error message

No tool call found for toolCallId: ${options.toolCallId}

What it means

Thrown when an HTTP Request node is configured with contentType='json' but the jsonBody actually looks like XML/SOAP. The validator flags this because sending an XML envelope through the JSON serializer produces a malformed request. Detected via payload starting with <?xml, <soap, <s:envelope, <env:envelope, or an expression referencing a soap/xml body field.

Source

Thrown at packages/@n8n/agents/src/runtime/loop/agent-runtime.ts:365

	async resume(
		method: 'stream',
		data: unknown,
		options: ResumeOptions & ExecutionOptions,
	): Promise<StreamResult>;
	async resume(
		method: 'generate' | 'stream',
		data: unknown,
		options: ResumeOptions & ExecutionOptions,
	): Promise<GenerateResult | StreamResult> {
		this.runId = options.runId;
		const state = await this.runState.resume(this.runId);
		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) {

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Set sendBody=true, contentType='raw', and place the envelope in the body field with rawContentType='text/xml' (or 'application/xml').
  2. Omit specifyBody, jsonBody, and bodyParameters so they do not conflict with the raw body.
  3. If the payload is dynamic, put an expression in body such as expr('{{ $json.soapBody }}') while keeping contentType='raw' and an XML rawContentType.

Example fix

// before
httpRequest({
  name: 'SOAP Call',
  sendBody: true,
  contentType: 'json',
  jsonBody: '<soap:Envelope ...></soap:Envelope>',
});

// after
httpRequest({
  name: 'SOAP Call',
  sendBody: true,
  contentType: 'raw',
  rawContentType: 'text/xml',
  body: '<soap:Envelope ...></soap:Envelope>',
});
Defensive patterns

Strategy: validation

Validate before calling

const XML_START = /^\s*(?:=\s*)?(?:\{{\s*)?['"`]?\s*(?:<\?xml|<soap:?|<s:envelope|<env:envelope)/i;

function looksLikeXml(value: unknown): boolean {
  return typeof value === 'string' && XML_START.test(value);
}

// before building:
if (params.contentType === 'json' && looksLikeXml(params.jsonBody)) {
  throw new Error('XML payload detected; use contentType=raw with rawContentType=text/xml and the body field.');
}

Prevention

When it happens

Trigger: params.contentType === 'json' AND looksLikeXmlPayload(params.jsonBody) is true — i.e. the jsonBody string matches the XML start pattern, or it is an expression (starts with '=' / contains '{{') whose body matches a soap/xml body/payload/envelope reference.

Common situations: Integrating a SOAP/legacy service and leaving the default JSON content type; an AI builder copies an XML envelope into jsonBody; the provider's docs show a SOAP envelope and the model maps it to the JSON body field.

Related errors


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