Budibase/budibase · error · Error

AI message content must be a string

Error message

AI message content must be a string

What it means

toPrompt converts Budibase AI chat messages into provider model messages, building a system instruction list and a modelMessages array. It enforces that every message's content is a plain string and rejects tool-role messages because the prompt pipeline only supports text conversation, not tool-call transcripts. Passing structured (array/parts) content or tool results therefore throws this Error.

Source

Thrown at packages/server/src/sdk/workspace/ai/llm/messages.ts:12

import type { Message } from "@budibase/types"
import type { ModelMessage } from "ai"

export function toPrompt(messages: Message[]): {
  instructions?: string
  messages: ModelMessage[]
} {
  const instructions: string[] = []
  const modelMessages: ModelMessage[] = []
  for (const message of messages) {
    if (typeof message.content !== "string") {
      throw new Error("AI message content must be a string")
    }
    if (message.role === "tool") {
      throw new Error("AI tool messages are not supported")
    }
    if (message.role === "system") {
      instructions.push(message.content)
    } else {
      modelMessages.push({
        role: message.role,
        content: message.content,
      } as ModelMessage)
    }
  }
  return {
    instructions: instructions.join("\n\n") || undefined,
    messages: modelMessages,
  }
}

View on GitHub (pinned to a81a902e9a)

Solutions

  1. Flatten any non-string content into a plain string before calling toPrompt (e.g. join text parts).
  2. Filter or convert role "tool" messages out of the array before calling toPrompt.
  3. Only pass messages you constructed with string content; keep tool transcripts out of the prompt path.
  4. If tool support is needed, use the lower-level LLM API instead of toPrompt.

Example fix

// before
toPrompt(history.messages as ModelMessage[])
// after
const msgs = history.messages
  .filter(m => m.role !== "tool")
  .map(m => ({ ...m, content: typeof m.content === "string" ? m.content : JSON.stringify(m.content) }))
toPrompt(msgs)
Defensive patterns

Strategy: validation

Validate before calling

const isPromptSafe = (msgs) => msgs.every(m => typeof m.content === "string" && m.role !== "tool")
if (!isPromptSafe(messages)) throw new Error("Messages must have string content and no tool role before toPrompt")

Type guard

const isTextMessage = (m: ModelMessage): m is ModelMessage & { content: string } =>
  typeof m.content === "string"

Try / catch

try {
  const prompt = toPrompt(messages)
} catch (e) {
  if (e.message.includes("content must be a string") || e.message.includes("tool messages")) {
    messages = messages.filter(m => m.role !== "tool").map(m => ({ ...m, content: typeof m.content === "string" ? m.content : JSON.stringify(m.content) }))
    return toPrompt(messages)
  }
  throw e
}

Prevention

When it happens

Trigger: Calling toPrompt with a messages array where any element has non-string content (e.g. AI SDK ModelMessage with array-of-parts content), or where any element has role "tool" (tool result messages from a prior tool-calling conversation).

Common situations: Feeding messages captured from a Vercel AI SDK / provider response back into toPrompt — those often carry array content blocks or tool results; persisting and replaying chat history that includes tool-call turns; a schema change in the message type from string content to parts.

Related errors


AI-assisted analysis of Budibase/budibase@a81a902e9a (2026-08-29). Data as JSON: /api/errors/07e4cc59599ca876. Report an issue: GitHub.