deepset-ai/haystack · error · ValueError
The '{% insert %}' expression must evaluate to a ChatMessage
Error message
The '{% insert %}' expression must evaluate to a ChatMessage or a list of ChatMessage objects. Got: {type(messages).__name__}. What it means
In Haystack's Jinja2 chat template extension, the `{% insert %}` expression must resolve to a ChatMessage or a list/tuple of ChatMessage objects. This error means the expression evaluated to some other type (e.g. str, dict, None after the empty check), so the extension cannot serialize it into message JSON for the rendered prompt.
Source
Thrown at haystack/utils/jinja2_chat_extension.py:278
as `_build_chat_message_json`, so the messages are parsed back into ChatMessage objects by the
ChatPromptBuilder alongside any literal `{% message %}` blocks. The full `ChatMessage.to_dict()` payload is
serialized so that all content types (tool calls, tool call results, images, reasoning, name and meta) round
trip without loss.
:param messages: The value the `{% insert %}` expression evaluated to. A missing or empty value expands to
nothing. A single ChatMessage is also accepted, since indexing with an integer (for example
`{% insert messages[-1] %}`) yields one message rather than a list. The value is validated at render time
because it comes from untrusted template input.
:param caller: Callable that returns the (empty) rendered body. Unused.
:return: Newline-terminated JSON lines, one per message, or an empty string if there are no messages.
:raises ValueError: If the value is not a ChatMessage or a list of ChatMessage objects.
"""
if isinstance(messages, ChatMessage):
messages = [messages]
if not messages:
return ""
if not isinstance(messages, (list, tuple)) or not all(isinstance(m, ChatMessage) for m in messages):
raise ValueError(
"The '{% insert %}' expression must evaluate to a ChatMessage or a list of ChatMessage objects. "
f"Got: {type(messages).__name__}."
)
return "".join(json.dumps(message.to_dict()) + "\n" for message in messages)
@staticmethod
def _parse_content_parts(content: str, start_tag: str, end_tag: str) -> list[ChatMessageContentT]:
"""
Parse a string into a sequence of ChatMessageContentT objects.
This method handles:
- Plain text content, converted to TextContent objects
- Structured content parts wrapped in sentinel tags, converted to ChatMessageContentT objects
:param content: Input string containing mixed text and content parts
:param start_tag: The opening sentinel tag (including the nonce)
:param end_tag: The closing sentinel tag (including the nonce)
:return: A list of ChatMessageContentT objectsView on GitHub (pinned to e318778c9b)
Solutions
- Wrap the value in a ChatMessage, e.g. ChatMessage.from_user(text) instead of a raw string.
- If passing a list, ensure every element is a ChatMessage; convert with [ChatMessage.from_user(t) for t in items].
- Check the pipeline component output connected to the template variable and verify its type annotation.
- If the intent is plain text insertion, use normal Jinja2 interpolation {{ var }} instead of {% insert %}.
Example fix
// before
messages = "Hello world"
{% insert %}{{ messages }}{% endinsert %}
// after
messages = [ChatMessage.from_user("Hello world")]
{% insert %}{{ messages }}{% endinsert %} Defensive patterns
Strategy: type-guard
Validate before calling
from haystack.dataclasses import ChatMessage
def validate_insert_value(v):
ok = isinstance(v, ChatMessage) or (isinstance(v, (list, tuple)) and all(isinstance(m, ChatMessage) for m in v))
if not ok:
raise TypeError(f"{% insert %} value must be ChatMessage or list of ChatMessages, got {type(v).__name__}") Type guard
def is_insertable(v) -> bool:
return isinstance(v, ChatMessage) or (isinstance(v, (list, tuple)) and all(isinstance(m, ChatMessage) for m in v)) Try / catch
try:
rendered = renderer.run(template=tpl, variables=vars)
except ValueError as e:
if "must evaluate to a ChatMessage" in str(e):
log.error("bad {% insert %} input: %s", vars)
raise Prevention
- Type-annotate pipeline variables feeding templates as ChatMessage or list[ChatMessage].
- Convert raw strings to ChatMessage.from_user before passing them to templates.
- Use {{ var }} for plain-text interpolation instead of {% insert %}.
- Add unit tests rendering every chat template with representative variables.
When it happens
Trigger: Using `{% insert %}` in a chat template with a template variable or expression that yields a plain string, dict, or other non-ChatMessage object instead of a ChatMessage or list of ChatMessages.
Common situations: Passing a raw string (e.g. a document's text) instead of a ChatMessage; assigning a list of strings; a pipeline variable typed wrongly and fed into the template; forgetting that the empty case is handled before this check.
Related errors
- Message content in template is empty or contains only whites
- Found unclosed <haystack_content_part> tag at position {tag_
- PythonCodeSplitter only works with text documents but conten
- Invalid template for condition: {condition_value!r} (type: {
- Invalid template for condition: {condition_value}
AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30).
Data as JSON: /api/errors/b3efbe1e07ad764c.
Report an issue: GitHub.