Mintplex-Labs/anything-llm · warning
Tools must be an array
Error message
Tools must be an array
What it means
Validation error from POST /api/scheduled-jobs in AnythingLLM's scheduled-jobs API. The optional `tools` field must be a JSON array of agent tool identifiers (or omitted/null); the guard fires only when the value is truthy, has length > 0, and is not an Array. In practice a non-empty string was sent instead of an array, because strings have a .length property and pass the first half of the check.
Source
Thrown at server/endpoints/scheduledJobs.js:160
[validatedRequest, isSingleUserMode],
async (request, response) => {
try {
const { name, prompt, tools, schedule } = reqBody(request);
let errorMessage = null;
if (!name?.trim()) {
errorMessage = "Name is required";
} else if (!prompt?.trim()) {
errorMessage = "Prompt is required";
} else if (!schedule?.trim()) {
errorMessage = "Schedule is required";
} else if (!ScheduledJob.isValidCron(schedule)) {
errorMessage = "Invalid cron expression";
} else if (tools?.length > 0 && !Array.isArray(tools)) {
errorMessage = "Tools must be an array";
}
if (errorMessage)
return response.status(400).json({
job: null,
error: errorMessage,
});
// New jobs default to enabled, so creating one always counts as an
// activation. Reject if it would push us past the configured cap.
const activation = await ScheduledJob.canActivate();
if (!activation.allowed) {
return response.status(400).json({
job: null,
error: `Cannot create: maximum of ${activation.limit} active scheduled jobs reached. Disable another job first.`,
});
}
const { job, error } = await ScheduledJob.create({
name: name.trim(),
prompt: prompt.trim(),
tools: tools || null,View on GitHub (pinned to 3aec848f28)
Solutions
- Send tools as a JSON array of strings, e.g. "tools": ["rag-search"], or omit the field entirely when no tools are needed
- Fix the client serializer so single-element arrays stay arrays (proper JSON body, FormData.getAll instead of .get)
- When building from free-form input, split CSV into an array before sending: tools.trim().split(',').map(t => t.trim()).filter(Boolean)
Example fix
// before
await fetch('/api/scheduled-jobs', {
method: 'POST',
body: JSON.stringify({ name, prompt, schedule, tools: 'rag-search' })
});
// after
await fetch('/api/scheduled-jobs', {
method: 'POST',
body: JSON.stringify({ name, prompt, schedule, tools: ['rag-search'] })
}); Defensive patterns
Strategy: validation
Validate before calling
// Before POST /api/scheduled-jobs
const tools = rawTools == null ? null : rawTools;
if (tools !== null && !Array.isArray(tools)) {
throw new Error(`tools must be an array of strings, got ${typeof tools}`);
}
await fetch('/api/scheduled-jobs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, prompt, schedule, tools })
}); Type guard
function isToolList(v) {
return v == null || (Array.isArray(v) && v.every(t => typeof t === 'string' && t.length > 0));
} Prevention
- Always JSON.stringify the request body (never send form-encoded values for tools)
- Normalize CSV or single tool names to arrays at the API boundary of your client
- Add a client-side unit test asserting tools is an array for every create path
When it happens
Trigger: POST /api/scheduled-jobs with body {"name":"Daily report","prompt":"...","schedule":"0 9 * * *","tools":"rag-search"} — tools as a plain string instead of ["rag-search"]. Also triggered by comma-separated values such as "rag-search,web-browse" from a form input or custom client.
Common situations: HTML form / query-string serialization collapsing a single-element array to a scalar; scripts written against an older free-form tools field; passing a variable that is conditionally a string; copy-pasting one tool name without array brackets.
Related errors
- Invalid path name
- Name and config are required
- Query parameter cannot be empty.
- Message is empty
- Must be a boolean
AI-assisted analysis of Mintplex-Labs/anything-llm@3aec848f28 (2026-08-18).
Data as JSON: /api/errors/f33d7c649806cafd.
Report an issue: GitHub.