{"record":{"id":"c3080f97f1972f81","repo":"Budibase/budibase","slug":"ai-user-message-must-be-a-string","errorCode":null,"errorMessage":"AI user message must be a string","messagePattern":"AI user message must be a string","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/server/src/api/controllers/ai/cron.ts","lineNumber":15,"sourceCode":"import { generateText, type ModelMessage } from \"ai\"\nimport { Ctx, GenerateCronRequest, GenerateCronResponse } from \"@budibase/types\"\nimport { ai } from \"@budibase/pro\"\nimport sdk from \"../../../sdk\"\n\nexport async function generateCronExpression(\n  ctx: Ctx<GenerateCronRequest, GenerateCronResponse>\n) {\n  const { prompt } = ctx.request.body\n  const request = ai.generateCronExpression(prompt)\n  const userMessage = request.messages.find(\n    message => message.role === \"user\"\n  )?.content\n  if (typeof userMessage !== \"string\") {\n    throw new Error(\"AI user message must be a string\")\n  }\n\n  const messages: ModelMessage[] = [{ role: \"user\", content: userMessage }]\n\n  const { chat, providerOptions } = await sdk.ai.llm.getDefaultLLMOrThrow()\n  const result = await generateText({\n    model: chat,\n    messages,\n    providerOptions: providerOptions?.(false),\n  })\n  const message = result.text?.trim()\n\n  if (message?.startsWith(\"Error generating cron:\")) {\n    ctx.throw(400, message)\n  } else {\n    ctx.body = { message }\n  }\n}","sourceCodeStart":1,"sourceCodeEnd":33,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/api/controllers/ai/cron.ts#L1-L33","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Always send a non-empty string prompt in the request body: { prompt: \"run every weekday at 9am\" }.","Validate prompt is present and a string client-side before calling the endpoint.","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.","Check for version drift between @budibase/types ModelMessage definitions and the controller's string assumption after upgrading."],"exampleFix":"// before\nconst { prompt } = ctx.request.body // prompt undefined\nconst request = ai.generateCronExpression(prompt)\n// after (server-side hardening)\nif (typeof prompt !== \"string\" || !prompt.trim()) {\n  throw new HTTPError(\"Prompt is required\", 400)\n}\nconst request = ai.generateCronExpression(prompt)","handlingStrategy":"validation","validationCode":"if (typeof prompt !== \"string\" || !prompt.trim()) {\n  throw new Error(\"A non-empty prompt string is required\")\n}\nawait api.post(\"/api/ai/cron\", { prompt })","typeGuard":"const isNonEmptyString = (v: unknown): v is string =>\n  typeof v === \"string\" && v.trim().length > 0","tryCatchPattern":"try {\n  const res = await api.post(\"/api/ai/cron\", { prompt })\n} catch (e) {\n  if (String(e.message).includes(\"AI user message must be a string\")) {\n    // prompt missing or upstream message shape changed; resend with a string prompt or upgrade server\n  }\n}","preventionTips":["Always send prompt as a non-empty string in the request body","Validate payloads before calling AI endpoints","Pin/align package versions so ModelMessage shapes match between packages","Add a server-side early check that prompt is a string before calling ai.generateCronExpression"],"tags":["validation","ai","llm","internal-error","type-mismatch"],"backgroundTag":"prompt-validation-failed","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}