n8n-io/n8n · error · Error

Message content must be a string or contain text content

Error message

Message content must be a string or contain text content

What it means

`extractMessageContent` accepts content as either a plain string or an array of typed parts (e.g. `[{type:'text', text:'...'}]`). If content is some other shape (a number, an object, an array with no `type:'text'` items), the function cannot extract text and throws. The text-extraction path also strips context tags via `cleanContextTags`.

Source

Thrown at packages/@n8n/ai-workflow-builder.ee/evaluations/langsmith/types.ts:83

	if (typeof content === 'string') {
		return cleanContextTags(content);
	}

	if (Array.isArray(content)) {
		// Extract text from complex content
		const textContent = content
			.filter((item) => item?.type === 'text')
			.map((item) => (item as { text: string }).text)
			.map(cleanContextTags)
			.join('\n');

		if (textContent) {
			return textContent;
		}
	}

	throw new Error('Message content must be a string or contain text content');
}

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Ensure each message has at least one `{type:'text', text:'...'}` part (or a top-level string `content`).
  2. If your messages legitimately have no text, skip them at the dataset/call site rather than passing them to `extractMessageContent`.
  3. If the SDK changed the content shape, update `extractMessageContent` to read the new field (the function already tolerates `message.kwargs?.content`).

Example fix

// before
// message.content = [{ type: 'image_url', image_url: { url: '...' } }]  (no text part)
const text = extractMessageContent(message);
// after
// message.content = [{ type: 'text', text: 'describe this' }, { type: 'image_url', image_url: { url: '...' } }]
const text = extractMessageContent(message);
Defensive patterns

Strategy: type-guard

Validate before calling

function hasTextContent(message: BaseMessage): boolean {
  const c = (message as { content?: unknown }).content;
  if (typeof c === 'string') return c.length > 0;
  if (Array.isArray(c)) return c.some((p) => p?.type === 'text' && typeof p.text === 'string');
  return false;
}
if (!hasTextContent(message)) throw new Error('message has no text part to extract');
const text = extractMessageContent(message);

Type guard

function isTextualMessage(message: unknown): message is BaseMessage {
  if (!message || typeof message !== 'object') return false;
  const c = (message as { content?: unknown }).content;
  if (typeof c === 'string') return true;
  if (Array.isArray(c)) return c.some((p) => p?.type === 'text');
  return false;
}

Prevention

When it happens

Trigger: A message whose `content` is a non-string scalar, an object literal, or an array containing only non-text parts (e.g. only `{type:'image_url'}`). Also: a LangSmith SDK version that changed the content shape, or a custom message class whose `content` is structured differently.

Common situations: Multimodal examples that include only image/tool parts and no text; legacy message format where text lives under a different key; SDK upgrade that wraps content in an extra layer; malformed dataset upload.

Related errors


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