Reference
Evaluations
A check is an assertion you attach to a workflow. When a run completes successfully the engine scores every check against the run's final state, cost, and duration, stores the pass/fail results on the run, and rolls a fleet-wide pass rate up onto the dashboard. Checks describe quality; they never change whether a run passed.
Checks turn “did the run finish?” into “did the run do the right thing?” A workflow carries a list of up to fifty checks. Each targets a state variable or a run metric and asserts something about it — the reply mentions the refund, the run stayed under five cents, the classifier output matches a known set. Scoring is automatic, additive, and completely detached from execution: it happens after a run has already succeeded, and a broken assertion becomes a failed result, not a failed run.
The seven check types
Checks fall into three families: text checks compare a state variable against a string, numeric checks compare a run metric against a number, and the expression check runs the same safe language as condition nodes.
| Field | Type | Default | Description |
|---|---|---|---|
| contains | text | target + value | Passes when the target state variable's text contains value (substring). |
| not_contains | text | target + value | Passes when the target's text does not contain value. |
| equals | text | target + value | Passes when the target's text equals value exactly (after case folding, unless case_sensitive). |
| regex | text | target + value | Passes when re.search(value, target) matches. value is a regular expression; ≤ 200 chars and must compile. |
| max_cost_usd | number | value | Passes when the run's total cost_usd is ≤ value. |
| max_duration_ms | number | value | Passes when the run's duration_ms is ≤ value. |
| expression | boolean | value | Passes when the safe expression in value evaluates truthy over the run context. See /docs/expressions. |
case_sensitive: true to compare exactly. For regex, case-insensitivity is applied as the re.IGNORECASEflag, and the pattern is searched against the target's original text (not the folded copy the other text types use).The check object
A check is a small JSON object. Only name and type are always required; which of target and value matter depends on the type.
| Field | Type | Default | Description |
|---|---|---|---|
| id | string | auto | Stable identifier. Omit it and the API assigns one (chk_ + 10 hex). Supplying the same id lets you preserve identity across an update. |
| name | string | required | Human label, 1–120 characters. Shown on every result. Required for all types. |
| type | enum | required | One of the seven check types above. |
| target | string | "" | The state variable to inspect. Required for text checks; ignored by numeric and expression checks. |
| value | string | number | "" | The comparison operand: a string for text checks, a number for cost/duration, an expression string for expression checks. |
| case_sensitive | boolean | false | Text checks only. When false, both sides are lower-cased (or IGNORECASE is set) before comparing. |
Validation rules
Checks are validated structurally before they are stored. Every problem found is reported at once, so a bad batch comes back with a list of reasons rather than the first failure. A workflow with any invalid check is rejected with 400 and none of the batch is saved.
| Field | Type | Default | Description |
|---|---|---|---|
| count | ≤ 50 | MAX_CHECKS | More than fifty checks → 'too many checks (max 50)'. |
| shape | list of objects | — | checks must be a list; each entry must be an object. |
| name | non-empty | — | A blank or missing name → 'check N: name is required'. |
| type | known | — | An unrecognized type → "check N: unknown type 'x'". |
| text target | present | — | contains / not_contains / equals / regex need a target → 'needs a target state variable'. |
| text value | non-empty | — | Text checks need a value (not null or empty). |
| regex | compiles, ≤ 200 | MAX_REGEX_LENGTH | The pattern must compile and be ≤ 200 characters, else 'invalid regex (…)' or 'regex too long (max 200)'. |
| numeric value | float-able | — | max_cost_usd / max_duration_ms need a numeric value, else 'needs a numeric value'. |
| expression | parses | — | The expression must be non-empty and pass the whitelist walk, else 'invalid expression (…)'. |
{
"detail": "Invalid checks: check 1: name is required; check 2: unknown type 'looks_good'; check 3: 'contains' needs a target state variable; check 4: invalid regex (missing ), unterminated subpattern at position 0); check 5: 'max_cost_usd' needs a numeric value; check 6: invalid expression (Function 'open' is not allowed)"
}
The evaluation context
When a run succeeds, the engine assembles a single context dictionary and evaluates every check against it. It contains:
| Field | Type | Default | Description |
|---|---|---|---|
| <state vars> | any | — | Every public run-state variable — agent output_keys, tool results, and anything upstream nodes wrote. These are the targets text checks name. |
| cost_usd | float | 0.0 | The run's total cost in USD, summed across every node. The metric max_cost_usd compares against; also readable by name in an expression. |
| duration_ms | int | 0 | The run's wall-clock duration in milliseconds. What max_duration_ms compares against. |
| input | object | {} | The original run input object, so a check can assert on what came in as well as what went out. |
A text check whose targetis not in the context does not error — it reads as an empty string, usually fails, and the detail is annotated (no 'target' in output) so you can tell a genuine miss from a typo. Inside an expression check, all of the above are available as names, plus the state alias — see the expression language.
Setting checks
/api/workflows/{id}/checksReplace the workflow's entire check list. The body is validated as a batch; any error returns 400 and stores nothing. On success every check is assigned a stable id and the full workflow is returned. This does not create a new graph version.
{
"checks": [
{ "name": "Mentions the refund", "type": "contains",
"target": "draft_reply", "value": "refund" },
{ "name": "No internal error leaked", "type": "not_contains",
"target": "draft_reply", "value": "Traceback" },
{ "name": "Category is known", "type": "regex",
"target": "category", "value": "^(billing|refund|technical)$" },
{ "name": "Under budget", "type": "max_cost_usd", "value": 0.05 },
{ "name": "Under 8 seconds", "type": "max_duration_ms", "value": 8000 },
{ "name": "Reply is substantial", "type": "expression",
"value": "len(draft_reply) > 40" }
]
}
{
"id": "9bc62f209c52",
"name": "Support Triage Crew",
"version": 2,
"checks": [
{ "id": "chk_a1b2c3d4e5", "name": "Mentions the refund", "type": "contains",
"target": "draft_reply", "value": "refund", "case_sensitive": false },
{ "id": "chk_f6a7b8c9d0", "name": "Under budget", "type": "max_cost_usd",
"target": "", "value": 0.05, "case_sensitive": false }
// …the rest, each with a chk_ id
]
}
Replace, not patch
PUT /checkssets the complete list — send every check you want to keep on each call. To preserve a check's identity across edits, echo back its id; omitting it mints a fresh chk_ id. Editing checks does not touch the graph or bump the workflow version, and it is audit-logged as update_checks.Reading results
A successful run whose workflow has checks carries an evalssummary. Runs with no checks — and every failed run — omit it entirely (evals is null).
/api/runs/{id}Full run detail, including the evals summary when the workflow had checks at completion time. The summary counts passes and carries a per-check result.
{
"id": "af772b3602ef",
"status": "success",
"cost_usd": 0.0035,
"duration_ms": 4210,
"evals": {
"passed": 4,
"total": 6,
"results": [
{ "id": "chk_a1b2c3d4e5", "name": "Mentions the refund", "type": "contains",
"passed": true, "detail": "'draft_reply' contains 'refund'" },
{ "id": "chk_11223344ff", "name": "No internal error leaked", "type": "not_contains",
"passed": true, "detail": "'draft_reply' does not contain 'Traceback'" },
{ "id": "chk_55667788ee", "name": "Category is known", "type": "regex",
"passed": true, "detail": "'category' matches /^(billing|refund|technical)$/" },
{ "id": "chk_f6a7b8c9d0", "name": "Under budget", "type": "max_cost_usd",
"passed": true, "detail": "cost_usd 0.0035 <= 0.05" },
{ "id": "chk_99aabbccdd", "name": "Under 8 seconds", "type": "max_duration_ms",
"passed": true, "detail": "duration_ms 4210 <= 8000" },
{ "id": "chk_00ffeeddcc", "name": "Reply is substantial", "type": "expression",
"passed": false, "detail": "expression did not hold" }
]
}
}
| Field | Type | Default | Description |
|---|---|---|---|
| evals.passed | int | — | How many checks passed. |
| evals.total | int | — | How many checks ran (equals the check count at completion). |
| results[].id | string | — | The check's stable id. |
| results[].name | string | — | The check's label, copied from its definition. |
| results[].type | string | — | The check type that produced this result. |
| results[].passed | bool | — | Whether the assertion held. |
| results[].detail | string | — | A human-readable explanation — see the detail formats below. |
The run_evaluatedevent is also pushed on the run's WebSocket stream with passed and total the moment scoring finishes — see Events & streaming.
Detail strings, per type
Each result's detail is generated by the check and states exactly what was compared. These are the formats:
| Field | Type | Default | Description |
|---|---|---|---|
| contains | text | — | 'target' contains 'value' · '…' does not contain '…' when it fails. |
| not_contains | text | — | 'target' does not contain 'value' · '…' contains '…' when it fails. |
| equals | text | — | 'target' equals 'value' · '…' does not equal '…' when it fails. |
| regex | text | — | 'target' matches /value/ · '…' does not match /…/ when it fails. |
| max_cost_usd | number | — | cost_usd 0.0035 <= 0.05 · cost_usd 0.12 > 0.05 when it fails. |
| max_duration_ms | number | — | duration_ms 4210 <= 8000 · duration_ms 9300 > 8000 when it fails. |
| expression | boolean | — | expression held · expression did not hold · expression error: <message> when it raises. |
| missing target | text | — | Appends (no 'target' in output) when the named state variable is absent. |
Fleet-wide pass rate
Individual results roll up into a single quality signal across the whole workspace. The dashboard's metrics endpoint counts every check on every evaluated run.
/api/metrics/overviewDashboard aggregates, including eval_pass_rate (percentage of all checks that passed across all evaluated runs) and evaluated_runs (how many runs carried checks). eval_pass_rate is null when nothing has been evaluated yet.
{
"total_runs": 412,
"success_rate": 96.8,
"total_cost_usd": 1.84213,
"active_workflows": 5,
"runs_by_day": [ { "date": "2026-07-07", "count": 34, "cost_usd": 0.1521 } ],
"eval_pass_rate": 91.4,
"evaluated_runs": 288
}
eval_pass_rate is a percentage of checks, not of runs: if 288 runs ran 6 checks each and 158 of those checks failed, the rate is over checks, so one flaky assertion on many runs weighs the same as a total failure on one. Use it to spot regressions in aggregate; use each run's evals to see which assertion moved.Setting checks from the SDK
Both SDKs resolve a workflow by id or exact name and replace its check list, returning the updated workflow. Results then arrive on each run as evals.
from agentos_sdk import AgentOS
client = AgentOS("http://localhost:8000", api_key="aos_live_…")
client.set_checks("Support Triage Crew", [
{"name": "mentions refund", "type": "contains",
"target": "draft_reply", "value": "refund"},
{"name": "under budget", "type": "max_cost_usd", "value": 0.05},
{"name": "no error leaked", "type": "expression",
"value": "'error' not in draft_reply.lower()"},
])
run = client.run("Support Triage Crew", input={"ticket": "double charged"})
print(run["evals"]["passed"], "of", run["evals"]["total"], "checks passed")
import { AgentOS } from "@agentos/sdk";
const client = new AgentOS({ baseUrl: "http://localhost:8000", apiKey: "aos_live_…" });
await client.setChecks("Support Triage Crew", [
{ name: "mentions refund", type: "contains", target: "draft_reply", value: "refund" },
{ name: "under budget", type: "max_cost_usd", value: 0.05 },
{ name: "no error leaked", type: "expression",
value: "'error' not in draft_reply.lower()" },
]);
One worked example per type
contains & not_contains
Both fold case unless case_sensitive is set. Given draft_reply = "We'll process your refund today.":
{ "name": "mentions refund", "type": "contains",
"target": "draft_reply", "value": "refund" }
// → passed: true, detail: "'draft_reply' contains 'refund'"
{ "name": "no stack trace", "type": "not_contains",
"target": "draft_reply", "value": "Traceback" }
// → passed: true, detail: "'draft_reply' does not contain 'Traceback'"
equals
Exact match after case folding. Given category = "Billing":
{ "name": "routed to billing", "type": "equals",
"target": "category", "value": "billing" }
// → passed: true, detail: "'category' equals 'billing'" (case-insensitive)
{ "name": "routed to billing (strict)", "type": "equals",
"target": "category", "value": "billing", "case_sensitive": true }
// → passed: false, detail: "'category' does not equal 'billing'"
regex
The value is a regular expression, searched against the target. Given order_id = "GB204815":
{ "name": "well-formed order id", "type": "regex",
"target": "order_id", "value": "^[A-Z]{2}[0-9]{6}$" }
// → passed: true, detail: "'order_id' matches /^[A-Z]{2}[0-9]{6}$/"
max_cost_usd & max_duration_ms
These ignore target and read the run metric. Given cost_usd = 0.0035 and duration_ms = 4210:
{ "name": "under 5 cents", "type": "max_cost_usd", "value": 0.05 }
// → passed: true, detail: "cost_usd 0.0035 <= 0.05"
{ "name": "under 8 seconds", "type": "max_duration_ms", "value": 8000 }
// → passed: true, detail: "duration_ms 4210 <= 8000"
expression
A boolean over the full context — state variables plus cost_usd, duration_ms, and input. It composes conditions no single typed check can:
{ "name": "substantial reply, on budget", "type": "expression",
"value": "len(draft_reply) > 40 and cost_usd <= 0.05" }
// → passed: true, detail: "expression held"
{ "name": "echoes requested locale", "type": "expression",
"value": "input.get('locale') == 'en-US'" }
// → passed: false, detail: "expression did not hold"
// a check that raises never breaks the run — it just fails:
{ "name": "typo'd variable", "type": "expression", "value": "reploy != ''" }
// → passed: false, detail: "expression error: Unknown variable 'reploy'"
Checks run only on success, and never break a run
Evaluation happens after a run has already reachedsuccess. A failed, cancelled, or still-paused run is never scored, so a run that failed halfway never carries evals. And because every check is caught internally — an unknown type, a raising expression, a method on the wrong kind of value — a broken assertion becomes a passed: false result with an explanatory detail. Checks measure quality; they cannot make a successful run fail.Blocking a deploy on a regression
Checks and experiments tell you a candidate got worse; an evaluation gate stops you from shipping it. Attach thresholds to a workflow and Ballast enforces them server-side at release time: when you cut a release, it evaluates the most recent completed evaluation for that workflow version against the gate and blocks the deploy with 422 if it regressed past your bar. An eval regression now preventsa production deploy — it isn't just a dashboard warning a human can miss.
/api/workflows/{id}/eval-gateSet the release gate thresholds. Any omitted threshold is not enforced; an empty body clears the gate.
| Field | Type | Default | Description |
|---|---|---|---|
| dataset_id | string | none | The evaluation dataset the gate is scored against (optional metadata for the studio). |
| min_pass_rate | number | none | Minimum check pass-rate (%) the latest evaluation of this version must meet. |
| min_expected_match_rate | number | none | Minimum expected-output match rate (%) required. |
| max_avg_cost_usd | number | none | Maximum average cost per case allowed. |
$ curl -X POST ".../api/workflows/9bc62f209c52/release" \
-d '{"environment": "production"}'
HTTP/1.1 422 Unprocessable Entity
{ "detail": "eval gate: check pass-rate 41.0% is below the required 90.0% — deployment blocked" }