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 objects

View on GitHub (pinned to e318778c9b)

Solutions

  1. Wrap the value in a ChatMessage, e.g. ChatMessage.from_user(text) instead of a raw string.
  2. If passing a list, ensure every element is a ChatMessage; convert with [ChatMessage.from_user(t) for t in items].
  3. Check the pipeline component output connected to the template variable and verify its type annotation.
  4. 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

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


AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30). Data as JSON: /api/errors/b3efbe1e07ad764c. Report an issue: GitHub.