n8n-io/n8n · error · Error

expectedToolInvocations must specify a non-empty `anyOf`, `n

Error message

expectedToolInvocations must specify a non-empty `anyOf`, `noneOf`, `anyOfToolCalls`, `allOfToolCalls`, or `noneOfToolCalls` list

What it means

Thrown by `validateRule()` in discovery/expected-tools-invoked.ts when an `ExpectedToolInvocations` rule has none of the five list fields populated with at least one entry. The validator runs at check-execution time (inside `runExpectedToolsInvokedCheck`) as a defense-in-depth: the case-file zod schema already enforces this via `.refine`, so reaching this throw means the rule was constructed in code (not loaded from JSON) or the schema was bypassed. Without an expectation, the check would pass vacuously, masking a real regression — hence the explicit fail.

Source

Thrown at packages/@n8n/instance-ai/evaluations/discovery/expected-tools-invoked.ts:63

		.filter((a) => a.role.length > 0)
		.map((a) => `${SPAWN_PREFIX}${a.role}`);
}

function matches(name: string, invokedTools: string[], spawnedAgents: string[]): boolean {
	if (name.startsWith(SPAWN_PREFIX)) {
		return spawnedAgents.includes(name);
	}
	return invokedTools.includes(name);
}

function validateRule(rule: ExpectedToolInvocations): void {
	const hasAnyOf = Array.isArray(rule.anyOf) && rule.anyOf.length > 0;
	const hasNoneOf = Array.isArray(rule.noneOf) && rule.noneOf.length > 0;
	const hasAnyOfToolCalls = Array.isArray(rule.anyOfToolCalls) && rule.anyOfToolCalls.length > 0;
	const hasAllOfToolCalls = Array.isArray(rule.allOfToolCalls) && rule.allOfToolCalls.length > 0;
	const hasNoneOfToolCalls = Array.isArray(rule.noneOfToolCalls) && rule.noneOfToolCalls.length > 0;
	if (!hasAnyOf && !hasNoneOf && !hasAnyOfToolCalls && !hasAllOfToolCalls && !hasNoneOfToolCalls) {
		throw new Error(
			'expectedToolInvocations must specify a non-empty `anyOf`, `noneOf`, `anyOfToolCalls`, `allOfToolCalls`, or `noneOfToolCalls` list',
		);
	}
}

function toolCallMatchesExpectation(
	toolCall: EventOutcome['toolCalls'][number],
	expectation: ForbiddenToolCall,
): boolean {
	if (toolCall.toolName !== expectation.toolName) return false;

	const argsContainAny = expectation.argsContainAny ?? [];
	if (argsContainAny.length === 0) return true;

	const argsText = JSON.stringify(toolCall.args).toLowerCase();
	return argsContainAny.some((term) => argsText.includes(term.toLowerCase()));
}

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Ensure the rule object passed to `runExpectedToolsInvokedCheck` has at least one of `anyOf`, `noneOf`, `anyOfToolCalls`, `allOfToolCalls`, or `noneOfToolCalls`, each as a non-empty array.
  2. If the rule came from a JSON case file, fix the file so `expectedToolInvocations` declares at least one populated list (the zod schema should have caught this — investigate why it didn't).
  3. If writing a test helper, default to populating `anyOf` with the tool you're asserting was invoked.

Example fix

// before
const rule = { anyOf: [], noneOf: [] };
runExpectedToolsInvokedCheck({ ...scenario, expectedToolInvocations: rule }, outcome);
// after
const rule = { anyOf: ['create_credential'] };
runExpectedToolsInvokedCheck({ ...scenario, expectedToolInvocations: rule }, outcome);
Defensive patterns

Strategy: type-guard

Validate before calling

function hasNonEmptyExpectation(rule: ExpectedToolInvocations): boolean {
  return (
    (Array.isArray(rule.anyOf) && rule.anyOf.length > 0) ||
    (Array.isArray(rule.noneOf) && rule.noneOf.length > 0) ||
    (Array.isArray(rule.anyOfToolCalls) && rule.anyOfToolCalls.length > 0) ||
    (Array.isArray(rule.allOfToolCalls) && rule.allOfToolCalls.length > 0) ||
    (Array.isArray(rule.noneOfToolCalls) && rule.noneOfToolCalls.length > 0)
  );
}
if (!hasNonEmptyExpectation(rule)) throw new Error('rule has no expectation');

Type guard

function isValidExpectationRule(rule: unknown): rule is ExpectedToolInvocations {
  if (typeof rule !== 'object' || rule === null) return false;
  const r = rule as Record<string, unknown>;
  return ['anyOf','noneOf','anyOfToolCalls','allOfToolCalls','noneOfToolCalls']
    .some((k) => Array.isArray(r[k]) && (r[k] as unknown[]).length > 0);
}

Prevention

When it happens

Trigger: A test constructs an `ExpectedToolInvocations` object programmatically with all lists undefined/empty. A migration/refactor produced a rule object missing its keys. A future code path calls `runExpectedToolsInvokedCheck` with a hand-built rule and forgets to populate any list. The schema `.refine` was weakened or removed, letting an empty rule through.

Common situations: Unit tests building minimal fixtures. A helper that builds expectations conditionally and all conditions were false. Drift between the runtime validator and the zod schema after a refactor.

Related errors


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