Evaluator configuration
Configure evaluator kinds, exact versions, subjects, sampling, and paid-evaluation budgets.
On this page
Start with your first evaluation for a runnable workflow. See API and permissions for endpoint and credential requirements.
The three pieces
| Piece | What it is |
|---|---|
| Evaluator | A named, versioned rule that turns a run into a number and a label. |
| Sampling rule | Which runs an evaluator sees, at what rate, with what per-tick ceiling. |
| Result | One score, with the full record of what produced it. |
Evaluator versions are exact, and never float
An evaluator has an id and a list of versions. A version is immutable: changing a rule creates a new version and leaves the old one exactly as it was.
Every reference must name an existing version explicitly; there is no "latest" alias. References are validated when written, so a missing version fails the request immediately. Pin the same version when comparing releases.
Creating an evaluator
curl -X POST https://app.anectico.com/api/v1/evaluation/evaluators \
-H "Authorization: Bearer $ANECTICO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"evaluator_id": "task_completion",
"display_name": "Task completion",
"metric_name": "task_completion",
"kind": "EVALUATOR_KIND_DETERMINISTIC",
"config_json": "{\"aggregation\":\"all\",\"assertions\":[{\"field\":\"run.status\",\"op\":\"equals\",\"value\":\"AGENT_RUN_STATUS_COMPLETED\"},{\"field\":\"run.error_steps\",\"op\":\"lte\",\"number\":0}]}"
}'
The response carries the evaluator and its version 1. Append a new version with
POST /api/v1/evaluation/evaluators/{evaluatorId}/versions; there is no update or patch, by design.
Evaluator kinds
EVALUATOR_KIND_DETERMINISTIC — declarative assertions over a run's observable fields.
{
"aggregation": "all",
"assertions": [
{"field": "run.status", "op": "equals", "value": "AGENT_RUN_STATUS_COMPLETED"},
{"field": "run.error_steps", "op": "lte", "number": 0},
{"field": "steps.tool_names", "op": "contains", "value": "search_docs"}
]
}
aggregation is all (a gate: every assertion must hold, the score is 1 or 0) or fraction (the
share of assertions that held).
| Field | Type | Operators |
|---|---|---|
run.status |
one value | equals, not_equals |
run.error_steps, run.step_count, run.tool_calls, run.model_calls, run.duration_ms |
number | lte, gte |
steps.outcomes, steps.tool_names, steps.canonical_operations |
set | contains, not_contains |
session.turn_count, session.run_count, session.duration_ms, session.cost_nanos |
number | lte, gte |
The session.* fields describe a whole conversation and are answerable only for
SUBJECT_KIND_AGENT_SESSION — see What you can name as the subject.
A deterministic evaluator can instead return one native measurement. A measurement config contains
measurement and no assertions or aggregation:
{"measurement":"run.cost_nanos"}
| Measurement | Unit | When it is measurable |
|---|---|---|
run.duration_ms |
milliseconds | The recorded run reports a duration. |
run.cost_nanos |
billionths of a US dollar | The run has client-reported or calculated model cost. A missing or unpriced cost is not zero and produces no score. |
safety.error_steps |
count | The run contains explicitly identified safety-check steps with reported outcomes. No safety check, or an unknown outcome, is not a safe zero and produces no score. |
session.turn_count |
count | The session's turns carry turn ids. A session with none is not a session of nought turns; it produces no score. |
session.run_count |
count | Always measurable for a session that exists. |
session.duration_ms |
milliseconds | The time between the session's first and last recorded activity. A session with one moment of activity measures 0, which is a real answer. |
session.cost_nanos |
billionths of a US dollar | The session's runs reported model cost. Nothing on a session says whether an unpriced conversation was free, so a zero is treated as unmeasured and produces no score. |
Use the assertion form for reliability (for example, completed status and zero error steps), a reference-trajectory or judge evaluator for quality, and the native measurements for safety, latency, and cost. Experiments and release gates treat each as the evaluator's real numeric result; they do not fill missing dimensions with defaults.
EVALUATOR_KIND_REFERENCE_TRAJECTORY — did the agent do the right things in the right order.
{
"step_key": "canonical_operation",
"match": "ordered_subsequence",
"reference": [
"CANONICAL_OPERATION_INVOKE_AGENT",
"CANONICAL_OPERATION_RETRIEVAL",
"CANONICAL_OPERATION_CHAT"
]
}
ordered_subsequence (the default) scores the longest in-order match divided by the reference
length; extra intervening steps do not reduce the score. exact requires an identical sequence.
step_key may be canonical_operation or tool_name.
EVALUATOR_KIND_LLM_JUDGE — a model scores the run against a rubric you write.
Use a judge for criteria such as groundedness or relevance that cannot be expressed as deterministic assertions. It reads the selected evidence and scores it against your rubric.
{
"instructions": "Be strict. Prefer a low score when you are unsure.",
"criteria": [
{"name": "grounded", "description": "every claim in the answer traces to the retrieved context", "weight": 2},
{"name": "addressed", "description": "the answer resolves the question that was asked"}
],
"evidence": ["run_summary", "model_transcript"],
"max_evidence_chars": 12000
}
| Field | Requirement | Meaning |
|---|---|---|
criteria |
required, 1–8 | Each needs a name and a description. weight defaults to 1. |
evidence |
required | What the judge is shown. See below. |
instructions |
optional | A preamble, rendered ahead of the criteria. |
model |
optional | A specific model id. Omit to use the default. |
max_evidence_chars |
optional, 500–40000 | Evidence budget. Defaults to 12000. |
Rubric fields are validated; unknown fields are rejected, not ignored.
The result shape is fixed — one score, one label, one sentence of reasoning — and is not something a rubric sets.
What a judge is shown
evidence is a declaration, and it is the one that decides what leaves your project:
| Selector | What the judge sees | Content |
|---|---|---|
run_summary |
Status, step counts, duration. | — |
step_sequence |
The ordered operations, outcomes and tool names. | — |
model_transcript |
Recorded prompt and response bodies. | model_transcript |
tool_calls |
Recorded tool and MCP call arguments and results. | tool_arguments |
The first two carry no message bodies at all — a tool name is a symbol, not the arguments it was called with — so a rubric built only from them shows a model nothing you recorded.
Your content policy decides whether the evidence may be sent
Sending recorded content to an evaluation model is a transfer, and it is governed by your project's
content policy at the judge_transfer boundary. The policy is
consulted before anything is sent, never after.
Two properties are worth stating exactly, because both are easy to get wrong and neither is visible from the outside:
- The question asked of your policy is derived from what the evidence actually contains, not from
what the rubric might want. A rubric that selects
tool_calls, running against a run that recorded no tool arguments, asks nothing abouttool_arguments— so a project that permits transcripts and refuses tool arguments is evaluated normally on those runs. - But a kind of content your policy actually withheld is never quietly dropped. When the content
your rubric asked for was removed by the policy, the transfer is put to the policy again for
exactly that kind, and a refusal is a refusal for the whole evaluation —
403 Forbidden— even when the rubric's other selectors still had something to show. A judge scoring "was the answer grounded in the transcript" over a run summary alone is scoring something you did not ask about. If the removal was not your policy's decision (a narrower credential, for instance), the evaluation proceeds and the model is told which kinds of content were withheld, by name, so it cannot read an absence as evidence that nothing was there. - A refusal is terminal and it is recorded. The run is not sent, is never scored 0, and is not retried in a loop. It is recorded as withheld, naming the kind of content, the boundary, and which of three things decided: your policy said so, the project has no policy yet, or the policy could not be read. "Nobody judged this" and "nobody sampled this" stay different facts.
The read and judge_transfer cells are independent for automatic quality checks. A run may be
hidden from product reads and still evaluated when judge_transfer allows its content; denying
judge_transfer still prevents the transfer whatever the read cell says.
Why some rules are refused
Evaluator configuration is validated when you write it, and some rules are refused even though they are well-formed. Each refusal names its reason. The reason is always the same one:
A field that nothing fills is not a field you can measure.
A run's operation, outcome and similar values carry a placeholder when your instrumentation did not report them. A rule written against that placeholder does not measure your agent — it measures how completely you are instrumented, and reports the answer as a quality score. Worse, a trajectory written against it scores higher the less instrumented the run is, inverting the metric entirely.
So you cannot assert on OUTCOME_UNKNOWN, on CANONICAL_OPERATION_UNKNOWN, or on fields no
instrumentation populates. If you need a value that is refused, the fix is to emit it — see
Instrumenting LLM and agent calls.
The same principle applies at run time. If a run carries nothing the evaluator can measure, it is recorded as unmeasured and retried later, never scored zero. A zero would say your agent failed; the truth is that nobody measured it. A judge is held to the same rule twice over: a run carrying none of the evidence its rubric selected is not sent to a model at all, and a model that comes back with something unusable is retried rather than recorded as a failing score.
Sampling production traffic
curl -X PUT https://app.anectico.com/api/v1/evaluation/sampling-rules \
-H "Authorization: Bearer $ANECTICO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"evaluator": {"evaluator_id": "task_completion", "version": 1},
"subject_kind": "SUBJECT_KIND_AGENT_RUN",
"sampling_rate": 0.1,
"per_tick_limit": 50,
"enabled": true
}'
sampling_rate(0–1) is a true cost cap. Selection is derived from a stable hash of the run, so the same run gets the same answer every time and the unselected share is never evaluated at all. It is not a per-cycle coin flip, which would eventually select everything.per_tick_limitcaps how many evaluations start per cycle. Zero disables the rule, as doesenabled: falseor a rate of 0.
A rule is evaluated only where you create one. Nothing is evaluated by default.
Evaluating one run on demand
curl -X POST https://app.anectico.com/api/v1/evaluation/evaluate \
-H "Authorization: Bearer $ANECTICO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"evaluator": {"evaluator_id": "task_completion", "version": 1},
"subject_kind": "SUBJECT_KIND_AGENT_RUN",
"subject_ref": "0af7651916cd43dd8448eb211c80319c"
}'
This is the same engine, with sampling skipped — naming a run is selecting it. Asking twice for the
same run and evaluator version returns the stored answer with already_evaluated: true; it does not
re-run and does not spend again.
Set your client's timeout above 30 seconds
The call is synchronous: it reads the subject, and for an LLM-judge evaluator it waits for the model. A judge call routinely takes 10–30 seconds and is allowed up to five minutes. Clients whose default request timeout is 30 seconds — which is most of them — will hang up on a judge that is still working, and the evaluation is then abandoned rather than stored.
Give this one endpoint its own timeout of six minutes — a little above the five the server allows, so that what you read is the server's answer rather than your own client giving up — or use a deterministic evaluator (which answers in under a second) when you need a fast answer.
If the five minutes do run out, you get 504 Gateway Timeout with a message, not a dropped
connection. The anectico CLI already applies its own longer timeout to anectico evals evaluate
and anectico evals experiments run; every other command keeps the short default.
What you can name as the subject
subject_kind says what subject_ref is, and it is also the label the stored result carries. Three
kinds can be evaluated today:
subject_kind |
subject_ref is |
Notes |
|---|---|---|
SUBJECT_KIND_AGENT_RUN |
the run (trace) id | The whole run. This is what sampling rules select. |
SUBJECT_KIND_AGENT_STEP |
"<run_id>/<span_id>" |
One step of a run — the span_id is the one the run graph reports for that step. The score is about that step: the evaluator sees only it, not its siblings. |
SUBJECT_KIND_AGENT_SESSION |
the session key | The whole conversation: every run that shares that session key. Only the session.* fields can score it, and only a rubric that asks for no recorded content can be run over it — see below. |
SUBJECT_KIND_AGENT_TURN is part of the result vocabulary — a stored result can carry it — but
cannot be submitted here: a turn cannot be looked up from its id alone. SUBJECT_KIND_DATASET_ITEM
is a batch subject and is evaluated by running an experiment over a dataset version, not through this
endpoint. Naming either returns 501 Not Implemented. A result is never labelled with a subject
that was not read: an unsupported kind is refused rather than scored from the run behind it.
Which fields an evaluator can assert on depends on what you named, and each family is answerable for its own subject only:
run.*describe the whole run and are answerable forSUBJECT_KIND_AGENT_RUNand for dataset items. Asking one about a step or a session returns400 Bad Request("carries nothing this evaluator can measure") rather than reporting the run's number under another subject's name.steps.*andsafety.error_stepsare answerable for a run and for a step, and for a step subject they describe that step alone.session.*describe a whole conversation and are answerable only forSUBJECT_KIND_AGENT_SESSION. Asking one about a single run of that session returns400 Bad Requestfor the same reason in the other direction — a run's own duration or cost is not the conversation's.
What a judge reads for each subject
A judge that asked for model_transcript or tool_calls has to be shown recorded content, and what
that content is depends on the subject:
- A run — that run's own recorded content.
- A step — the content of the run the step belongs to, with the step itself as the subject the score is filed under.
- A session — nothing. A conversation is a set of runs and has no transcript of its own, and
there is no answer to "show the judge the session" that would not mean choosing runs on your
behalf. Rather than pick one, a content-asking rubric over a session is refused with
400 Bad Requestnaming the kind. A rubric built only fromrun_summaryandstep_sequenceis not refused for content — it reads none — but a session rollup carries neither a run summary nor a step sequence, so it has nothing to show a judge either and answers "carries nothing this evaluator can measure". Score a conversation with thesession.*fields, which read the rollup itself.
| Response | Meaning |
|---|---|
404 Not Found |
Nothing here matches what you named — the run, the step, or the evaluator version. Look the identifier up again; retrying it unchanged will not help. A run that was just produced may not be readable for a few seconds, and the same call succeeds once it is. |
504 Gateway Timeout |
Either the subject could not be read within its own budget, or the whole evaluation ran past the five minutes the call is allowed. This is not a 404: it says the subject could not be read, never that it does not exist, so retry the identifier you already have rather than looking for a new one. No result is stored. A judge that had already started may still have been charged, and the retry is free once a verdict exists — asking again for the same run and evaluator version returns the stored answer. |
501 Not Implemented |
This subject_kind cannot be evaluated on demand. See the table above; retrying will not help. |
400 Bad Request |
The subject_ref cannot name a subject of that kind — a step reference must be "<run_id>/<span_id>". |
409 Conflict |
Already being evaluated. Retry shortly. |
429 Too Many Requests |
The daily evaluation budget is spent. |
400 Bad Request |
The run carries nothing this evaluator can measure. |
403 Forbidden |
Your content policy withholds content your rubric asked for from a judge. The message names the kind of content and the boundary, never the content itself. |
400 Bad Request |
A judge whose rubric asks for recorded content cannot be run over this subject_kind. Today that is a session: a conversation has no single transcript to show, and the answer names the kind rather than reading one of its runs. Score the session with session.* fields, or judge its runs individually. |
400 Bad Request |
Judging is not configured or its credentials were permanently refused. An operator must repair the judge setup before retrying. |
503 Service Unavailable |
Judging is temporarily unavailable because of a timeout, throttle, transport failure, or upstream outage. Retry is appropriate. |
The spend budget
Evaluators that call a model cost money, so the daily ceiling is a hard limit rather than a throttle: it is enforced transactionally, and it does not open up when anything is degraded.
curl -X PUT https://app.anectico.com/api/v1/evaluation/budget \
-H "Authorization: Bearer $ANECTICO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"daily_paid_evaluation_cap": 500}'
GET /api/v1/evaluation/budget returns the cap and today's consumption.
Two things to know:
- An organization with no configured budget runs no paid evaluations. The safe state is the default; you opt in to spending. A judge in an organization with no budget never runs.
- Evaluators that do not call a model never consume budget.
EVALUATOR_KIND_DETERMINISTICandEVALUATOR_KIND_REFERENCE_TRAJECTORYare in that category, so they keep working after a paid budget is exhausted.EVALUATOR_KIND_LLM_JUDGEis not: it is charged one unit per run evaluated, once — a retried delivery of a score that was already produced never charges again.
The budget is charged after your content policy permits the transfer, so a run your policy withholds costs nothing.