n8n-io/n8n · error · Error
filterableKeys must contain at least one key
Error message
filterableKeys must contain at least one key
What it means
buildFilterInputSchema(keys) constructs the model-facing filter tool input from a keys map (key name -> human description for the LLM). It requires at least one key, verified by the isNonEmptyArray type guard on Object.keys(keys). An empty keys object would produce a schema that allows no filterable dimensions, making the filter tool useless and confusing the model — rejected up front.
Source
Thrown at packages/@n8n/agents/src/sdk/vector-store-filter.ts:78
}
// eq, ne
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
throw new Error(
`Filter operator "${operator}" on key "${key}" requires a string, number, or boolean value.`,
);
}
}
function isNonEmptyArray(arr: string[]): arr is [string, ...string[]] {
return arr.length > 0;
}
/** Builds the zod schema for the model-facing `filter` tool input, scoped to `keys` (key -> description). */
export function buildFilterInputSchema(keys: Record<string, string>) {
const keyNames = Object.keys(keys);
if (!isNonEmptyArray(keyNames)) {
throw new Error('filterableKeys must contain at least one key');
}
const keyDescriptions = keyNames.map((key) => `- ${key}: ${keys[key]}`).join('\n');
// Discriminated by `operator` so the model-facing schema rejects the same
// operator/value mismatches the runtime validator does (e.g. an array for
// "eq", or a scalar for "in"), instead of failing later during search.
return z
.array(
z.discriminatedUnion('operator', [
z.object({
key: z.enum(keyNames),
operator: z.enum(SCALAR_OPERATORS),
value: z.union([z.string(), z.number(), z.boolean()]),
}),
z.object({
key: z.enum(keyNames),
operator: z.enum(ARRAY_OPERATORS),View on GitHub (pinned to 5ac6606e81)
Solutions
- Provide at least one key: filterableKeys: { category: 'Document category' }.
- If you have no filterable metadata, omit filterableKeys entirely — asTool() then builds a simpler query-only tool without the filter input.
- When keys come from external data, guard with Object.keys(keys).length > 0 before passing filterableKeys.
Example fix
// before — throws
store.asTool({ filterableKeys: {} });
// after — provide keys
store.asTool({ filterableKeys: { category: 'Document category', source: 'Source system' } });
// or omit filterableKeys entirely
store.asTool(); Defensive patterns
Strategy: validation
Validate before calling
function asToolWithFilters(store: VectorStore, keys: Record<string, string>) {
if (Object.keys(keys).length === 0) {
return store.asTool(); // omit filterableKeys entirely
}
return store.asTool({ filterableKeys: keys });
} Type guard
function hasAtLeastOneKey(keys: Record<string, string>): keys is Record<string, string> {
return Object.keys(keys).length > 0;
} Prevention
- Skip filterableKeys entirely when no metadata dimensions are filterable.
- Derive filterableKeys from a non-empty metadata schema so it can never be empty by construction.
- Guard with Object.keys(keys).length > 0 before passing the option.
When it happens
Trigger: Calling VectorStore.asTool({ filterableKeys: {} }) or directly invoking buildFilterInputSchema({}). The error fires before the zod discriminated-union schema is constructed.
Common situations: Dynamically deriving filterableKeys from metadata that ended up empty; passing an empty object as a placeholder during development; refactoring that clears the keys map; conditionally building keys and the condition produced nothing.
Related errors
- Invalid filter operator "${operator}" for key "${key}". Supp
- VectorStore "${this.name}" requires a description — set it v
- Filter operator "${operator}" on key "${key}" requires a non
- Filter operator "${operator}" on key "${key}" requires array
- Filter operator "${operator}" on key "${key}" requires a str
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/64e81c3991ec5461.
Report an issue: GitHub.