Evals
An eval answers one question about a stage or an agent: is it any good, and did my last change make it better or worse? It runs the target over many realistic inputs, grades every output, repeats each input to measure how stable the target is, and lets you compare two runs. A stage case is one input and one output; an agent case is a whole conversation (see Evals for agents).
Evals are a general quality tool for configurable stages and agents. A stage that triages tickets, summarizes research, reviews a draft, classifies an email, writes release notes or checks an ops runbook can all be evaluated the same way. Nothing in an eval is specific to one domain.
An eval is a file in your project, evals/<id>.eval.yaml. It names a target (one stage, or one agent), a dataset of cases (datasets/<name>.dataset.jsonl), a list of graders and run defaults. You run it from the console, the CLI or MCP. Results are stored in the run store, so they survive restarts and can be compared later. Saving an eval never changes past runs: each run keeps a snapshot of exactly what it used.
Concepts
| Term | Meaning |
|---|---|
| Eval | The definition file evals/<id>.eval.yaml: target, dataset, graders, defaults |
| Target | What the eval tests: one stage (kind: stage) or one agent (kind: agent) |
| Dataset | A file of cases |
| Case | One test input with an id, an input, and optional expected, tags, title, goal, metadata. For an agent it is a scenario |
| Repeat | One execution of a case. Each case runs repeats times because models vary |
| Result | One (case, repeat): the stage output (or the agent conversation) plus its grades. 24 cases with 3 repeats make 72 results |
| Grader | One rule that scores a result: schema, field, reference, rubric or code; for agents also tool_calls, conversation_check or goal_reached |
| Grade | One grader's verdict on one result |
| Rubric | A grader written in words and judged by a model |
| Judge | The model that applies rubrics |
| Eval run | One execution of an eval: all results, grades, statistics, cost, and a config snapshot |
| Stage run | The normal Stageflow run started for each result. Marked origin = eval and hidden from the Runs list, Inbox and Overview |
| Eval session | For an agent target, the agent session opened for each conversation. Marked as an eval session and hidden the same way |
| Config snapshot | Everything that determined an eval run, stored with it and never edited |
| Fingerprint | The first 8 hex characters of a hash of the config snapshot. Equal fingerprints mean the same setup |
| Engine | The component that executes results and grades them. Today promptfoo, installed on demand |
See also the Glossary. Agent terms (scenario, driver, simulator, conversation subject, tool policy) are in Agent concepts.
Quick start with the sample
The repository ships a small sample in examples/evals/: a critique stage that reviews short draft specifications, 8 cases (6 with a planted problem, 2 clean), and four graders that need no judge model. See the sample README.
-
Set a default model. The sample stage has no
model:, so it uses the project default. Add one tostageflow.yaml:version: 1 model: anthropic/claude-sonnet-4-5 # any model id you have a provider forWithout a default
model, the pipeline does not load andsf validatereportseval.target_unresolved("Target pipeline … does not load"). -
Get the sample into your project. From a checkout, the catalog in the repository root already lists
examples/evals. In another project, open the console (sf ui), go to Evals and choose Try the sample eval. It copies the pipeline, eval and dataset into your project'spipelines/,evals/anddatasets/roots and rewrites the eval's two path lines. If it already exists, the console opens it instead. -
Validate and install the engine (once per machine, needs Node 22.22 or newer and network access):
sf validate --strict sf eval validate critique-sample sf eval engine install -
Run it:
sf eval run critique-sampleThis calls your model for each case and repeat (8 cases x 2 repeats = 16 stage runs).
max_cost_usd: 1caps the spend. The command waits, prints the outcome, and exits0only when the run passed.
Some failures are normal. The reference grader looks for keywords such as expir or backoff, so a correct finding worded differently can fail. Read the failing cases, then tighten the prompt or the expected keywords.
To try an agent instead, follow Evals for agents: the repository also ships a support-agent sample that runs with no judge model.
Writing an eval
A complete file, with every section:
version: 1
id: review-findings
name: Review findings
description: Review flags real defects
type: capability
target:
kind: stage
pipeline: ../pipelines/feature-ship.pipeline.yaml
stage: review
dataset: ../datasets/review-diffs.dataset.jsonl
task:
goal: Review the change and list defects.
defaults:
repeats: 3
judge: anthropic/claude-sonnet-4-5
pass_threshold: 0.8
max_cost_usd: 2
concurrency: 4
graders:
- id: output-schema
type: schema
must_pass: true
- id: findings-cite-lines
type: field
checks:
- { path: "payload.findings[*].file", op: not_empty }
- { path: "payload.findings[*].line", op: exists }
- id: finds-expected-issues
type: reference
weight: 3
mode: each_mentioned
path: payload.findings
expected: expected.findings
- id: clear-and-actionable
type: rubric
rubric: A developer could act on each finding without asking questions.
scale: 1_to_5
pass_at: 4
budget:
latency_ms: 300000
cost_usd: 0.3
The full key tables are in YAML catalog: Evals. Unknown keys are errors, so typos are caught.
Target
The target is one stage, in one of two forms (paths are relative to the eval file). To evaluate an agent instead, see Evals for agents.
target: { kind: stage, pipeline: ../pipelines/ship.pipeline.yaml, stage: review } # a stage of a pipeline
target: { kind: stage, file: ../stages/review.yaml } # a standalone stage file
The stage version hash (first 6 hex characters, shown in the console) changes whenever anything that can change the stage's behavior changes: prompt, model, io, verify, mcp, gate_kinds, timeout_ms.
Defaults
repeats (1 to 10, default 3), pass_threshold (default 0.8), max_cost_usd (default 2), concurrency (1 to 8, default 4), judge (a model id such as anthropic/claude-sonnet-4-5) and model (replaces the stage's model for every case). A run can override repeats, judge, model, max_cost_usd and the case selection without editing the file; the overrides are recorded in the snapshot.
Graders
Every grader has an id, a type, an optional description, a weight (default 1) and must_pass (default false). A must_pass grader that fails fails the result whatever the score, and is left out of the weighted mean. An eval has 1 to 20 graders.
schema. Passes when the envelope status is success and the payload validates against the stage's io.output.schema. Needs the stage to declare one (otherwise eval.schema_grader_without_output_schema).
- { id: output-schema, type: schema, must_pass: true }
field. Checks values at paths in the envelope ({ status, summary, artifacts, payload }). All checks must hold. Paths use dots, [n] for an index and [*] for "every element". Operators: exists, not_empty, equals, not_equals, contains, matches (a JavaScript regular expression), gte, lte, between ([min, max]), in, length_gte, length_lte.
- id: severity-valid
type: field
checks:
- { path: "payload.issues[*].severity", op: in, value: [high, medium, low] }
With [*], every matched node must pass. If [*] matches nothing (the array is empty), exists, not_empty and every comparison fail; only length_gte and length_lte still run, on the empty array itself. A check such as payload.issues[*].severity in [...] therefore fails when issues is empty.
reference. Compares the output with the case's expected. mode: each_mentioned needs an expected array of strings and scores the share found in the output text, ignoring case and extra whitespace (an empty array scores 1). mode: equals is deep equality. path defaults to payload, pass_at (0 to 1) defaults to 1.
- id: finds-expected-issues
type: reference
weight: 3
mode: each_mentioned
path: payload.issues
expected: expected.issues
A case with nothing at the expected path gets an error grade EVAL_CASE_MISSING_EXPECTED, not a failure, and is excluded from that grader's statistics.
rubric. A judge model grades the output against your words. scale: pass_fail (default) or 1_to_5 with pass_at (1 to 5, default 4). Optional judge overrides the default judge and up to 6 examples ({ verdict: pass|fail, output, reason? } for pass_fail; { score: 1-5, output, reason? } for 1_to_5) show the judge what each outcome looks like.
- id: no-invented-files
type: rubric
weight: 2
rubric: |
Pass if every file named in the review exists in the diff.
Fail if the review cites a file, function or line the diff does not contain.
scale: pass_fail
The judge sees the rubric, the examples, the case input, the case expected if present, and the stage output. It must reply with JSON; a reply that is not JSON, or an HTTP error from the provider, becomes an error grade (EVAL_JUDGE_ERROR) that is left out of the score. 1-to-5 scores are normalized to (v-1)/4 for statistics; the raw value is stored too. Rubrics are not calibrated: nothing checks the judge against your own labels (see What is not built). Pick a judge that is not the stage's model; if they match, validation warns (eval.judge_same_as_stage) because models tend to favor their own output.
code. The body of a JavaScript function that receives (output, testCase, context) where output is the envelope, testCase is the case, and context = { evalId, runId, repeat }. It returns a boolean, a number from 0 to 1, or { pass, score?, reason? }. A number passes when it is at least pass_threshold. A thrown error, a bad return value or running past 5 seconds is an error grade (EVAL_GRADER_CODE_ERROR), not a failure.
- id: no-false-alarms
type: code
weight: 2
code: |
return !testCase.tags.includes("no-issue") || output.payload.issues.length === 0;
code graders run JavaScript on your machine with your permissions, the same trust level as verify: command. Only run evals whose files you trust.
How a case becomes a task
For each result Stageflow starts a normal stage run with this task: goal from the case's goal, else the eval's task.goal, else "Evaluate case context and constraints from the eval's task; input from the case. No checkout or repository is set, so a stage that needs one cannot be evaluated yet (eval.stage_needs_checkout warns). A result whose stage run stops to ask the operator (a HITL gate) is recorded as an error EVAL_STAGE_NEEDS_INPUT and its stage run is cancelled (eval.stage_has_gates warns).
Budget warnings
budget: { latency_ms, cost_usd } flags a result that is slower or costlier than the number. These are warnings shown on the result; they never fail it.
Scoring
A result's score is the weighted mean of its graders' scores, leaving out must_pass graders and graders that errored. A result passes when no must_pass grader failed and the score is at least pass_threshold. A stage that failed, timed out or asked for input counts as a failed result, not as missing data. The run passes when it completed and its pass rate is at least pass_threshold.
Datasets
A dataset is a file of cases in datasets/ (or any root listed under catalog.datasets). The console edits .dataset.jsonl; .dataset.json and .dataset.csv load and run but are read-only in the console, which offers Convert to JSONL.
{"id":"c001","title":"Session token never expires","input":{"draft":"Users sign in with email and password. The server returns a session token that stays valid until the user signs out."},"expected":{"issues":["expir"]},"tags":["security"]}
{"id":"c003","title":"Clear export spec","input":{"draft":"The export button downloads the current table as a CSV file."},"expected":{"issues":[]},"tags":["no-issue"]}
| Key | Rule |
|---|---|
id |
Required. ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$, unique, stable (never renumbered) |
input |
Required object. Passed as the task input and checked against the stage's io.input.schema |
title |
Optional, up to 120 characters |
expected |
Optional, any JSON. Used by reference graders and shown to rubric judges |
tags |
Optional, up to 10 of ^[a-z0-9][a-z0-9-]{0,31}$. Used to pick which cases a run uses |
goal |
Optional. Replaces the eval's task.goal for this case |
metadata |
Optional, free-form. Never read by Stageflow |
JSON files hold an array of cases or { "cases": [...] }. CSV files use a header: id, title, goal, tags (split on ;), input.<name>, expected.<name>, and any :json suffix to parse a cell as JSON.
- Ids. A hand-written file without ids is an error, so ids never drift. Uploads get
c001,c002, ... in order. - Invalid cases are skipped. A case whose
inputdoes not match the stage'sio.input.schemais never run or graded. Validation warns (dataset.input_mismatch,eval.cases_skipped) and the run records how many were skipped. If none remain, the eval is invalid (eval.no_valid_cases). - Size. Up to 5,000 cases, 1 MB per case, 50 MB per uploaded file. A small set gives wide error bars; fewer than 5 cases warns (
dataset.few_cases). - In the console. Upload a file (JSONL, JSON or CSV), paste text, add cases from past runs of the stage, or add a case by hand. Uploading maps each field of the stage's input schema to a column of your file and previews which rows are ready and which are skipped. The console always writes JSONL.
Running an eval
Only one eval run is active per Host. A second start fails with EVAL_RUN_ACTIVE. Eval stage runs use the normal admission queue with no priority, so a large eval shares capacity with real runs; the default concurrency of 4 keeps that bounded.
- Console. Evals, then Run on a row (or Run again on a result). The dialog picks cases (all, failed last time, or hand-picked), repeats, cost limit, stage model and judge (searchable dropdowns), which earlier completed run to compare with when it finishes, and shows an estimate. Live progress shows results as they arrive; Stop run keeps the results so far. A run that is still going can be reopened at any time: its Evals row shows "Run #n running" and opening the eval lands on the live page. See Console: Evals.
- CLI.
sf eval run <id|path>; see CLI reference and CI. - MCP.
run_eval,get_eval_run,cancel_eval_runand others; see MCP.
A run needs the engine and a Host. sf eval run starts the shared global Host if nothing is listening.
Cost control
Every run has a cost limit (max_cost_usd, default 2) covering stage runs and judge calls. When spending reaches it, the run stops with status budget_stopped, the remaining results are marked skipped_budget, and the run does not pass. Completed results are kept and their statistics are still computed, labeled incomplete.
The estimate in the run dialog is advisory: it uses the cost of the eval's last completed run, or the cost of the stage's last 20 non-eval executions. With no history it says so and the limit still applies. Judge cost is computed by Stageflow from the judge's token counts and the model catalog's prices; a model with no known price counts as $0 and the run shows that judge cost is unknown.
Limits: up to 5,000 results per run (cases x repeats), 10 repeats, concurrency 8. More than 500 results warns (eval.large_run).
Timeouts
The Host does not enforce a stage's timeout_ms itself, so the eval runner does: after the stage's timeout_ms plus a 60 second safety margin (30 minutes when the stage sets none) it cancels the stage run and records EVAL_STAGE_TIMEOUT for that result.
Reading results
The result page has four tabs: Overview, Cases, Graders and Config.
Headline numbers
| Number | Meaning |
|---|---|
| Pass rate | Passed results divided by executed results. Errored results count as failed and are also reported separately |
| 95% CI (by case) | A 95% confidence interval that treats cases, not results, as the independent units. Repeats of one case are alike, so counting them separately would make the interval too narrow. With few cases the interval is wide on purpose: 10 cases with 8 passing is [50%, 100%], not 80% |
| Mean score | Mean score of completed results; spread is how much a case's score varies between its repeats |
| Reliability (pass^k) | Share of cases that pass on every repeat. The honest number when you need the stage to work each time. Hidden when repeats is 1 |
| pass@k | Share of cases that pass on at least one repeat. Shows what the stage can do on a good try; it overstates how dependable it is |
| Flaky cases | Cases that passed on some repeats and failed on others. A single run on these is noise |
| Latency p50 / p95 / slowest | Over completed results |
| Cost | Stage cost plus judge cost, per result and in total; tokens in and out |
Why repeats exist. Models vary from one call to the next. One execution per case tells you what happened once. Several executions show how stable the stage is (reliability, flaky cases) and tighten the estimate.
Graders tab and failure types
By grader shows each grader's pass rate, mean, range, errors and the change against the previous completed run (shown as "grader changed" when the grader's definition changed, because the two are not comparable). The case drawer shows the input, each repeat's output, every grade with its reason, and a review: mark the result Pass or Fail, add failure-type tags (tags are only for results that failed) and a one-sentence note. Reviews and tags are stored and summarized in the Failure types card. They are for your own triage; nothing uses them to check the judge.
Sort failures automatically {#sort-failures}
After a run you can ask a small, fast classifier to sort every failed or errored result, so you do not read each transcript. Press Sort failures in the console, run sf eval sort <evalRunId>, call POST /api/eval-runs/:id/failures/sort, or use the MCP tool sort_eval_failures. Sorting is manual only: nothing sorts after a run on its own.
Each failed result gets a suggested bucket, a suggested failure type and a confidence:
| Bucket | Meaning | What you do |
|---|---|---|
agent_wrong |
A careful human would say the target did something wrong | Change the agent or stage |
grader_wrong |
The output was fine, a grader failed it by mistake (false alarm, too literal, judge noise) | Fix the grader |
case_wrong |
The scenario or its expected values are wrong or ambiguous | Fix the dataset |
unclear |
Not enough signal | Read it |
Failure types are fixed lists, not YAML. Agent evals: missing_tool_call, unneeded_tool_call, wrong_or_missing_fact, policy_violation, tone, ungrounded_claim, off_topic, no_handoff, other. Stage evals: wrong_value, missing_field, schema_invalid, policy_violation, ungrounded_claim, tone, other. For each failing code or rubric grader the classifier also gives the probability that its failure is a false alarm.
Suggestions, not tags. Nothing is tagged until you accept. Accepting writes the failure type as the usual failure-type tag and a bucket:<bucket> tag through the same path as a hand review, then marks the suggestion accepted; dismissing hides it. A suggestion with a bucket confidence below 70% or the bucket unclear is low confidence and is left out of bulk accept (accept with no ids takes pending suggestions at or above min_confidence, default 0.7). Sorting again replaces pending suggestions and keeps accepted and dismissed ones; by default (only_unsorted; --resort on the CLI, only_unsorted: false in the API and MCP) results that already have a suggestion are skipped, and --resort re-sorts pending ones only (accepted and dismissed are never replaced).
Classifiers. classifier is auto, jev or judge.
jevcalls TypeSafe Jev (System One) atPOST https://api.typesafe.ai/v1/systemonewith the pinned modeljev-1.13.0, which is recorded on every suggestion. Cost is about $0.04 per million input tokens; one request per result takes about half a second. It never explains itself and always picks an answer, which is whyunclearis an explicit choice and the confidence decides what you trust. Sorting cost is tracked separately astriage_cost_usdand is not part of the run's cost limit.judgeuses the run's judge model through the same one-shot model call as the agent simulator, with a fixed prompt and a JSON reply. It costs model tokens and needs a Host with an agent backend.autousesjevwhen a TypeSafe key exists, elsejudgewhen the run has a judge model, else answers422 EVAL_CLASSIFIER_UNAVAILABLEwithmissing: "typesafe_key"or"judge_model".
TypeSafe key. The first one found wins: the stored provider credential typesafe (Settings, Providers, TypeSafe; see Providers), then the environment variables STAGEFLOW_PROVIDER_TYPESAFE_API_KEY, TYPESAFE_API_KEY, JEV_API_KEY. The key is never logged or returned; GET /api/eval-runs/:id/failures/status reports only the names of the sources that hold one (for example ["JEV_API_KEY"]).
What each result sends is one bounded JSON state of at most 24,000 characters (redacted, truncation recorded): the case title, tags, input and expected value, the output (for an agent, the transcript and tool calls with policy outcomes), each failed grader with its reason and definition, and the ids of the graders that passed. See Privacy and trust.
Compare
Compare two runs of the same eval (sf eval compare <a> <b>, or the Compare page). a is the baseline, b the candidate. The console's Compare page has a picker for each side, and a run's Compare with (in the Run dialog) can use any earlier completed run as the baseline, not only the previous one. The comparison is paired by case: both runs saw the same cases, so case difficulty cancels out. The verdict:
| Verdict | Meaning |
|---|---|
improved |
The 95% interval of the change excludes zero, on the positive side |
regressed |
The interval excludes zero, on the negative side |
no_clear_difference |
The interval includes zero. The console words this inconclusive: the data do not show a real change, in either direction |
insufficient_data |
Fewer than 5 cases in common |
An inconclusive result is not "no change"; it means this dataset cannot tell. The page also reports about how many cases would be needed to detect a 5 point difference. It lists what stayed the same (stage, model, dataset, graders, judge, repeats) and what changed, with a line diff of the system prompt when the stage differs. Warnings appear when the datasets or graders differ, because scores are then not comparable.
Capability and regression evals
type: capability asks "can the stage do this?" and starts with a pass rate well under 100%. type: regression guards behavior that already works. The type is guidance only and changes no behavior. On the Evals list, a capability eval whose last run passed with a pass rate of 98% or more is saturated: it can no longer show improvement, so keep it as a regression eval and add harder cases to a new capability eval. Other list statuses are never_run, running (a run is queued or running), passing, failing and regressed (the last run failed and the one before passed).
Evals for agents {#evals-for-agents}
An eval can also test an agent (see Agents). Everything above still applies: the same file, dataset, graders, repeats, statistics, compare, CLI, MCP and console. What changes is the unit under test. A stage case is one input and one output. An agent case is a conversation: several user messages, several agent replies and any tool calls in between. Stageflow plays the user, records the whole conversation, and grades it.
Use it to check that an agent still answers from its instructions, asks a clarifying question when it should, declines what it should decline, calls the right tools and never calls the wrong ones, before you save a change to its instructions, model or tools.
Agent concepts {#agent-concepts}
| Term | Meaning |
|---|---|
| Scenario | An agent case: the input of a dataset row. It describes the user, either as a fixed list of messages or as a persona and a goal |
| Driver | What plays the user in a scenario: a script (fixed messages) or a simulator |
| Simulator | A model that plays the user from a persona, a goal and some facts. Set with defaults.user_model (default: the judge) |
| Conversation subject | What graders read: the messages, the recorded tool calls, the final reply, the number of turns, why it ended, and usage. It is the agent counterpart of a stage's envelope |
| Tool policy | What happens when the agent calls a tool during an eval: read_only (default), mocked or real |
| Approval policy | What happens when the agent calls a tool that would ask a person first: record_only (default), deny or auto_approve_read |
| Eval session | The agent session Stageflow opens for each conversation. It is a normal agent session marked as an eval session, and it is hidden from the places people look (see Eval sessions) |
Most terms from the concepts table above carry over: a result is one repeat of one scenario, a grade is one grader's verdict on the conversation, and the run's config snapshot records the agent's revision and definition, the tool and approval policies and the simulator.
Quick start with the sample
The repository ships examples/evals/support-sample.*: a small support agent (support_sample) that answers from four facts in its instructions, six scripted scenarios and five graders. It uses script mode and no rubric or goal_reached grader, so it needs no judge model. It still calls the agent's own model for every turn, so a default model must be configured.
-
Set a default model in
stageflow.yaml(model: anthropic/claude-sonnet-4-5, or any id you have a provider for). -
Install the sample into your project. The agent resolves from the project's
agents/directory, so the sample has to be installed there. Through a running Host,POST /api/evals/samplewith{ "kind": "agent" }writes the agent through the agent service (so it gets a revision), enables it, and copies the eval and dataset into yourevals/anddatasets/roots with the dataset path rewritten. It fails withSAMPLE_NO_DEFAULT_MODEL(422) when the project has no default model and withSAMPLE_EXISTS(409) if the sample, or an agent with its id, is already there. By hand: copy the three files fromexamples/evals/(the agent asagents/support_sample.agent.yaml), fix thedataset:path, and optionallysf agents enable support_sample. Evals may run a disabled agent, which only warns (eval.agent_disabled). -
Validate, install the engine, run:
sf eval validate support-sample sf eval engine install sf eval run support-sampleThis calls your model for each turn: 6 scenarios x 2 repeats, one to two turns each.
max_cost_usd: 1caps the spend. The command waits, prints the outcome and exits0only when the run passed.
To turn the sample into a simulated eval, add persona, goal and opening to the scenarios, set conversation.mode: simulated, a defaults.user_model and a goal_reached grader. The sample README shows the lines.
Writing an agent eval
version: 1
id: support-basics
name: Support agent basics
type: capability
target:
kind: agent
agent: support # agents/support.agent.yaml
dataset: ../datasets/support-scenarios.dataset.jsonl
defaults:
repeats: 3
pass_threshold: 0.8
judge: anthropic/claude-sonnet-4-5
user_model: anthropic/claude-sonnet-4-5
max_cost_usd: 3
concurrency: 2
conversation:
mode: simulated
max_turns: 8
turn_timeout_ms: 120000
tools: read_only
approvals: record_only
read_only_tools: [billing.lookup_invoice]
graders:
- id: no-refund-without-approval
type: tool_calls
must_pass: true
must_not_call: [billing.refund]
- id: looks-up-the-invoice
type: tool_calls
must_call: [billing.lookup_invoice]
max_calls: 3
- id: one-question-at-a-time
type: conversation_check
max_agent_questions_per_turn: 1
- id: reaches-the-goal
type: goal_reached
weight: 3
- id: stays-polite
type: rubric
rubric: The agent stays polite and never blames the customer.
The key tables are in YAML catalog: Agent evals. Points that differ from stage evals:
- Target.
target: { kind: agent, agent: <id> }. There is norevisionkey: a run reads the agent's current revision once when it starts and records it. There is notaskblock. Archived agents are an error (eval.agent_archived). - Defaults.
concurrencydefaults to 2 andmax_cost_usdto 1, lower than for stages, because a conversation holds a model session for many turns and costs more.modelreplaces the agent's model for every scenario.user_modelis the simulator. conversation. Mode, turn limit, timeouts and the two policies below. Omit it for script mode, 8 turns,read_only,record_only.- Limits. The eval's limits replace the agent's own
limitsfor eval sessions:max_turns + 1agent turns, a session time ofceil(max_turns x turn_timeout_ms / 60000) + 5minutes, no per-session cost limit (the eval's cost limit applies), always-on off. The snapshot records both the agent's values and the effective ones. A scenario also stops aftermax_turns x turn_timeout_ms + 60 sof wall clock. The agent file is never edited. - Side effects off. Eval sessions never send on channels, run schedules, use web chat, push OTLP spans, or call the groundedness judge. Capture is forced to
fullso graders and the transcript see tool arguments and results.
Tool policy
conversation.tools decides what a tool call does. Every call is recorded in the conversation whatever happens to it, with its outcome.
tools |
Read-class tools | Every other tool |
|---|---|---|
read_only (default) |
Run | Stubbed, or mocked if the scenario has a tool_mocks entry for it |
mocked |
Mocked if the scenario has a mock, else run | Mocked if the scenario has a mock, else stubbed |
real |
Run | Run |
Read-class means the built-in read, grep, find and ls, a Stageflow tool whose risk is read, and any MCP tool you list in conversation.read_only_tools. Everything else, including write, edit, bash, destructive or external Stageflow tools, MCP tools you have not listed and browser tools, is treated as not read-only, because Stageflow cannot tell whether an MCP tool changes anything. read_only_tools cannot upgrade a built-in or Stageflow tool: listing write or bash there has no effect and is a validation error (eval.read_only_tools_not_mcp).
Outcomes you will see on a recorded tool call (policy_outcome):
ran: the tool executed.stubbed: it did not execute; the agent was told "was not executed in this evaluation." mocked: it did not execute; the agent received the scenario's canned text. A mock reaches the agent as an error-flagged tool result, because the runtime can only block a call with a reason. Treat mocks as approximate.would_ask_firstanddenied: see below.
Always blocked. Under read_only and mocked, stageflow.run_eval, stageflow.sort_eval_failures, stageflow.cancel_eval_run, stageflow.agent_chat, stageflow.fire_trigger, stageflow.probe_project_mcp and every sub-agent call (agent:<id>) are blocked and recorded denied. Under real, run_eval, sort_eval_failures, cancel_eval_run, agent_chat and fire_trigger stay blocked (no recursion, no triggers). A tool the agent is not granted, a write to a protected path and the operator-only Stageflow tools are denied under every policy. Eval sessions are read only for operators: the console hides the composer, and chat and approval requests on an eval session are refused with agent.session_eval_readonly. Sub-agents the agent is allowed to call run in their own eval sessions under the same policy; their cost is summed into the result.
real needs two consents. tools: real requires conversation.allow_real_tools: true in the file (without it: eval.real_tools_not_allowed, an error) and consent for each run: --allow-real-tools on the CLI, allow_real_tools: true in the API and MCP start body, a checkbox in the console's run dialog. Without the run consent the start is refused (EVAL_REAL_TOOLS_NOT_CONFIRMED, HTTP 422, exit 1). Validation always shows eval.tools_real as a warning. In real mode the agent's write, shell and MCP tools act on your project and your services from this machine. Use a throwaway copy of the project, or stay on read_only. A run can only make the policy safer than the file says (tools to read_only or mocked, approvals to record_only or deny); a riskier override is refused (EVAL_INVALID).
Approval policy
Some tools ask a person first: the tools in the agent's approvals.ask_first, and bash always (see Agent runtime: asking first). In an eval nobody is there to decide, so a person is never asked: no approval row is created, no turn waits, and nothing reaches the Inbox.
approvals |
An ask-first call that is read-class | An ask-first call that is not read-class |
|---|---|---|
record_only (default) |
would_ask_first |
would_ask_first |
deny |
denied |
denied |
auto_approve_read |
Goes on to the tool policy (can run) | would_ask_first |
In every case the agent is told "Approval for would_ask_first is what a tool_calls grader can assert with must_ask_first: "the agent tried something that needs approval". Use it to check that an agent asks before refunding rather than refunding.
Scenarios and datasets
A dataset row for an agent eval is a scenario. Dataset files are the same as for stages; only input is read differently. There are two modes, set by conversation.mode and overridable per scenario with input.mode.
{"id":"s002","title":"Order status, scripted","input":{"mode":"script","script":["Where is my order 1042?","It was ordered on Monday.","Thanks"]},"expected":{"tools":["orders.get_status"]}}
{"id":"s001","title":"Refund request, angry customer","input":{"mode":"simulated","persona":"Annoyed Pro customer, short sentences.","goal":"Get a refund for a double charge on invoice INV-2093.","opening":"I was charged twice and I want my money back.","facts":["invoice INV-2093","charged on the 3rd"],"tool_mocks":{"billing.lookup_invoice":{"result":{"status":"duplicate"}}}},"expected":{"outcome":"escalated_to_human","tools":["billing.lookup_invoice"],"forbidden_tools":["billing.refund"]},"tags":["billing"]}
script.input.scriptis the list of user messages (1 to 40). They are sent in order. The conversation ends after the last one is answered (stop_reason: script_end) or atmax_turns. It is deterministic, cheap, and best for regressions.simulated.input.openingis sent as the first user message as written. After each agent reply the simulator model writes the next user message frompersona,goalandfacts(things the user knows and reveals only when asked or natural), and ends the conversation when the goal is met, the user gives up, it repeats itself, ormax_turnsis reached. It adds a model call per turn and some noise.tool_mocksmaps tool ids to{ "result": … }or{ "error": "…" }. They apply underread_only(for tools that would be stubbed) andmocked, and are ignored underreal.expectedis free JSON. Three keys have a meaning:outcome(text, the fallback goal forgoal_reached),tools(the defaultmust_callof atool_callsgrader) andforbidden_tools(the defaultmust_not_call).
Field limits are in YAML catalog: datasets. A scenario that fails its mode's check is skipped when a run starts and reported (dataset.input_mismatch, "scenario …"); typos inside input warn (dataset.scenario_unknown_field). In the console you can add scenarios in a form, upload a file, or build script scenarios from finished sessions of the agent (From past sessions: each selected session becomes a script of its user messages, with expected.tools set to the tools it called and the tag from-session; tool results are not copied). An upload maps columns to input.opening, input.goal, input.persona, input.mode, input.facts (split), expected.outcome, expected.tools, expected.forbidden_tools, title and tags. Scripts and tool mocks cannot be mapped from CSV; they need a JSON or JSONL file (a hand-written CSV can use input.script:json). A dataset can serve a stage eval and an agent eval at once, but most rows will be invalid for one of them, so the console warns.
Graders for conversations
A grader reads the conversation subject, not the envelope. A grader type that cannot apply to the target is an error at validation (eval.grader_not_applicable).
| Type | Stage | Agent |
|---|---|---|
schema |
yes | no |
field, reference, code, rubric |
yes | yes |
tool_calls, conversation_check, goal_reached |
no | yes |
tool_calls. Passes when every rule holds; score 1 or 0.
- id: no-refund-without-approval
type: tool_calls
must_pass: true
must_not_call: [billing.refund] # none of these may be called
- id: looks-up-the-invoice
type: tool_calls
must_call: [billing.lookup_invoice] # each must be called at least once
max_calls: 3 # at most this many calls in all
args:
- { tool: billing.lookup_invoice, path: invoice_id, op: matches, value: "^INV-[0-9]+$" }
- id: asks-before-refunding
type: tool_calls
must_ask_first: [billing.refund] # must appear as would_ask_first
A call counts as called whatever its outcome, so an attempted billing.refund that was stubbed still breaks must_not_call. args checks the arguments of every call to that tool (path into the arguments, op as for field); a tool never called passes it vacuously, so pair it with must_call. A trailing .* matches a server prefix. When must_call or must_not_call is absent, the case's expected.tools and expected.forbidden_tools are used; write [] to opt out. With no rule at all for a case the grade is an error (EVAL_CASE_MISSING_EXPECTED) and the case is left out of that grader's statistics.
conversation_check. Deterministic checks of the shape of the conversation: max_turns (user turns), max_agent_questions_per_turn (a question is a ? followed by a space or the end of the text, outside code fences and URLs) and final_reply (a list of field checks applied to the last assistant message).
goal_reached. A judge reads the conversation and decides whether the agent achieved the scenario's goal (input.goal, else expected.outcome). It does not trust the simulator's own "done" signal. Pass or fail only. Optional judge and guidance. Needs a judge model.
Existing graders on a conversation. field and reference paths start at the conversation, with no prefix: final_reply, turns, stop_reason, messages[*].text, tool_calls[*].name, tool_calls[0].args.invoice_id, usage.cost_usd. reference defaults to path: final_reply. code receives the conversation as output. rubric has the judge read a bounded transcript: at most 30 turns, 2,000 characters per message, 500 for tool arguments and 500 for tool results, with a rubric prompt written for conversations. The conversation graders see text that is already redacted and capped (below).
The conversation subject. Stored with each result and shown as the transcript:
| Field | Meaning |
|---|---|
messages[] |
{ role, text, turn }, user and assistant, up to 80 messages (the first 4 and last 76 are kept) of up to 4,000 characters |
tool_calls[] |
{ name, turn, args, result_summary, status, policy_outcome } in call order, up to 200 |
final_reply, turns |
The last assistant message; user turns completed |
stop_reason |
script_end, resolved, abandoned (the simulated user gave up), max_turns, no_progress (the user repeated itself), empty_message; error or timeout on a failed result |
usage |
Cost and tokens, split into agent (sub-agents included) and simulator, and the call counts |
All text is redacted for secrets before it is capped and stored.
Running an agent eval
Run it from the console, sf eval run <id>, or run_eval, as for a stage (Running an eval). Differences:
- What is called. Each conversation calls the agent's model (and its sub-agents'), the simulator model for each simulated turn after the opening, and the judge for
rubricandgoal_reachedgraders. Run overrides:--model(the agent's model),--user-model,--judge, plus the safer-onlytoolsandapprovalsand therealconsent described above. - The revision is pinned at start. The run reads the agent once, runs that definition (with your model override) for every scenario, and records its revision and definition hash. Editing the agent while a run is in progress does not change that run. Afterwards the Config tab marks the run "edited since" when the agent's current revision differs, and Compare shows what changed in the agent. The run dialog has no revision picker: it always uses the current revision.
- Failures. A conversation whose agent turn fails (backend error, guardrail block, model timeout, limit reached) is recorded with the partial transcript as
stage_failedandEVAL_AGENT_FAILED; one that exceedsturn_timeout_msor the wall clock isEVAL_AGENT_TIMEOUT; a simulator failure iserrorwithEVAL_SIMULATOR_ERROR. Failed results count as failed, not as missing, and are not graded. A turn can never wait for an approval. A turn that succeeds but returns no text and no output tokens (typically a provider rate limit or outage) is anerrorwithEVAL_AGENT_EMPTY_REPLY: it is excluded from scoring, not graded. After 5 consecutiveEVAL_AGENT_EMPTY_REPLYorEVAL_AGENT_FAILEDcases the run stops early asfailedwith "Stopped after 5 consecutive provider failures; check your provider key or rate limit", and the remaining results arecancelled. - Try on one case. The console can run one scenario once (
POST /api/evals/try) and show the transcript and grades without storing a run. It can take minutes, it honors the real-tools consent, and it is refused while an eval run is active.
Cost and limits
An estimate shown before a run counts agent turns, simulator calls and judge calls. With a previous completed run it uses that run's actual mean cost; without one it uses fixed assumptions (up to 6 user turns for simulated scenarios, the script length for scripted ones, two tool calls per turn, typical token counts) and says so, and a missing model price makes it unknown. Cost counts the agent, its sub-agents, the simulator and the judge; where a provider reports no simulator usage, the simulator's cost is estimated from text length and flagged. A multi-turn case can cost 4 to 10 times a stage case. The cost limit is checked before a conversation starts, not inside it, so a run can pass max_cost_usd by up to concurrency conversations. Results not started when the limit is reached are skipped and the run ends budget_stopped. Budgets (budget.latency_ms, budget.cost_usd) apply per conversation.
Agent turns share the Host's model capacity with real agent use, and one eval run is active per Host, so keep concurrency at 2 unless you know the provider can take more.
Reading results
The result pages are the same, with agent additions: Turns and Tool calls tiles (the mean per conversation), the cost split into agent, simulator and judge, and a Transcript tab in the case drawer. The transcript lists each turn's messages, with tool call chips showing the outcome (Ran, Stubbed, Mocked, Would ask first, Denied), the arguments and the result summary, the stop reason, and a link to the agent run (with its waterfall) while the session still exists. A failed result shows the partial transcript and the error. Compare shows the agent's changed definition keys and an instructions line diff instead of a stage diff, and informational deltas for mean turns and tool calls. Comparing a stage run with an agent run, or runs of different agents, is refused (EVAL_COMPARE_MISMATCH).
Honest statistics for conversations. The formulas are unchanged, but their meaning shifts:
- pass^k is stricter. A conversation has many places to go wrong, so a scenario that passes one repeat in two is common. Reliability (pass on every repeat) will usually sit well below the pass rate. Read it before trusting a number.
- Repeats matter more. Use at least 3 and look at flaky scenarios; one run of a conversation is closer to one coin flip than one run of a stage.
- Simulated users add noise. Both the agent and the simulator vary, so two repeats of one scenario can be different conversations. Script mode removes the second source. When you compare two runs, keep the simulator model, its prompt version and the scenarios the same (Compare lists these when they differ); a changed simulator makes two runs not comparable.
- Deterministic graders are cheap, judged ones are not. Prefer
tool_callsandconversation_checkfor what you can state as a rule, and usegoal_reachedandrubricfor what you cannot. - The conversation graders (
tool_calls,conversation_check) are rules you wrote; the judged ones are not calibrated against your labels, as for stages.
Eval sessions {#eval-sessions}
Each conversation runs in an eval session: a real agent session marked source: "eval", pinned to the revision the run recorded. Eval sessions are hidden from the agent's session list, the Inbox, pending approvals, counts, and any retention-visible view, so evals never show up as real work. They stay readable by id: the result's transcript links to the agent run page, which shows the usual spans, tokens and cost, and a banner naming the eval and run. Deleting an eval run deletes its eval sessions and their sub-agent sessions; the stored conversation on the result remains readable if a session is already gone. Sessions an eval run left behind (a crash) are cleaned up when the Host next starts, once they are older than 7 days and no result references them. See Agents and Agent observability.
Troubleshooting agent evals
| Symptom | Cause and fix |
|---|---|
eval.agent_not_found |
The agent file is not in the project's agents/ directory (sf eval validate reads the file; a Host reads the agent catalog). For the sample, install it (see above) |
eval.agent_archived |
The agent is archived. Restore it or point the eval at another agent |
eval.agent_disabled (warning) |
The agent is disabled. Evals run it anyway; enable it only if you also want to chat with it |
eval.agent_invalid |
The agent file has problems (the finding names the first). Fix it in the agent editor |
SAMPLE_NO_DEFAULT_MODEL |
The agent sample needs a default model: in stageflow.yaml |
eval.simulator_unsupported / eval.user_model_missing |
A simulated scenario needs a simulator, and only anthropic/…, openai/… and openrouter/… models can play it. Set defaults.user_model (or a judge) to one of those |
eval.grader_not_applicable |
A schema grader on an agent, or tool_calls, conversation_check, goal_reached on a stage |
| A tool shows Stubbed and the agent says it "was not executed" | Expected under read_only: the tool is not read-only (or an MCP tool you did not list). List it in conversation.read_only_tools if it is safe, give the scenario a tool_mocks entry, or accept the stub. Under read_only a stubbed write still counts as a call for must_not_call |
| A tool shows Would ask first | The tool is in the agent's ask_first list (or is bash). Under record_only the call is recorded and denied, never run. Assert it with must_ask_first, or use auto_approve_read for read-class tools |
eval.real_tools_not_allowed / EVAL_REAL_TOOLS_NOT_CONFIRMED |
tools: real needs allow_real_tools: true in the file and the per-run consent (--allow-real-tools, the API or MCP flag, or the dialog checkbox) |
| The run shows "edited since" on the agent | The agent changed after the run started. The run used the revision it recorded; run again to test the new one |
Results EVAL_AGENT_TIMEOUT |
A turn ran past turn_timeout_ms, or the scenario past its wall clock. Raise turn_timeout_ms or simplify the scenario |
Results EVAL_AGENT_EMPTY_REPLY |
The agent replied with no text and no output tokens. Usually the provider rate-limited or failed. These results are error: they are not graded and are left out of the pass rate. Check the provider key and limits, then rerun the failed cases |
Run ends failed with "Stopped after 5 consecutive provider failures" |
Five agent cases in a row ended EVAL_AGENT_EMPTY_REPLY or EVAL_AGENT_FAILED (constant EVAL_MAX_CONSECUTIVE_PROVIDER_FAILURES), so the run stopped and the cases not yet started are cancelled. Fix the key, quota or model, then rerun; completed results are kept |
Many empty replies on an OpenRouter :free model |
Free models have a daily request limit, and once it is used up the provider answers with nothing. Wait for the limit to reset, switch the agent or --model to a paid model, or lower concurrency. Rerun with Failed last time to repeat only the cases that errored |
Results EVAL_AGENT_FAILED |
An agent turn failed; the message carries the agent error code (for example agent.guardrail_blocked or agent.limit_reached). Open the agent run from the partial transcript |
Results EVAL_SIMULATOR_ERROR |
The simulator model failed or returned nothing usable. Check its key and model, or switch to script mode |
| A scenario was skipped | Its input fails the mode's check. The dataset page lists the reason |
Every tool_calls grade is EVAL_CASE_MISSING_EXPECTED |
The grader has no rule and the scenarios have no expected.tools or expected.forbidden_tools. Add rules to the grader or expected to the scenarios |
Writing a good agent eval {#writing-a-good-agent-eval}
- Start with script scenarios and rule graders.
tool_callsandconversation_checkare free and repeatable. Addgoal_reachedorrubricfor what rules cannot say. - Baseline first. Run the unchanged agent with
repeats: 3(more for few scenarios) and keep that run as the baseline. One conversation is closer to a coin flip than a measurement. - Sort failures before editing anything. Press Sort failures (above) and read the buckets:
agent_wrongmeans change the agent;grader_wrongmeans fix the grader (loosen a literal check, tighten a vague rubric);case_wrongmeans fix the scenario or itsexpected. Editing the agent for a failure that belongs to a grader or a scenario makes the next comparison meaningless. - Change one thing per run. Edit the instructions, the model, one grader or the dataset, then run again and use Compare with the baseline. If the verdict is inconclusive, add scenarios (about how many are needed is shown) rather than repeats.
- Keep the setup comparable. The same simulator model, scenarios and graders; Compare warns when they differ.
- Move on when it passes. An eval that passes 98% or more is saturated: keep it as a regression eval and write harder scenarios.
The engine
Results are executed and graded by promptfoo, run as a pinned subprocess. It is not an npm dependency of Stageflow: it is installed on demand into $STAGEFLOW_HOME/eval-engine/<version>/.
sf eval engine status # installed? which version? Node ok?
sf eval engine install # npm install into $STAGEFLOW_HOME/eval-engine/0.124.0
- Pinned version: promptfoo
0.124.0. - Node: 22.22.0 or newer (Stageflow itself runs on Node 20 and up; only evals need the newer Node). Older Node fails with
EVAL_NODE_TOO_OLD. - Size: about 1.2 GB on disk and a one-time download. Install needs network access; running evals does not need promptfoo's servers.
- Offline behavior: after installation, only your own model providers are contacted. promptfoo's telemetry and remote features are blocked: the Host sets the disable flags and preloads a network guard that refuses connections to
*.promptfoo.appand*.promptfoo.dev. - Missing engine: the console shows a banner with the command, and a run start fails with
EVAL_ENGINE_UNAVAILABLE. - Docker: the image installs the engine at build time. See Docker: eval engine.
Judge models
Rubric graders and goal_reached need a judge model, and simulated scenarios need a simulator model (defaults.user_model, which falls back to the judge) from the same providers. In this release only anthropic/…, openai/… and openrouter/… model ids can be judges. Any other provider is refused when the run starts (EVAL_INVALID, "supported judge providers are …"). The stage itself can use any provider.
The judge's key comes from a Pi auth.json entry of type api_key for that provider (the credential file the Host already uses; see Providers), or else the environment variable of the same name: ANTHROPIC_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY. OAuth credentials (for example subscription tokens) are not API keys and cannot be used for judging. The key goes to the engine process through its environment and is never written into a config file or a result.
Privacy and trust
- Running an eval sends each case's input to the stage's model as a normal stage run.
- For rubric graders, each case's input and the stage output are also sent to the judge model's provider.
- Nothing is sent to promptfoo's services. Telemetry and remote features are blocked.
codegraders run JavaScript on your machine with your permissions, the same trust level asverify: command.- Results, outputs and your reviews are stored in the local run store (Data directory). Stage runs started by evals are retained and purged like other runs.
Sorting failures is opt-in per click (console, sf eval sort, the API or MCP) and never runs after a run. It sends each failed result's case, output and grader reasons (a bounded, redacted state) to TypeSafe (Jev) or, with the judge classifier, to the judge model's provider. TypeSafe states that it does not train on customer requests; Stageflow does not rely on that statement. Sorting does not use promptfoo.
For agent evals, in addition:
- A simulator model and the judge read the conversation (the scenario, the agent's replies and a bounded view of tool arguments and results), so they send it to their providers.
- Each scenario's messages go to the agent's model as a normal agent turn.
- Tool calls in
realmode act on the project and on any service the agent's tools reach, from this machine. By default (read_only) write, shell and unlisted tools are stubbed and recorded, not executed. - Nothing is sent to promptfoo's services.
- Transcripts store redacted text only: secrets are redacted before text is capped and stored.
- Eval sessions are hidden from agent lists, the Inbox, counts and retention-visible views, but remain readable by id.
Known limitations
- Stage fidelity for pipeline targets. When you evaluate a stage of a pipeline, Stageflow rebuilds the stage from its own YAML (the
uses:file or the inline entry) with the pipeline's model default. Pipeline-level overlays are not applied: entry-levelmcp,secrets,requires, a wrappercompletion, pipelinechannelsandschemas, andincludemerges. A stage that depends on those can behave differently under eval than inside its pipeline. - No checkout. Cases run with no repository or checkout.
- One run at a time per Host.
- Stage
timeout_msis enforced by the eval runner with a 60 second margin (30 minutes when unset), not by the run manager. - Judge cost is estimated from token counts and catalog prices; an unknown price counts as $0.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
EVAL_ENGINE_UNAVAILABLE or "Eval engine not installed" |
Run sf eval engine install. In Docker with an existing volume, run it inside the container |
EVAL_NODE_TOO_OLD |
Use Node 22.22.0 or newer for the Host. sf eval engine status shows the Node it sees |
eval.target_unresolved ("does not load") |
The target pipeline or stage file failed to load. For the sample, set a default model: in stageflow.yaml |
EVAL_INVALID mentioning "supported judge providers" |
The judge is not anthropic/…, openai/… or openrouter/…. Pick one of those, or remove the rubric graders |
Rubric grades show EVAL_JUDGE_ERROR |
No api_key credential or env var for the judge provider, a provider error, or a reply that was not JSON. Check the key and retry |
eval.judge_missing |
A rubric grader has no judge and the eval has no defaults.judge |
Results show EVAL_STAGE_NEEDS_INPUT |
The stage stopped at a HITL gate. Evals cannot answer gates; remove the gate or evaluate a stage without one |
dataset.input_mismatch / cases skipped |
Those cases do not match the stage's io.input.schema and are not run. Fix them on the dataset page |
EVAL_RUN_ACTIVE |
Another eval run is active on this Host. Wait or cancel it (sf eval runs cancel <id>) |
Run ends budget_stopped |
It reached max_cost_usd. Raise the limit with --max-cost, or run fewer cases or repeats. Completed results are kept |
EVAL_STAGE_TIMEOUT |
The stage ran past its timeout_ms plus 60 seconds. Raise the stage's timeout_ms or simplify the case |
FILE_CHANGED when saving |
The file changed on disk since it was loaded. Reload and reapply the edit |
| Engine work files needed for debugging | Set STAGEFLOW_EVALS_KEEP_WORKDIR=1 to keep $STAGEFLOW_HOME/eval-runs/<evalRunId>/ after a run (see Environment variables) |
Agent evals have their own table in Troubleshooting agent evals. Validation findings are listed in YAML catalog: Evals.
Error codes {#error-codes}
On a result (shown in the case drawer and stored with the result):
| Code | Result status | Meaning |
|---|---|---|
EVAL_STAGE_FAILED |
stage_failed |
The stage run failed |
EVAL_STAGE_TIMEOUT |
stage_failed |
The stage ran past its timeout_ms plus 60 seconds |
EVAL_STAGE_NEEDS_INPUT |
needs_input |
The stage stopped at a HITL gate |
EVAL_AGENT_FAILED |
stage_failed |
An agent turn failed; the message has the agent error code |
EVAL_AGENT_TIMEOUT |
stage_failed |
A turn passed turn_timeout_ms or the conversation passed its wall clock |
EVAL_AGENT_EMPTY_REPLY |
error |
The agent answered with no text and no output tokens; not graded |
EVAL_SIMULATOR_ERROR |
error |
The simulator model failed or returned nothing usable |
EVAL_ENGINE_ERROR |
error |
The engine finished without a result for the case, or the case cannot run (for example an approval wait) |
EVAL_BUDGET_EXCEEDED |
skipped_budget |
The cost limit was reached before the case started |
EVAL_CANCELLED |
cancelled |
The run was stopped, or stopped early after 5 consecutive provider failures |
EVAL_JUDGE_ERROR |
grade error |
The judge call failed or did not reply with JSON; left out of the score |
EVAL_GRADER_CODE_ERROR |
grade error |
A code grader threw, returned a bad value or ran past 5 seconds |
EVAL_CASE_MISSING_EXPECTED |
grade error |
A grader needs expected (or tool rules) the case does not have |
EVAL_GRADER_NOT_APPLICABLE |
grade error |
The grader type cannot read this subject; validation reports eval.grader_not_applicable first |
On a request (HTTP, CLI and MCP):
| Code | HTTP | Meaning |
|---|---|---|
EVAL_INVALID |
422 | The definition has errors (findings lists them), or a run override is not allowed |
EVAL_NOT_FOUND, EVAL_RUN_NOT_FOUND, DATASET_NOT_FOUND |
404 | No such eval, run or dataset |
EVAL_AMBIGUOUS, DATASET_AMBIGUOUS |
409 | The id exists in more than one project; pass project_root |
EVAL_EXISTS, DATASET_EXISTS, SAMPLE_EXISTS |
409 | Creating something that already exists |
EVAL_RUN_ACTIVE |
409 | Another eval run is active on this Host (also returned when sorting a run that is still running) |
FILE_CHANGED |
409 | The file changed on disk since it was loaded |
DATASET_READ_ONLY |
409 | .json and .csv datasets are read-only; convert to JSONL first |
EVAL_DEFINITION_ID_MISMATCH |
422 | The saved definition's id differs from the file's |
DATASET_INVALID, DATASET_CASE_OP_INVALID |
422 | The dataset or a case edit is invalid (duplicate id, unknown run, bad format) |
DATASET_NAME_INVALID |
400 | The dataset name does not match the name pattern |
EVAL_TAGS_ON_PASSED_RESULT |
400 | Failure-type tags apply only to results marked failed |
EVAL_REAL_TOOLS_NOT_CONFIRMED |
422 | tools: real without the per-run consent |
EVAL_CLASSIFIER_UNAVAILABLE |
422 | No failure classifier can run (missing: "typesafe_key" or "judge_model") |
EVAL_COMPARE_MISMATCH |
400 | The two runs have different targets |
EVAL_ENGINE_UNAVAILABLE, EVAL_NODE_TOO_OLD |
503 | The engine is not installed, or Node is older than 22.22.0 |
SAMPLE_NO_DEFAULT_MODEL |
422 | The sample needs a default model: in stageflow.yaml |
SAMPLE_AGENTS_UNAVAILABLE |
503 | This Host cannot create agents, so the agent sample cannot be installed |
SAMPLE_SOURCE_MISSING |
500 | The packaged sample files were not found |
Validation findings (eval.*, dataset.*) are listed in YAML catalog: Evals.
HTTP API
The console uses these routes (all under the Host's /api, with the same auth as other routes). Eval runs are addressed by eval_run_id (er_ plus 12 hex characters) or by eval id and run number.
| Route | Purpose |
|---|---|
GET /api/eval-engine |
Engine status |
GET /api/evals, POST /api/evals |
List (with status, banner, footer); create |
GET /api/evals/:id, PUT /api/evals/:id |
One eval with findings; save |
POST /api/evals/validate, POST /api/evals/try, POST /api/evals/sample |
Validate a draft; run one case; install the sample (body kind is stage or agent, default stage) |
GET /api/evals/agent-targets |
Agents that can be an eval target, with model, revision, tools and why one is unavailable |
POST /api/evals/:id/runs, GET /api/evals/:id/runs/:n, GET /api/evals/:id/runs/estimate |
Start a run; read by number; estimate |
GET /api/evals/:id/failure-types |
Failure-type vocabulary |
GET /api/eval-runs, GET /api/eval-runs/:id, DELETE /api/eval-runs/:id |
List, read, delete runs (?subject_ref=<session or stage run id> finds the run that owns a session) |
GET /api/eval-runs/:id/results, …/results/:caseId, PUT …/results/:caseId/:repeat/review |
Result rows, one case, a review |
POST /api/eval-runs/:id/cancel, GET /api/eval-runs/:id/export |
Cancel; export |
POST /api/eval-runs/:id/failures/sort |
Sort failed results (body classifier, only_unsorted, limit); answers { batch_id, classifier, model, sorted, skipped, failed, triage_cost_usd, suggestions }; 409 EVAL_RUN_ACTIVE, 422 EVAL_CLASSIFIER_UNAVAILABLE |
GET /api/eval-runs/:id/failures/suggestions?status= |
Suggestions and a summary (counts by bucket, by type, by status, low confidence) |
POST /api/eval-runs/:id/failures/suggestions/accept, …/dismiss |
Accept (by ids, or in bulk by min_confidence and bucket) or dismiss suggestions |
GET /api/eval-runs/:id/failures/status |
Which classifiers can run and which auto would use |
GET /api/eval-compare?a=&b= |
Compare two runs |
GET /api/datasets, POST /api/datasets, GET /api/datasets/:id, PATCH /api/datasets/:id/cases, POST /api/datasets/:id/convert, POST /api/datasets/preview, GET /api/datasets/candidates |
Datasets (GET /api/datasets/:id?eval_id= checks scenarios against an agent eval; candidates?agent=<id> lists finished sessions to turn into scenarios) |
For agent targets, POST /api/evals/:id/runs also takes user_model, allow_real_tools, and the safer-only tools and approvals; the estimate route takes the same options; a run that needs real tools without consent answers 422 EVAL_REAL_TOOLS_NOT_CONFIRMED. Result detail carries the stored conversation (output), turns, tool_call_count and agent: { id, session_id }; run stats carry mean_turns and mean_tool_calls. Result detail also carries repeats[].suggestion (the result's current suggestion, or null).
The console formats numbers and never computes them. Run detail carries lowest_cases_by_grader (per grader id, up to five { case_id, mean_score, worst_repeat_index } ordered by mean grade score); result rows carry pass_fraction and mean_score; every compare delta (deltas.*) is { a, b, delta, percent_change }, where percent_change is (b - a) / a * 100, or null when a is 0 or a side is missing.
Not built {#not-built}
These are not part of this release:
commandgraders (graders that run a command in a workspace).- Judge calibration. Rubrics are shown as "Not calibrated": nothing checks the judge against your labels, and reviews are not used for that.
- Pipeline evals (evaluating a whole pipeline end to end).
- CI triggers or scheduled eval runs.
sf eval runworks in CI, but nothing starts evals on a schedule. - Synthetic data generation, production monitoring, red-teaming and deleting an eval from the console.
Not built for agent evals:
- Voice and channel-specific evals. Conversations run in memory; no channel, schedule or web chat is involved.
- Red-teaming and adversarial scenario generators, and auto-generated scenarios. You write or import scenarios.
- Production or online evals. Evals run on scenarios, not on live traffic.
- Multi-agent topologies as a target. One agent is the target; its sub-agents run inside it.
- Human labelling and judge calibration for conversations, as for stages.
- Scheduled or CI triggers.
sf eval runand its exit codes work in CI, but nothing starts runs for you. - A
groundednessgrader, and a revision picker in the run dialog (a run uses the agent's current revision). - A shipped simulated sample. The sample is script mode; the README shows how to make it simulated.
- Editing the agent from the eval screens, beyond what evals already do for their own files.
See also
- Console: Evals
- YAML catalog: Evals
- CLI reference:
sf eval - MCP: eval tools
- CI: evals
- Sample project (a stage sample and an agent sample)
- Agents and Agent definition