Manage evals as code
Keep eval suites in your repository beside the Flow or Agent that they test. Run
the suites as a continuous integration (CI) gate so a regression stops in the
pipeline. This page covers the eval counterpart to managing Flows as code and
managing Agents as code. It uses the ensure protocol and adds the run command
to run and grade suites.
Define the suite
Use defineEval to create a pure, local definition. It validates the input shape
and returns a canonical definition. A suite names a target, a set of
cases, and the graders that score each case. The target is a saved Flow
or Agent identified by its portable name.
The following definition targets a Flow and combines suite-level and case-level graders:
Save the definition in a *.eval.ts file anywhere in your repository. For
example, use evals/support-triage.eval.ts. Export the suite as the file’s
default export.
The suite’s identity combines its name and your account scope. If you omit
name, it defaults to flow:<target> or agent:<target>. A target can have
one unnamed suite. Give suites for the same target distinct names, such as
smoke and regression, to keep them separate.
Graders
Graders run against each case’s final output. Deterministic checks run without a model. AI judges call a model.
The SDK provides the following grader builders:
Suite-level graders run on every case. The case’s expect graders are merged
after the suite-level graders. The effective grader set contains both lists, so
cases in one suite can check different things. Put shared checks in
graders, and put input-specific checks in each case’s expect list.
The following definition applies a shared check and different case-specific checks:
The refund case uses noError, calledTool('issue_refund'), and
judges.answersQuestion(). The greeting case uses noError, usedNoTools,
and contains('help'). The suite therefore contains two different effective
grader sets.
AI judge grading
A judge grader evaluates the whole run, not only its final output. If the run
has a captured execution trace, the judge uses the recorded steps and tool calls,
including their inputs and outputs, with the final output. This supports process
criteria such as confirming an order number before issuing a refund or avoiding
a call to delete_record.
For a conversation-seeded case, the judge also receives the seeded conversation that the run answers. Write criteria that refer to that conversation without restating it in the expected answer. If deterministic checks also run, the judge receives their pass or fail outcomes as ground truth. Write judge criteria for qualitative aspects that checks cannot express, such as tone, faithfulness, and reasoning quality. Use trace graders for exact deterministic sequence assertions.
The judge returns one of three verdicts:
- Pass: The criterion is clearly met.
- Fail: The criterion is clearly not met.
- Insufficient evidence: The run does not provide enough information to decide. This verdict renders as a warning and does not fail the case, including under
--strict.
Each verdict includes the judge’s reasoning. Run results in the dashboard include evidence chips that link to the steps or tool calls that support the verdict.
The platform judge is a Runtype-owned system Flow. Runtype embeds your criteria in an evaluation prompt and grades the run with a default model. An automatic cross-provider fallback handles unavailable providers.
The default judge also uses family-aware routing. If it shares a creator family
with the model that produced the output, Runtype selects a comparable judge from
a different family. If no usable cross-family judge is available to your account,
Runtype keeps the default judge. Set model on a judge grader to pin a judge
model. A pinned model is used exactly as written, regardless of family routing.
Judges with judgeFlowId also bypass family routing.
Custom judge flows
When the platform judge prompt does not cover your criteria, set judgeFlowId to
run one of your Flows as the judge. For example, use a retrieval step, rubric
lookup, or multi-step deliberation in the judge Flow.
The following grader uses a Flow ID placeholder:
Replace YOUR_FLOW_ID with the ID of the Flow that grades the case.
Your Flow runs through the normal Flow pipeline and counts like any other run. It follows this contract:
- Inputs: The Flow receives
criteria,caseName,expected, andtranscriptas Flow variables. The{{transcript}}value contains the indexed run trajectory and final output. For conversation-seeded cases, it starts with an unindexedSeeded conversationsection. If the case has check graders, it also contains an unindexedDeterministic check resultssection with each check’s ground-truth outcome. - Output: The final output must be the platform verdict JSON:
{"reasoning": string, "verdict": "pass" | "fail" | "insufficient_evidence", "evidence": [{"kind": "message" | "tool_call", "index": number}]}.
The grader validates this contract when it runs. If the judge Flow is missing,
fails, or returns invalid verdict JSON, the grader returns an
insufficient-evidence warning that names the problem. The warning cannot fail or
silently pass the case. When judgeFlowId is set, model is ignored. Each Flow
step selects its own model.
Split a large criterion into focused checks
Split criteria that combine several obligations into focused judges. For example, separate confirming an order number from avoiding delivery-date promises. Each judge then returns a clear verdict for one obligation.
Use decomposeCriteria to propose the split:
Review the proposal before you save it. Edit the wording, remove unwanted
sub-checks, and save each accepted sub-check as its own judge row. The call
does not persist anything. The resulting graders are ordinary AI graders, so
family-aware routing, .soft(), and .atLeast() apply. A single returned
sub-check means that the criterion is already focused.
Trace graders
Output graders evaluate the final output. Trace graders evaluate recorded behavior, including which tools and steps run, their order, completion status, and cost. Trace graders are deterministic and free. They use the captured execution trace. Use them when the assertion concerns behavior instead of text.
The SDK provides the following trace grader builders:
The following definition checks tool usage and run completion:
For ranStep and stepOrder, a step matches by configured name, Flow step ID,
or step type. For example, ranStep('prompt') checks that any prompt step ran.
stepOrder(['Draft reply', 'send-email']) combines a prompt name with a
context-step type. The trace records actual execution order, so stepOrder also
works for interleaved Flows such as prompt → upsert-record → prompt.
Trace graders are captured on the synchronous run path, including the
runtype eval run CI gate and client.evals.runSuite. The eval run records every
executed step and tool call, including steps that disable streaming to end users.
Nothing is streamed to a client during an eval run. If a run produces no trace,
trace graders fail with an explanatory reason instead of passing silently.
Severity: gate vs soft
Each grader is a gate by default. A failed gate fails its case. Mark a grader
soft with .soft() to report a miss without failing the case. A soft miss
fails only when the run uses --strict.
Mark latency, cost, or subjective judge checks soft when you want to track them
without blocking a merge. Keep correctness checks as gates.
The following definition uses gate and soft graders:
Chain .gate() or .soft() from any grader builder. .gate() is the default.
A soft miss appears as a warning in the run output. The .atLeast(n) handle
remains for backward compatibility. The judge returns a binary pass or fail
verdict, which maps internally to 5 or 1. Cutoffs from 2 to 5 therefore require
a pass verdict. .atLeast(1) passes, and new suites can omit it.
Converge at deploy time
Use ensure to make the saved suite match your repository definition. It creates,
updates, or leaves the suite unchanged. It does not run the suite. Call it from
continuous integration and continuous delivery (CI/CD), alongside the rest of
your deployment.
Pass your eval definition as suite to the ensure method:
ensure first sends a probe with the suite name and content hash. If the hash
matches, it writes nothing. If the content differs, it upserts the suite and
replaces its cases. The target Flow or Agent resolves by name, so the same
definition converges in every environment. Your credentials select the target
that you update.
Renaming a suite does not rename the existing suite. ensure creates another separate suite under
that name and leaves the old suite in place. It does not delete the old suite. Treat the name as
the stable identity.
Colocate evals with a Flow
Attach eval suites to a Flow definition with an inline evals array. Converging
the Flow also converges its inline evals in the same call. An inline eval without
target uses the Flow that contains it.
The following Flow definition includes an inline eval suite:
The Flow and each inline suite use separate endpoints. flows.ensure does not
send eval content in the Flow request, so inline evals do not change the Flow’s
content hash. The response returns each suite’s converge outcome under evals.
To run a colocated suite, pass its returned suiteId to
client.evals.runSuite({ suiteId }), or review the suite in the dashboard. The
runtype eval run command discovers standalone *.eval.ts files, not suites
converged inline with a Flow.
Run as a CI gate
Use the runtype eval run CLI command to discover your *.eval.ts files,
converge each suite, run every case against its target, grade the outputs, and
return an exit code. A regression produces a non-zero exit code.
The following commands run suites, select strict grading, or scope discovery:
Replace the following:
<alias>: a release alias on the target Agent, such aspr-482<agent-version-id>: an Agent version id fromGET /v1/agents/{agentId}/versions
The command returns the following exit codes:
To make soft graders fail the build, pass --strict. Without it, soft misses
appear as warnings and do not change the exit code.
A typical pipeline converges your Flows and Agents, then runs the gate against staging:
Set the STAGING_API environment variable to the staging API URL in your CI
workflow.
A non-zero exit blocks the merge. The run command first converges the suite
definition with ensure, so the saved suite remains visible in the dashboard.
Saved-suite gate runs also persist scores by default. Each run receives a durable
runId, which the CLI prints and the API returns. The saved outcomes build a
run-by-run history that you can read with GET /v1/eval/runs/{runId}/scores.
Pass virtual: true in the API request, or pass --virtual to the CLI, to keep
the run ephemeral.
The run is synchronous. Each case executes in one request. A suite has a 50-case per-run ceiling
and a 4-minute total wall-clock budget for case execution. Cases that do not run within the budget
are reported as failed with an explanatory message, and the response remains complete and scored.
Keep slow, multi-step suites well under 50 cases. For larger or slower suites, use a batch eval.
The run command supports Flow, standard Agent, and external Agent targets. An external Agent
in Delegate mode executes directly over A2A, so its replay semantics are narrower: multi-turn
input is flattened into one message, and tool traces must be reported by the external Agent. An
external Agent in Managed mode compiles its cached Agent Card skills into a virtual flow and
executes through Runtime; case messages remain a transcript and Runtime captures the orchestration
tool trace. Recorded-tool replay cases are currently skipped for external Agent targets in either
mode. A claude_managed Agent is not supported here. Use a batch eval for that target.
Batch-evaluate a delegated external Agent
Use POST /v1/eval/submit when a Delegate-mode external Agent needs the asynchronous batch
lane. Supply one baseline eval config and put each record’s conversation in
metadata.messages:
The remote endpoint is opaque to Runtype, so this lane evaluates only the Agent’s saved remote
configuration. Omit overrides (an empty object is also accepted) and options.modelOverride.
Model, step, sub-Agent, Claude Managed, and advisor overrides return
EXTERNAL_AGENT_OVERRIDE_NOT_APPLICABLE instead of running an unchanged baseline. The batch has
no hosted-model snapshot because Runtype does not control or observe the remote model. Supplying
more than one eval config returns EXTERNAL_AGENT_COMPARISON_NOT_SUPPORTED because Runtype cannot
vary the opaque endpoint between configs.
Run inline without persisting
Set virtual: true on a suite, or pass --virtual, to run it without converging
anything to the dashboard. This mode does not persist the suite or its results,
and it skips ensure. Use it for local checks.
Manage suites imperatively
Use ensure when your repository owns the suite definition. When you want to
inspect suites, edit cases, or manage a suite outside a repository, use the
SDK’s ID-addressed create, read, update, and delete (CRUD) methods in the
client.evals.suites.* namespace. These methods use the /v1/eval/suites REST
API.
The following code creates a suite, adds a case, and runs the suite:
Replace YOUR_FLOW_ID with the ID of the Flow to evaluate.
The SDK maps the following methods to REST endpoints:
The acceptProposal method materializes a proposal as a case with
origin: 'generated'. A trace-sourced proposal uses
origin: 'saved_from_run'. The accepted case keeps a provenance backlink. If you
pass an edited body in options.case, the method records the acceptance as
edited_accepted.
The addCaseFromExecution method freezes an Agent conversation at a fork point,
attaches recorded tool results as editable mocks, and saves the case with
origin: 'saved_from_run'. It returns case and replayable fields. A value
of false for replayable means that a tool output was truncated during
capture. The capture does not run the Agent again.
A suite’s target is immutable. Create another suite to evaluate a different Flow or Agent.
Replay mode
By default, a captured saved_from_run case grades only the Agent’s next action.
The default is recordedToolMode: 'next_step'.
During replay, the first tool call is recorded as intent in the trace with its name and arguments, but it is not executed. A text reply is graded as the output. Use a captured case with an Agent-target suite. A Flow-target suite reports an actionable failure instead of running the case.
Set recordedToolMode to 'continue' on defineEval, suites.create, or
suites.update to serve toolMocks by tool name and continue after the first
action. Graders can then evaluate what the Agent does after a tool result returns.
For example, they can evaluate whether the Agent apologizes after a failed refund.
A replay does not call the real tool. It also works with a toolType: 'local'
tool, which otherwise pauses on its first call while a client resolves it. When
a replayed call has no matching mock, recordedToolUnmatchedPolicy selects the
outcome. Set it to 'fail' for the default failure, or to 'stub' for a canned
tool unavailable success that lets the run continue.
You do not need a captured case to use toolMocks. Add them to a manual case’s
input without a checkpoint:
In this example, validate_product does not run. The mock’s output is served
instead. Edit the output on a captured or hand-authored case to test an
alternative result. For example, set a lookup result to not_found or set
isError to true to test recovery. Continue mode requires an Agent target.
Run a suite by ID
Suites with up to 50 enabled cases run synchronously and return the same scored
shape as POST /v1/eval/run. Larger suites return HTTP 202 with mode: 'batch'
and a runId. The API queues the run and grades it when it completes. Poll
GET /v1/eval/runs/{runId}/scores for the results. Pass mode to force either
path.
Conversation-seeded cases, which include messages in input, run on both
paths. In a batch run, each case keeps its seeded conversation on its own record,
and the judge receives it as it does on a synchronous run.
Replay-mode cases are the exception. Captured cases and cases whose toolMocks
are served with recordedToolMode: 'continue' run only on the synchronous path.
A batch run containing one of these cases is rejected with
REPLAY_MODE_CASES_UNSUPPORTED_ON_BATCH.
Both synchronous and batch runs persist per-case scores in the same run history. Each run counts once against the daily eval limit.
Evaluate a specific Agent version
An Agent-target run can name the version it evaluates instead of taking the saved
configuration. POST /v1/eval/suites/{suiteId}/run, POST /v1/eval/run, and
POST /v1/eval/submit all accept an agent selector carrying exactly one of
alias or versionId:
The MCP run_eval_suite and submit_eval tools expose the same choice as
agent_alias and agent_version_id, and the CLI’s runtype eval run takes
--alias or --version-id.
The selector belongs to the run, not to the suite: a suite is a fixed case set, and which version you grade it against is a run-time choice. The run resolves the selector once, before its first case, so moving the alias mid-run does not split the run across two definitions.
Every run records what it evaluated, and the run reads return it alongside the
scores: agentVersionId, agentTargetResolution (alias, version, or
legacy-live-row when no selector was supplied), agentTargetAlias,
caseManifestHash (SHA-256 over the case set the run used) and
evaluatorFingerprint (SHA-256 over the judges and grading configuration).
Omitting the selector keeps today’s behavior: the run executes the Agent’s saved
configuration and records agentTargetResolution: "legacy-live-row". Selectors
are not available for external or Claude Managed Agents, which answer 422
with code: "selector_unsupported_agent_type", and a Flow-target suite answers
400.
Review generated case proposals
Generated cases do not enter a suite directly. generateCases creates candidates
from the target definition across a category and persona matrix. Categories
include happy path, edge case, ambiguous, adversarial, long input, and formatting
stress. Personas include a frustrated customer and a non-native speaker.
The API filters candidates for gradeability. It reports dropped candidates in
droppedCount and stores the remaining candidates as proposals. Each proposal
stores its source, sourceRefs with the assigned category and persona, and a
one-sentence rationale. Review each proposal with acceptProposal or
rejectProposal. Accept a proposal as written, or pass options.case to save an
edited body. Accepted proposals link to the created case through
acceptedCaseId.
The getCoverage method reports target tools and instruction clauses without an
exercising case or grader. Pass one gap to generateCases, such as
{ gap: { toolName } } or { gap: { instruction } }, to create targeted
proposals with source: 'coverage_gap'. Each generation call reserves one
daily-eval quota slot. A suite accepts at most 50 new proposals per rolling day
across all sources.
Manage a suite from code and the SDK
A suite definition contains name, description, and graders. Both this SDK
namespace and ensure can write the definition. A definition edit in this
namespace sets lastModifiedSource. The dashboard shows the Managed in code
badge when that value is sdk or terraform.
Changing graders invalidates the config-as-code content hash. The next ensure
call then re-converges the repository definition instead of treating it as an
unchanged match. Cases remain server-authoritative data. Adding or editing a case
does not change provenance or the hash, so those cases survive a deploy when the
ensure probe returns unchanged. A repository definition change still replaces
all cases during full convergence.
Writes, including create, update, delete, and case edits, require a credential
with EVALS:WRITE. Reads accept EVALS:READ, EVALS:WRITE, FLOWS:READ, or
FLOWS:EXECUTE. The run endpoint accepts FLOWS:EXECUTE or EVALS:WRITE.
Wire protocol (direct API use)
If you do not use the TypeScript SDK, use two endpoints for the gate, ensure
and run, plus two endpoints for reading and reviewing scores:
POST /v1/eval/ensure: Converge the suite. Send a probe with the suitenameandcontentHash. If the hash misses, the API returns HTTP200withresult: "definitionRequired". Retry with the fulldefinition. A successful response includesresult,suiteId, andcontentHash.POST /v1/eval/run: Run and score the suite. SendsuiteIdto run a saved suite, or senddefinitionto run an inline virtual suite. Addstrict: trueto fail the suite on soft grader misses. Saved-suite runs persist scores by default. Addvirtual: trueto skip persistence. Inline definition runs are always ephemeral. The response includessuiteId,name,targetType,runId,score,passed,totalCases,passedCases, andcases. Each case includesname,passed,outcomes,outputExcerpt, anderrored. An errored case also carrieserror, the provider or execution failure text, so a failing run is diagnosable from the result itself. Each outcome includesseverityasgateorsoft; an absent value meansgate. Mark a grader soft withseverity: "soft"in its configuration.GET /v1/eval/runs/{runId}/scores: Read scores for a persisted run. The response includes the suite score and per-case grader outcomes. It uses the same shape as the run response and includesrunId,suiteId,name,strict,score,totalCases,passedCases, andcases. Each case includescaseId,name,passed,outcomes,outputExcerpt, anderror(the failure text for an errored case, otherwisenull). The API reproduces verdicts under the strict mode used to grade the run. Each persisted outcome also includesscoreIdandhumanVerdict, which can beagree,disagree, ornull. The live run response omits these fields because ephemeral runs have no score rows to review. In the SDK, callclient.evals.getRunScores(runId).POST /v1/eval/scores/{scoreId}/review: Record a human review of an AI-grader verdict. Sendverdictasagree,disagree, ornull;nullclears an earlier review. A check-grader row returns HTTP400because its verdict is deterministic. Reviews contribute to the suite’sjudgeAgreementfraction, whichGET /v1/eval/suites/{id}returns. In the SDK, callclient.evals.reviewScore(scoreId, verdict).
The ensure call requires EVALS:WRITE. The run call accepts FLOWS:EXECUTE
or EVALS:WRITE. The scores read accepts FLOWS:READ, FLOWS:EXECUTE,
EVALS:READ, or EVALS:WRITE. Reviewing a score accepts FLOWS:EXECUTE or
EVALS:WRITE. Each run counts once against the daily eval limit. Reading scores
and reviewing verdicts do not count. A server-side error before the API returns
a result releases the run’s quota slot.
Next steps
Continue with these pages:
- What are Evals?: learn the concepts behind suites, cases, and graders
- Managing eval suites: work with saved suites on the dashboard’s Evals page
- Running an eval: run and compare evals from the dashboard
- Manage Flows as code: converge the Flows that your evals target
- Manage Agents as code: converge the Agents that your evals target