Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Create offline evaluation tests for Output SDK workflows using @outputai/evals. Use when implementing test evaluators with verify(), creating dataset YAML files, building eval workflows, or running workflow tests via CLI.
.claude/skills/growthxai-output-dev-eval-testing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 82% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 38% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 75% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 19% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 229% | 0% |
The @outputai/evals package provides an offline evaluation framework for testing workflow quality using datasets and evaluators. This is complementary to the runtime evaluator() from @outputai/core:
| Aspect | Runtime Evaluators (@outputai/core) | Offline Eval Tests (@outputai/evals) | |--------|----------------------------------------|------------------------------------------| | When | During workflow execution | After execution, at test time | | Where | evaluators.ts in workflow folder | tests/evals/ in workflow folder | | Purpose | Live quality scoring with confidence | Dataset-driven pass/fail verification | | Triggered by | Workflow orchestration | output workflow test CLI command | | Returns | EvaluationBooleanResult, etc. | Verdict helpers (pass/partial/fail) |
Use offline eval testing when you want to validate workflow behavior against known datasets, build regression test suites, or assess subjective quality with LLM judges.
tests/evals/ or tests/datasets/verify() from @outputai/evalsevalWorkflow()output workflow test commandsAdd a tests/ directory inside the workflow folder:
src/workflows/{workflow_name}/
├── workflow.ts
├── steps.ts
├── evaluators.ts # Runtime evaluators (optional)
├── types.ts
└── tests/
├── datasets/
│ ├── happy_path.yml
│ └── edge_case.yml
└── evals/
├── evaluators.ts # Offline eval test evaluators
├── workflow.ts # Eval workflow definition
└── judge_topic@v1.prompt # LLM judge prompts (optional)verify()Import verify and Verdict from @outputai/evals (not @outputai/core):
typescript// tests/evals/evaluators.ts import { verify, Verdict } from '@outputai/evals'; import { z } from '@outputai/core';
verify() Signaturetypescriptverify(options, checkFn)
Options:
name — unique evaluator identifier (snake_case)input — Zod schema for the workflow input (optional, defaults to z.any())output — Zod schema for the workflow output (optional, defaults to z.any())Check function receives:
typescript{ input, // typed workflow input output, // typed workflow output context: { ground_truth: Record<string, unknown> // from dataset YAML } }
Returns: any Verdict helper result.
typescriptimport { verify, Verdict } from '@outputai/evals'; import { z } from '@outputai/core'; export const evaluateSum = verify( { name: 'evaluate_sum', input: z.object({ values: z.array(z.number()) }), output: z.object({ result: z.number() }) }, ({ input, output }) => Verdict.equals(output.result, input.values.reduce((a, b) => a + b, 0)) );
Ground truth values come from the dataset YAML and are available via context.ground_truth:
typescriptexport const lengthCheck = verify( { name: 'length_check', input: blogInput, output: blogOutput }, ({ output, context }) => Verdict.gte(output.blog_post.length, Number(context.ground_truth.min_length ?? 100)) );
All deterministic helpers return results with confidence 1.0.
| Method | Description | |--------|-------------| | Verdict.equals(actual, expected) | Strict equality (===) | | Verdict.closeTo(actual, expected, tolerance) | Within numeric tolerance | | Verdict.gt(actual, threshold) | Greater than | | Verdict.gte(actual, threshold) | Greater than or equal | | Verdict.lt(actual, threshold) | Less than | | Verdict.lte(actual, threshold) | Less than or equal | | Verdict.inRange(actual, min, max) | Within inclusive range |
| Method | Description | |--------|-------------| | Verdict.contains(haystack, needle) | String includes substring | | Verdict.matches(value, pattern) | Regex match | | Verdict.includesAll(actual, expected) | Array contains all expected values | | Verdict.includesAny(actual, expected) | Array contains at least one expected value |
| Method | Description | |--------|-------------| | Verdict.isTrue(value) | Value is true | | Verdict.isFalse(value) | Value is false |
| Method | Description | |--------|-------------| | Verdict.pass(reasoning?) | Explicit pass | | Verdict.partial(confidence, reasoning?, feedback?) | Partial pass with confidence | | Verdict.fail(reasoning, feedback?) | Explicit fail |
Before writing a judge prompt, identify the specific failure mode via error analysis (output-eval-error-analysis). Design the judge following output-eval-judge-prompt. After writing it, validate against human labels using output-eval-validate-judge.
For subjective quality assessments, use judge functions with .prompt files:
typescriptimport { verify, judgeVerdict, judgeScore, judgeLabel } from '@outputai/evals'; // Returns pass/partial/fail verdict from an LLM export const evaluateTopic = verify( { name: 'evaluate_topic', input: blogInput, output: blogOutput }, async ({ input, output, context }) => judgeVerdict({ prompt: 'judge_topic@v1', variables: { blog_title: output.title, blog_post: output.blog_post, required_topic: String(context.ground_truth.required_topic ?? input.topic) } }) ); // Returns a numeric score from an LLM export const evaluateQuality = verify( { name: 'evaluate_quality', input: blogInput, output: blogOutput }, async ({ input, output }) => judgeScore({ prompt: 'judge_quality@v1', variables: { blog_title: output.title, blog_post: output.blog_post, topic: input.topic } }) ); // Returns a string label from an LLM export const evaluateTone = verify( { name: 'evaluate_tone', input: blogInput, output: blogOutput }, async ({ output }) => judgeLabel({ prompt: 'judge_tone@v1', variables: { blog_title: output.title, blog_post: output.blog_post } }) );
.prompt File FormatJudge prompt files live alongside evaluators in tests/evals/:
yaml# tests/evals/judge_topic@v1.prompt --- provider: anthropic # current as of 2026-05-04 — run output-dev-model-selection for the latest model: claude-haiku-4-5-20251001 temperature: 0 maxTokens: 1000 --- <system> You are an evaluation judge. Assess whether a blog post is faithfully about the required topic. Return a JSON object with: - verdict: "pass" if the blog clearly focuses on the topic, "partial" if it mentions the topic but lacks depth, "fail" if it is not about the topic - reasoning: a brief explanation of your judgment </system> <user> Required topic: {{ required_topic }} Blog title: {{ blog_title }} Blog post: {{ blog_post }} Judge whether this blog post is faithfully about the required topic. </user>
The eval workflow wires evaluators together and defines how to interpret results.
typescript// tests/evals/workflow.ts import { evalWorkflow } from '@outputai/evals'; import { evaluateSum } from './evaluators.js'; export default evalWorkflow({ name: 'simple_eval', evals: [ { evaluator: evaluateSum, criticality: 'required', interpret: { type: 'boolean' } } ] });
Each entry in the evals array has:
evaluator — the function created by verify()criticality — 'required' (affects pass/fail) or 'informational' (reported but doesn't block)interpret — how to convert the evaluator's return value into a verdict| Type | Evaluator Returns | Mapping | |------|-------------------|---------| | { type: 'boolean' } | Verdict.equals(), Verdict.gte(), etc. | true = pass, false = fail | | { type: 'verdict' } | judgeVerdict() or Verdict.pass/partial/fail() | Direct pass-through | | { type: 'number', pass: 0.7, partial: 0.4 } | judgeScore() | >=pass = pass, >=partial = partial, else fail | | { type: 'string', pass: ['a', 'b'], partial: ['c'] } | judgeLabel() | Label in pass list = pass, in partial list = partial, else fail |
typescriptexport default evalWorkflow({ name: 'blog_generator_eval', evals: [ { evaluator: lengthOfOutput, criticality: 'required', interpret: { type: 'boolean' } }, { evaluator: evaluateTopic, criticality: 'required', interpret: { type: 'verdict' } }, { evaluator: evaluateQuality, criticality: 'required', interpret: { type: 'number', pass: 0.7, partial: 0.4 } }, { evaluator: evaluateContent, criticality: 'informational', interpret: { type: 'boolean' } }, { evaluator: evaluateTone, criticality: 'informational', interpret: { type: 'string', pass: ['professional', 'informative'], partial: ['casual'] } } ] });
The eval workflow name must end in _eval and match the pattern {workflow_name}_eval. The CLI resolves this automatically — output workflow test blog_generator looks for blog_generator_eval.
For methodology on designing diverse datasets that cover failure-prone regions, see output-eval-dataset-design.
Datasets are YAML files in tests/datasets/. Each file represents one test case.
yamlname: basic_input input: values: - 1 - 2 - 3 - 4 - 5 last_output: output: result: 15 executionTimeMs: 100 date: '2026-02-13T00:00:00.000Z'
Ground truth provides expected values for evaluators. You can set global values and per-evaluator overrides:
yamlname: stripe_blog input: topic: "Stripe the payment processor" requirements: "Include a link to https://stripe.com/en-gb/pricing" last_output: output: title: "Stripe: The Modern Payment Processing Platform" blog_post: | Stripe has revolutionized online payment processing... executionTimeMs: 5000 date: '2026-02-16T00:00:00.000Z' ground_truth: notes: "Known good case" evals: length_of_output: min_length: 100 evaluate_topic: required_topic: "Stripe the payment processor" evaluate_content: required_content: "https://stripe.com/en-gb/pricing"
The ground_truth.evals.<evaluator_name> values are merged with the top-level ground truth and passed to the evaluator via context.ground_truth.
output workflow test <workflow_name>Runs evaluations against all datasets for a workflow.
| Flag | Description | |------|-------------| | --cached | Use cached output from dataset files (skip workflow execution) | | --save | Run workflow fresh and save output + eval results back to dataset files | | --dataset <names> | Comma-separated list of dataset names to run (default: all) | | --json | Output machine-readable JSON instead of the rendered report |
Execution flow:
tests/datasets/--cached: executes the workflow for each dataset to get fresh output{workflow_name}_eval workflowoutput workflow dataset list <workflow_name>Lists all datasets for a workflow with their cached status.
| Flag | Description | |------|-------------| | --format <type> | Output format: table (default) or text | | --json | Output machine-readable JSON |
output workflow dataset generate <workflow_name> [scenario]Generates a new dataset file by running the workflow.
| Flag | Description | |------|-------------| | --input <json> | Workflow input as a JSON string or file path | | --name <name> | Dataset filename (defaults to scenario name) | | --trace <path> | Generate from a local trace file instead of running the workflow | | --download | Download traces from S3 and convert to datasets | | --limit <n> | Max traces to download from S3 (default: 5) |
bash# Generate dataset from inline JSON input output workflow dataset generate my_workflow --input '{"key": "value"}' --name my_test # Generate from a scenario file output workflow dataset generate my_workflow basic # Run evals with cached output (fast, no re-execution) output workflow test my_workflow --cached # Run evals fresh and save results output workflow test my_workflow --save # Run specific datasets only output workflow test my_workflow --dataset happy_path,edge_case # List all datasets output workflow dataset list my_workflow
bash# 1. Start the dev server npm run output:dev # 2. Generate datasets from real workflow runs output workflow dataset generate blog_generator --input '{"topic": "AI"}' --name ai_post # 3. Edit the dataset YAML to add ground_truth values for your evaluators # 4. Run evals with --save to cache output and eval results output workflow test blog_generator --save # 5. Iterate on evaluators, re-run with cached output (fast) output workflow test blog_generator --cached # 6. List all datasets output workflow dataset list blog_generator
verify, Verdict from @outputai/evals (not @outputai/core)evalWorkflow from @outputai/evals.js extension{workflow_name}_eval patterntests/datasets/tests/evals/name in snake_casecriticality is set to 'required' or 'informational' for each evalinterpret type matches evaluator return type.prompt files are in tests/evals/ alongside evaluatorsz is imported from @outputai/core (not zod)output-dev-evaluator-function — Runtime evaluators using evaluator() from @outputai/coreoutput-dev-scenario-file — Creating scenario JSON files for workflow executionoutput-dev-folder-structure — Understanding project directory layoutoutput-dev-prompt-file — Creating .prompt files for LLM operationsoutput-eval-error-analysis — Identify failure modes before building evaluatorsoutput-eval-judge-prompt — Design effective LLM judge promptsoutput-eval-dataset-design — Generate diverse test datasetsoutput-eval-validate-judge — Validate LLM judges against human labelsoutput-eval-audit — Audit an existing eval suite for trustworthiness| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-03 | fail→fail | 15,374 | 10,371 | -33% | 1 | 1 | 0% | 2,629 | 6,314 | +140% | 0 | 0 | — |
case-04 | fail→pass | 17,416 | 8,247 | -53% | 1 | 1 | 0% | 3,113 | 5,660 | +82% | 0 | 0 | — |
case-01 | fail→pass | 20,919 | 8,541 | -59% | 1 | 1 | 0% | 4,381 | 6,029 | +38% | 0 | 0 | — |
case-02 | fail→pass | 15,637 | 6,575 | -58% | 1 | 1 | 0% | 2,994 | 5,246 | +75% | 0 | 0 | — |
case-05 | pass→fail | 15,504 | 10,504 | -32% | 1 | 1 | 0% | 2,698 | 6,014 | +123% | 0 | 0 | — |
case-06 | pass→pass | 15,484 | 6,850 | -56% | 1 | 1 | 0% | 2,489 | 5,290 | +113% | 0 | 0 | — |
case-07 | fail→pass | 23,192 | 1,979 | -91% | 1 | 1 | 0% | 3,679 | 4,387 | +19% | 0 | 0 | — |
case-08 | fail→pass | 8,393 | 2,126 | -75% | 1 | 1 | 0% | 1,334 | 4,386 | +229% | 0 | 0 | — |
case-09 | fail→pass | 10,410 | 2,535 | -76% | 1 | 1 | 0% | 1,719 | 4,434 | +158% | 0 | 0 | — |
case-10 | fail→pass | 15,857 | 4,624 | -71% | 1 | 1 | 0% | 2,529 | 4,929 | +95% | 0 | 0 | — |
case-11 | fail→pass | 9,774 | 2,606 | -73% | 1 | 1 | 0% | 1,490 | 4,609 | +209% | 0 | 0 | — |
case-12 | pass→pass | 8,399 | 2,678 | -68% | 1 | 1 | 0% | 1,247 | 4,509 | +262% | 0 | 0 | — |
case-13 | fail→pass | 8,556 | 1,975 | -77% | 1 | 1 | 0% | 1,398 | 4,401 | +215% | 0 | 0 | — |
case-14 | fail→pass | 6,117 | 2,063 | -66% | 1 | 1 | 0% | 936 | 4,391 | +369% | 0 | 0 | — |
case-15 | fail→pass | 11,706 | 1,944 | -83% | 1 | 1 | 0% | 1,764 | 4,390 | +149% | 0 | 0 | — |
case-16 | pass→pass | 12,181 | 5,296 | -57% | 1 | 1 | 0% | 1,921 | 5,044 | +163% | 0 | 0 | — |
case-17 | fail→pass | 13,380 | 5,021 | -62% | 1 | 1 | 0% | 2,368 | 5,026 | +112% | 0 | 0 | — |
case-18 | fail→pass | 14,406 | 4,211 | -71% | 1 | 1 | 0% | 2,317 | 4,836 | +109% | 0 | 0 | — |
case-19 | fail→pass | 21,377 | 1,781 | -92% | 1 | 1 | 0% | 3,291 | 4,328 | +32% | 0 | 0 | — |
case-20 | fail→pass | 10,019 | 1,595 | -84% | 1 | 1 | 0% | 1,626 | 4,292 | +164% | 0 | 0 | — |
case-21 | fail→fail | 9,541 | 1,639 | -83% | 1 | 1 | 0% | 1,482 | 4,315 | +191% | 0 | 0 | — |
case-22 | pass→pass | 7,893 | 1,893 | -76% | 1 | 1 | 0% | 1,136 | 4,329 | +281% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +64 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.