How it works
A CI job starts a test suite, stores the batch id from the response, then polls that batch until every run is finished. The CLI, the REST API and the SDKs all start the same run.From the CLI
--wait polls until the batch is complete and exits non-zero when a run failed, which is what fails the job. The test suite is named by its id or by its name.
Add --format json (or -o json) to get one final document on stdout instead of the progress lines. It contains outcome, tallies and the per-run results, so a job step can read the verdict without parsing prose.
From the REST API
POST /api/v1/test-suites/{id}/run schedules one run for each scenario of the test suite, against each target you name:
targets and, when you want them, name, repeatCount, simulatorModel, judgeModel, parameters, note and idempotencyKey. Without name, the run goes under the run plan named after the test suite and the target, so every run of the same test suite against the same agent joins one history. Repeat the same idempotencyKey to make a retried job join the first run instead of starting a second one.
The response has the values the CI job needs:
batchRunId identifies the batch. jobCount is the number of runs the batch gets. created says whether this run created the plan or joined one that already had the name.
To run a configuration of your own instead of a whole test suite, POST /api/v1/run-plans/run takes a config object with the scope, the targets, the repeat count and the two models. The scope is one of { "mode": "all" }, { "mode": "test_suites", "testSuiteIds": [...] }, { "mode": "labels", "labels": [...] }, or { "mode": "scenarios" } with scenarioIds beside it. GET /api/v1/run-plans lists the plans, and POST /api/v1/run-plans/{id}/run runs one again with its stored configuration.
The
/api/suites family still answers and is deprecated. Move CI jobs to /api/v1/test-suites and /api/v1/run-plans.From Python
langwatch.run_plans.run(...) starts a plan of your own, with scope, targets, repeat_count, simulator_model and judge_model. scope="test_suites" takes test_suite_ids=[...], and scope="scenarios" takes scenario_ids=[...].
From TypeScript
langwatch.runPlans.run({ ... }) takes the same body as POST /api/v1/run-plans/run.
Compare two agents, or one agent on two settings
A run goes against every target you name, so naming two targets runs each scenario against both and stores the results under one batch, one column per target on the results page. A target can also have its own parameter values, so the same agent named twice with different values is a comparison of that agent on two settings. Compare agents covers how the platform shows them.? or & must be written as %3F or %26. A value is read as the type it looks like: true and false become booleans, a plain number becomes a number, everything else stays text.
On the REST API the same values go in runParameters on the target:
runParameters are merged over the run-level parameters, and the target wins. Use parameters for what every target shares, such as a fixture id or a tenant, and runParameters for what differs between the targets.
Poll the batch
GET /api/simulation-runs/batches/{batchRunId} answers with the counts of one batch:
Stop the poll when
isComplete is true and totalCount is at least jobCount. The two conditions go together: the platform creates the runs asynchronously, so a batch read right after the trigger can have fewer runs than jobCount, and a batch of two created runs out of six reports itself complete.
The same asynchronous creation makes the first read answer 404. The batch becomes readable when its first run is stored, which is a moment after the trigger returns. Treat a 404 as “not stored yet” and poll again until your own timeout expires. A 404 is a missing batch only after that timeout.
Every other status stops the job at once. A wrong token answers 401 and a failed read answers 5xx, and a loop that polls through them reports a batch timeout thirty minutes later for a fault that was clear on the first read.
Exit the CI job nonzero when failCount is above zero or stalledCount is above zero. A stalled run settles without passing, so it counts in settledCount but not in failCount, and a job that reads failCount alone reports success for a batch that never finished its work. Open the batch in the platform for the run details.
langwatch test-suite run ... --wait does the same poll, and its exit code follows the results.
Give the batch a note
--note stores one short line with the batch: why it was run, or what changed. Every run in the batch has it, and the platform shows it beside the run.
cut above trims a long commit subject to that limit. langwatch simulation-run list and langwatch simulation-run get show the note back.