Budibase/budibase · error · HTTPError
AI message content must be a string
Error message
AI message content must be a string
What it means
toPrompt converts an internal ModelMessage[] into prompt instructions and only supports string content. If any message's content is an array or other structured form (multimodal/parts), it throws HTTPError 422 because table generation prompts are text-only.
Source
Thrown at packages/pro/src/ai/generators/tableGeneration.ts:180
}
private getErrorMessage(err: unknown): string {
if (!err || typeof err !== "object") {
return String(err)
}
const error = err as Record<string, unknown>
return typeof error.message === "string" ? error.message : String(err)
}
private 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 HTTPError("AI message content must be a string", 422)
}
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
- Flatten message content to a string before calling generation: join text parts (part.text) into one string.
- Strip or convert non-text content (images/tool calls) from the message history.
- Normalize messages at the boundary where they are produced so all content fields are strings.
- Update the generator to handle structured content if multimodal prompts are genuinely needed.
Example fix
// before
const msgs = history.map(m => ({ role: m.role, content: m.content })) // content may be array
// after
const msgs = history.map(m => ({
role: m.role,
content: typeof m.content === "string"
? m.content
: m.content.map(p => p.text).join("\n"),
})) Defensive patterns
Strategy: validation
Validate before calling
const allStrings = messages.every(m => typeof m.content === "string")
if (!allStrings) throw new Error("Flatten multimodal message content to strings first") Type guard
function isTextMessage(m: ModelMessage): m is ModelMessage & { content: string } {
return typeof m.content === "string"
} Try / catch
try {
return await generate(req)
} catch (e) {
if (e.status === 422 && e.message.includes("content must be a string")) {
return generate(req.map(flattenContent))
}
throw e
} Prevention
- Normalize message content to strings where messages are produced
- Strip image/tool-call parts from histories reused across AI features
- Add a shared flattenContent util for multimodal content arrays
When it happens
Trigger: Passing messages whose content is not a plain string (e.g. content arrays like [{type:"text",...}] from multimodal chat history or another AI pipeline) into the table generation flow, reached via the result/generate path.
Common situations: Reusing conversation history built for vision/multimodal models, piping messages from a different LLM SDK that structures content as parts, programmatic API usage constructing ModelMessage objects manually.
Related errors
- We didn't understand your prompt. This can happen if the pro
- LLM not available
- Tool name must be under 64 characters long
- Cannot create a query tool without a query ID
- Unsupported BBAI model: ${model}
AI-assisted analysis of Budibase/budibase@a81a902e9a (2026-08-29).
Data as JSON: /api/errors/aaa8d98d407878ea.
Report an issue: GitHub.