
Evals Write Spec
- 3 installs
- 21.2k repo stars
- Updated August 5, 2026
- elastic/kibana
evals-write-spec skill documents Write LLM evaluation spec files with datasets, tasks, and evaluators using the @kbn/evals Playwright fixture.
About
evals-write-spec skill documents Write LLM evaluation spec files with datasets, tasks, and evaluators using the @kbn/evals Playwright fixture. Use when authoring new eval specs, adding datasets or evaluators, or debugging evaluation test failures.. name: evals-write-spec disable-model-invocation: true
- Write LLM evaluation spec files with datasets, tasks, and evaluators using the @kbn/evals Playwright fixture.
- Platform-specific setup patterns for evals-write-spec.
- Evidence-backed steps from upstream SKILL.md.
- When-to-use criteria for evals-write-spec versus alternatives.
Evals Write Spec by the numbers
- 3 all-time installs (skills.sh)
- Ranked #1,759 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
evals-write-spec capabilities & compatibility
- Capabilities
- evals write spec quick start · evals write spec when to use guidance · evals write spec integration patterns
- Works with
- elasticsearch
- Use cases
- security audit
What evals-write-spec says it does
disable-model-invocation: true
Eval specs use the `evaluate` Playwright fixture (not `test`). A spec file follows this structure:
npx skills add https://github.com/elastic/kibana --skill evals-write-specAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 21.2k |
| Last updated | August 5, 2026 |
| Repository | elastic/kibana ↗ |
How do I use evals-write-spec correctly?
Write LLM evaluation spec files with datasets, tasks, and evaluators using the @kbn/evals Playwright fixture. Use when authoring new eval specs, adding datasets or evaluators, or debugging evaluation
Who is it for?
Teams implementing evals-write-spec workflows from the catalog.
Skip if: Skip when requirements clearly match a different specialized stack.
When should I use this skill?
User asks about evals-write-spec, write llm evaluation spec files with datasets, tasks, and evaluators using the @kbn/evals .
What you get
Working evals-write-spec setup with validated configuration and next steps.
Files
Write Eval Specs
Spec File Anatomy
Eval specs use the evaluate Playwright fixture (not test). A spec file follows this structure:
import { evaluate, tags, selectEvaluators, type Example, type TaskOutput } from '@kbn/evals';
evaluate.describe('Suite name', { tag: tags.serverless.observability.complete }, () => {
evaluate.beforeAll(async ({ fetch, log }) => {
// one-time setup: install docs, create agents, load archives
});
evaluate.afterAll(async ({ fetch, log }) => {
// teardown: uninstall docs, delete agents, unload archives
});
evaluate('test name', async ({ executorClient, connector }) => {
await executorClient.runExperiment(
{ dataset, task },
evaluators
);
});
});When a suite has a custom src/evaluate.ts, import from there instead of @kbn/evals:
import { evaluate } from '../src/evaluate';Tags
Every evaluate.describe must have a tag. Common choices:
| Tag | When to use |
|---|---|
tags.serverless.observability.complete | Observability domain evals |
tags.serverless.security.complete | Security domain evals |
tags.serverless.search | Search domain evals |
tags.stateful.classic | Stateful-only evals |
Import tags from @kbn/scout or @kbn/evals (re-exported).
Datasets
A dataset is an array of examples with typed input, output (expected), and optional metadata:
type MyExample = Example<
{ question: string },
{ expectedAnswer: string },
{ tags?: string[] }
>;
const dataset = {
name: 'my-dataset',
description: 'What this dataset tests',
examples: [
{
input: { question: 'What is 2+2?' },
output: { expectedAnswer: '4' },
metadata: { tags: ['math'] },
},
],
};Keep datasets focused. For local iteration, use --grep to run a subset:
node scripts/evals start --grep "my test name"Tasks
The task function receives an example and returns the output to evaluate:
task: async ({ input }) => {
const result = await someKibanaApi(input.question);
return { answer: result.content };
}Tasks can use any fixture available in the evaluate callback: fetch, inferenceClient, connector, esClient, kbnClient, or custom fixtures like chatClient.
Evaluators
There are two ways to provide evaluators to runExperiment:
1. Inline array -- pass evaluator objects directly (simple suites) 2. `selectEvaluators` -- typed wrapper that enforces Example/TaskOutput generics
CODE Evaluators
Deterministic, no LLM call. Use for binary checks:
{
name: 'NonEmpty',
kind: 'CODE',
evaluate: async ({ output }) => ({
score: output?.documents?.length > 0 ? 1 : 0,
}),
}LLM-as-Judge Criteria
Use evaluators.criteria(criteriaArray) for subjective quality checks. The judge LLM scores each criterion:
evaluators.criteria([
'The response correctly identifies the top users.',
'The response includes risk scores.',
]).evaluate({ input, output, expected, metadata })Correctness Analysis
Compares output against expected answer:
evaluators.correctnessAnalysis().evaluate({ input, output, expected, metadata })Groundedness Analysis
Checks if output is grounded in provided context:
evaluators.groundednessAnalysis().evaluate({ input, output, expected, metadata })Trace-Based Evaluators
Available from evaluators.traceBasedEvaluators:
inputTokens,outputTokens,cachedTokens-- token usagetoolCalls-- number of tool callslatency-- span latency in seconds
These read from the tracing ES cluster and require EDOT to be running.
RAG Evaluators
For retrieval-augmented generation with ground truth:
import { createPrecisionAtKEvaluator, createRecallAtKEvaluator, createF1AtKEvaluator } from '@kbn/evals';See evaluator-patterns.md for full examples.
Available Fixtures
| Fixture | Scope | Description |
|---|---|---|
executorClient | worker | Runs experiments, exports scores to ES |
inferenceClient | worker | Inference REST client bound to connector |
connector | worker | The model connector being evaluated |
evaluationConnector | worker | The judge connector |
evaluators | worker | DefaultEvaluators (criteria, correctness, groundedness, trace-based) |
fetch | worker | HttpHandler for Kibana API calls |
esClient | worker | Elasticsearch client (Scout cluster) |
kbnClient | worker | Kibana client with retries |
traceEsClient | worker | ES client for trace queries |
evaluationsEsClient | worker | ES client for evaluation score storage |
log | worker | ToolingLog for structured logging |
repetitions | worker | Number of experiment repetitions |
config | worker | Scout server config (hosts, auth) |
The evaluateDataset Pattern
For suites with many specs that share the same task + evaluator wiring, extract a reusable helper:
`src/evaluate_dataset.ts`:
import type { DefaultEvaluators, EvalsExecutorClient } from '@kbn/evals';
import type { MyChatClient } from './chat_client';
export type EvaluateDataset = (opts: {
dataset: { name: string; description: string; examples: MyExample[] };
}) => Promise<void>;
export function createEvaluateDataset({
chatClient, evaluators, executorClient,
}: {
chatClient: MyChatClient;
evaluators: DefaultEvaluators;
executorClient: EvalsExecutorClient;
}): EvaluateDataset {
return async ({ dataset }) => {
await executorClient.runExperiment(
{
dataset,
task: async ({ input }) => {
const response = await chatClient.converse({ messages: [{ message: input.question }] });
return { messages: response.messages, steps: response.steps };
},
},
[myCriteriaEvaluator, myToolCallsEvaluator]
);
};
}In the spec:
import { evaluate as base } from '../src/evaluate';
import type { EvaluateDataset } from '../src/evaluate_dataset';
import { createEvaluateDataset } from '../src/evaluate_dataset';
const evaluate = base.extend<{ evaluateDataset: EvaluateDataset }, {}>({
evaluateDataset: [
({ chatClient, evaluators, executorClient }, use) => {
use(createEvaluateDataset({ chatClient, evaluators, executorClient }));
},
{ scope: 'test' },
],
});
evaluate.describe('My suite', { tag: tags.serverless.search }, () => {
evaluate('my test', async ({ evaluateDataset }) => {
await evaluateDataset({ dataset: { name: '...', description: '...', examples: [...] } });
});
});Setup and Teardown
Use evaluate.beforeAll / evaluate.afterAll for expensive one-time operations:
- Install product docs: POST to
/internal/product_doc_base/install - Create agents/rules: Use
fetchorkbnClient - Load ES archives: Use
esArchiver.load(archivePath)(requires custom fixture)
Always clean up in afterAll -- delete agents, uninstall docs, unload archives.
Running Locally
# Full interactive flow
node scripts/evals start
# Specify model and judge
node scripts/evals start --model <connector-id> --judge <connector-id>
# Filter to a specific test
node scripts/evals start --grep "my test name"
# Run directly (services already running)
node scripts/evals run --model <connector-id> --judge <connector-id>Common Mistakes
- Forgetting the
tagonevaluate.describe-- Scout validates tags at runtime. - Missing
afterAllcleanup -- leftover agents/docs pollute subsequent runs. - Overly large datasets for local iteration -- use
--grepto target a singleevaluate()block. - Importing
evaluatefrom@kbn/evalswhen the suite has a customsrc/evaluate.ts-- you'll miss custom fixtures. - Using
testinstead ofevaluate-- theevaluatefixture provides all the evals-specific wiring.
References
- Evaluator type examples with real code: references/evaluator-patterns.md
- Suite scaffolding: use the
evals-create-suiteskill
Evaluator Patterns
Extended examples for each evaluator type, extracted from real eval suites.
CODE Evaluators with selectEvaluators
From llm-tasks -- deterministic checks with typed generics:
import { selectEvaluators, type Example, type TaskOutput } from '@kbn/evals';
type MyExample = Example & {
input: { searchTerm: string; products?: string[] };
metadata?: { minDocs?: number; requiredTerms?: string[] };
};
type MyTaskOutput = TaskOutput & {
success: boolean;
documents: Array<{ title: string; url: string; content: string }>;
};
await executorClient.runExperiment(
{ dataset, task },
selectEvaluators<MyExample, MyTaskOutput>([
{
name: 'NonEmptyDocuments',
kind: 'CODE',
evaluate: async ({ output, metadata }) => {
const minDocs = typeof metadata?.minDocs === 'number' ? metadata.minDocs : 1;
const count = output?.documents?.length ?? 0;
return { score: count >= minDocs ? 1 : 0, metadata: { minDocs, count } };
},
},
{
name: 'RequiredTermsInContent',
kind: 'CODE',
evaluate: async ({ output, metadata }) => {
const requiredTerms = metadata?.requiredTerms ?? [];
if (requiredTerms.length === 0) return { score: 1 };
const text = (output?.documents ?? [])
.slice(0, 3)
.map((d) => `${d.title}\n${d.content}`)
.join('\n');
const ok = requiredTerms.every((term) =>
text.toLowerCase().includes(term.toLowerCase())
);
return { score: ok ? 1 : 0, metadata: { requiredTerms } };
},
},
{
name: 'HasValidUrl',
kind: 'CODE',
evaluate: async ({ output }) => {
const urls = (output?.documents ?? []).map((d) => d.url);
const ok = urls.some((u) => typeof u === 'string' && u.startsWith('https://'));
return { score: ok ? 1 : 0, metadata: { urls: urls.slice(0, 3) } };
},
},
])
);Key points:
selectEvaluators<ExampleType, OutputType>([...])gives type safety oninput,output,expected,metadata.kind: 'CODE'means no LLM call -- fast and deterministic.- Return
metadatain the result to aid debugging.
LLM-as-Judge Criteria via evaluators.criteria
From security-solution-evals -- the criteria evaluator delegates to the judge LLM:
const mainCriteriaResult = await evaluators
.criteria([
'The response correctly identifies the top users.',
'The response includes risk scores for each user.',
'The response includes risk levels for each user.',
])
.evaluate({ input, output, expected, metadata });Each criterion is evaluated independently. The judge returns a score (0 or 1) and explanation per criterion.
Wrapping Criteria in a Reusable Evaluator
function createCriteriaEvaluator({ evaluators }: { evaluators: DefaultEvaluators }) {
return {
name: 'Criteria',
kind: 'LLM' as const,
evaluate: async ({ input, output, expected, metadata }: {
input: MyExample['input'];
output: MyTaskOutput;
expected: MyExample['output'];
metadata: MyExample['metadata'];
}) => {
const criteria = expected.criteria ?? [];
if (criteria.length === 0) {
return { score: 1, label: 'PASS', explanation: 'No criteria specified.' };
}
return evaluators.criteria(criteria).evaluate({ input, expected, output, metadata });
},
};
}Tool Call Evaluator
From security-solution-evals -- checks that specific tools were invoked:
function createToolCallsEvaluator({ evaluators }: { evaluators: DefaultEvaluators }) {
return {
name: 'ToolCalls',
kind: 'LLM' as const,
evaluate: async ({ input, output, expected, metadata }) => {
const toolCalls = expected.toolCalls ?? [];
const steps = output.steps ?? [];
if (toolCalls.length === 0) {
return { score: 1, label: 'PASS', explanation: 'No tool call assertions.' };
}
const results = [];
for (const assertion of toolCalls) {
const called = steps.some(
(s) => s.type === 'tool_call' && s.tool_id === assertion.id
);
if (!called) {
results.push({
score: 0,
label: 'FAIL',
explanation: `Tool "${assertion.id}" was not called.`,
});
continue;
}
if (assertion.criteria?.length) {
const criteriaResult = await evaluators
.criteria(assertion.criteria)
.evaluate({ input, expected: { criteria: assertion.criteria }, output, metadata });
results.push(criteriaResult);
} else {
results.push({ score: 1, label: 'PASS', explanation: `Tool "${assertion.id}" called.` });
}
}
const allPassed = results.every((r) => r.label === 'PASS');
const scores = results.map((r) => r.score ?? 0);
const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
return {
score: allPassed ? avg : 0,
label: allPassed ? 'PASS' : 'FAIL',
explanation: results.map((r) => r.explanation).join(' '),
};
},
};
}Dataset examples with tool call assertions:
{
input: { question: 'Which users have the highest risk scores?' },
output: {
criteria: [
'Return 10 users with the highest risk scores.',
'Return the risk levels of those users.',
],
toolCalls: [
{
id: 'security.entity_analytics.risk_score',
criteria: ['The ES|QL query should sort by risk score descending.'],
},
],
},
}RAG Evaluators
For retrieval quality with ground truth documents:
import {
createPrecisionAtKEvaluator,
createRecallAtKEvaluator,
createF1AtKEvaluator,
createRagEvaluators,
} from '@kbn/evals';
import type { GroundTruth, RetrievedDoc } from '@kbn/evals';The createRagEvaluators factory creates all three at once:
const ragEvals = createRagEvaluators({
k: 5,
extractRetrievedDocs: (output) =>
output.documents.map((d) => ({ id: d.id, content: d.content })),
extractGroundTruth: (expected) => expected.groundTruth,
});Ground truth in datasets uses document IDs mapped to relevance scores:
{
input: { question: 'How do I set up payments?' },
output: {
expected: 'You can start accepting payments using Wix Payments...',
groundTruth: {
knowledge_base: {
'doc_hash_abc123': 1,
'doc_hash_def456': 1,
},
},
},
}Trace-Based Evaluators
These are pre-built and available from evaluators.traceBasedEvaluators. They query the tracing ES cluster for spans matching the current trace ID:
const { inputTokens, outputTokens, cachedTokens, toolCalls, latency } =
evaluators.traceBasedEvaluators;Each returns a numeric score:
inputTokens/outputTokens/cachedTokens-- token counts fromgen_ai.usage.*span attributestoolCalls-- count of tool call spanslatency-- total span duration in seconds
To use trace-based evaluators, EDOT must be running and TRACING_ES_URL must point to an ES instance receiving traces.
Combining Multiple Evaluator Types
A common pattern passes both CODE and LLM evaluators to runExperiment:
await executorClient.runExperiment(
{ dataset, task },
[
createCriteriaEvaluator({ evaluators }),
createToolCallsEvaluator({ evaluators }),
{
name: 'HasResponse',
kind: 'CODE',
evaluate: async ({ output }) => ({
score: output?.messages?.length > 0 ? 1 : 0,
}),
},
]
);The executor runs all evaluators for each example and aggregates scores in the final report.
Related skills
FAQ
What does evals-write-spec do?
evals-write-spec skill documents Write LLM evaluation spec files with datasets, tasks, and evaluators using the @kbn/evals Playwright fixture.
When should I use evals-write-spec?
User asks about evals-write-spec, write llm evaluation spec files with datasets, tasks, and evaluators using the @kbn/evals .
Is this skill safe to install?
Review the Security Audits panel on this page before installing in production.