Budibase/budibase · error · Error

AI user message must be a string

Error message

AI user message must be a string

What it means

generateCronExpression takes the prompt from the request body and passes it to ai.generateCronExpression, then locates the resulting user-role message to feed to the LLM via generateText. Because ModelMessage content can be an array of parts, the controller asserts it is a plain string before building the ModelMessage[]; if the produced user message content is not a string (e.g. undefined because no user message exists, or structured content), it throws this Error.

Source

Thrown at packages/server/src/api/controllers/ai/cron.ts:15

import { generateText, type ModelMessage } from "ai"
import { Ctx, GenerateCronRequest, GenerateCronResponse } from "@budibase/types"
import { ai } from "@budibase/pro"
import sdk from "../../../sdk"

export async function generateCronExpression(
  ctx: Ctx<GenerateCronRequest, GenerateCronResponse>
) {
  const { prompt } = ctx.request.body
  const request = ai.generateCronExpression(prompt)
  const userMessage = request.messages.find(
    message => message.role === "user"
  )?.content
  if (typeof userMessage !== "string") {
    throw new Error("AI user message must be a string")
  }

  const messages: ModelMessage[] = [{ role: "user", content: userMessage }]

  const { chat, providerOptions } = await sdk.ai.llm.getDefaultLLMOrThrow()
  const result = await generateText({
    model: chat,
    messages,
    providerOptions: providerOptions?.(false),
  })
  const message = result.text?.trim()

  if (message?.startsWith("Error generating cron:")) {
    ctx.throw(400, message)
  } else {
    ctx.body = { message }
  }
}

View on GitHub (pinned to a81a902e9a)

Solutions

  1. Always send a non-empty string prompt in the request body: { prompt: "run every weekday at 9am" }.
  2. Validate prompt is present and a string client-side before calling the endpoint.
  3. If the prompt was provided and the error still occurs, inspect ai.generateCronExpression's output shape — content may be a content-parts array; flatten/normalize it to a string.
  4. Check for version drift between @budibase/types ModelMessage definitions and the controller's string assumption after upgrading.

Example fix

// before
const { prompt } = ctx.request.body // prompt undefined
const request = ai.generateCronExpression(prompt)
// after (server-side hardening)
if (typeof prompt !== "string" || !prompt.trim()) {
  throw new HTTPError("Prompt is required", 400)
}
const request = ai.generateCronExpression(prompt)
Defensive patterns

Strategy: validation

Validate before calling

if (typeof prompt !== "string" || !prompt.trim()) {
  throw new Error("A non-empty prompt string is required")
}
await api.post("/api/ai/cron", { prompt })

Type guard

const isNonEmptyString = (v: unknown): v is string =>
  typeof v === "string" && v.trim().length > 0

Try / catch

try {
  const res = await api.post("/api/ai/cron", { prompt })
} catch (e) {
  if (String(e.message).includes("AI user message must be a string")) {
    // prompt missing or upstream message shape changed; resend with a string prompt or upgrade server
  }
}

Prevention

When it happens

Trigger: POST to the AI cron endpoint with body.prompt undefined/null/empty such that ai.generateCronExpression produces no string user message, or the returned message content is a non-string (array of content parts) shape.

Common situations: API clients calling the endpoint without a prompt field; automation/webhook callers sending an empty body; upstream ai.generateCronExpression shape changing to multi-part content so the find returns a message whose content is not a string.

Related errors


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