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.

FieldTypeDescription
containstextPasses when the target state variable's text contains value (substring).
not_containstextPasses when the target's text does not contain value.
equalstextPasses when the target's text equals value exactly (after case folding, unless case_sensitive).
regextextPasses when re.search(value, target) matches. value is a regular expression; ≤ 200 chars and must compile.
max_cost_usdnumberPasses when the run's total cost_usd is ≤ value.
max_duration_msnumberPasses when the run's duration_ms is ≤ value.
expressionbooleanPasses when the safe expression in value evaluates truthy over the run context. See /docs/expressions.
Text matching is case-insensitive by default; set 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.

FieldTypeDescription
idstringStable identifier. Omit it and the API assigns one (chk_ + 10 hex). Supplying the same id lets you preserve identity across an update.
namestringHuman label, 1–120 characters. Shown on every result. Required for all types.
typeenumOne of the seven check types above.
targetstringThe state variable to inspect. Required for text checks; ignored by numeric and expression checks.
valuestring | numberThe comparison operand: a string for text checks, a number for cost/duration, an expression string for expression checks.
case_sensitivebooleanText 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.

FieldTypeDescription
count≤ 50More than fifty checks → 'too many checks (max 50)'.
shapelist of objectschecks must be a list; each entry must be an object.
namenon-emptyA blank or missing name → 'check N: name is required'.
typeknownAn unrecognized type → "check N: unknown type 'x'".
text targetpresentcontains / not_contains / equals / regex need a target → 'needs a target state variable'.
text valuenon-emptyText checks need a value (not null or empty).
regexcompiles, ≤ 200The pattern must compile and be ≤ 200 characters, else 'invalid regex (…)' or 'regex too long (max 200)'.
numeric valuefloat-ablemax_cost_usd / max_duration_ms need a numeric value, else 'needs a numeric value'.
expressionparsesThe expression must be non-empty and pass the whitelist walk, else 'invalid expression (…)'.
A rejected batch (400)
{
  "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:

FieldTypeDescription
<state vars>anyEvery public run-state variable — agent output_keys, tool results, and anything upstream nodes wrote. These are the targets text checks name.
cost_usdfloatThe 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_msintThe run's wall-clock duration in milliseconds. What max_duration_ms compares against.
inputobjectThe 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

PUT/api/workflows/{id}/checks

Replace 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.

Request
{
  "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" }
  ]
}
Response — the workflow, checks now carrying ids
{
  "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).

GET/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.

evals on the run
{
  "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" }
    ]
  }
}
FieldTypeDescription
evals.passedintHow many checks passed.
evals.totalintHow many checks ran (equals the check count at completion).
results[].idstringThe check's stable id.
results[].namestringThe check's label, copied from its definition.
results[].typestringThe check type that produced this result.
results[].passedboolWhether the assertion held.
results[].detailstringA 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:

FieldTypeDescription
containstext'target' contains 'value' · '…' does not contain '…' when it fails.
not_containstext'target' does not contain 'value' · '…' contains '…' when it fails.
equalstext'target' equals 'value' · '…' does not equal '…' when it fails.
regextext'target' matches /value/ · '…' does not match /…/ when it fails.
max_cost_usdnumbercost_usd 0.0035 <= 0.05 · cost_usd 0.12 > 0.05 when it fails.
max_duration_msnumberduration_ms 4210 <= 8000 · duration_ms 9300 > 8000 when it fails.
expressionbooleanexpression held · expression did not hold · expression error: <message> when it raises.
missing targettextAppends (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.

GET/api/metrics/overview

Dashboard 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.

Response (excerpt)
{
  "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.

Python
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")
TypeScript
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.":

contains / not_contains
{ "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":

equals
{ "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":

regex
{ "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:

numeric
{ "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:

expression
{ "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 reached success. 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.

PUT/api/workflows/{id}/eval-gate

Set the release gate thresholds. Any omitted threshold is not enforced; an empty body clears the gate.

FieldTypeDescription
dataset_idstringThe evaluation dataset the gate is scored against (optional metadata for the studio).
min_pass_ratenumberMinimum check pass-rate (%) the latest evaluation of this version must meet.
min_expected_match_ratenumberMinimum expected-output match rate (%) required.
max_avg_cost_usdnumberMaximum average cost per case allowed.
A blocked release
$ 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" }
The gate reads the latest completed evaluation for the workflow's current version, so run an experiment against the candidate before releasing it. With no gate configured, releases behave exactly as before — this is purely additive.