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

  1. Provide at least one key: filterableKeys: { category: 'Document category' }.
  2. If you have no filterable metadata, omit filterableKeys entirely — asTool() then builds a simpler query-only tool without the filter input.
  3. 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

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


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/64e81c3991ec5461. Report an issue: GitHub.