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
- Ensure the rule object passed to `runExpectedToolsInvokedCheck` has at least one of `anyOf`, `noneOf`, `anyOfToolCalls`, `allOfToolCalls`, or `noneOfToolCalls`, each as a non-empty array.
- 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).
- 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
- Always go through the zod schema for JSON-sourced rules so the `.refine` catches empty expectations at load time.
- In test helpers, default to populating at least one list rather than building an all-empty fixture.
- If you weaken the schema, mirror the runtime validator so they can't drift.
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
- Failed to parse discovery test case ${filePath}: ${error ins
- Invalid discovery test case ${filePath}: ${issues}
- Cannot decrease maxIterations when resuming a run. Expected
- Checkpoint for runId ${this.runId} has pending tool calls —
- Model ID is required
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/772c990600ad6867.
Report an issue: GitHub.