> ## Documentation Index
> Fetch the complete documentation index at: https://langwatch.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> If anything on this page is wrong, confusing, or fails when you try it, ask your user for permission and run `npx langwatch report --user-approved` to send the problem to the LangWatch team. No login is needed and secrets are redacted locally. `npx langwatch report --help` has the details.

# Run Test Suites from CI

> Start a test suite from a CI job and wait for every run of the batch to finish.

## How it works

A CI job starts a test suite, keeps 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

```bash theme={null}
langwatch test-suite run "Checkout regression" --target http:agent_abc123 --wait
```

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

## 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:

```bash theme={null}
curl -X POST "https://app.langwatch.ai/api/v1/test-suites/suite_abc123/run" \
  -H "X-Auth-Token: ${LANGWATCH_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "targets": [{ "type": "http", "referenceId": "agent_abc123" }],
    "note": "checkout rewrite"
  }'
```

The body takes `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 carries the values the CI job needs:

```json theme={null}
{
  "scheduled": true,
  "batchRunId": "batch_xyz789",
  "setId": "set_abc123",
  "jobCount": 2,
  "runPlanId": "plan_abc123",
  "planName": "Checkout regression Support Agent",
  "created": false,
  "platformUrl": "https://app.langwatch.ai/my-project/agent-testing/results/checkout-regression-support-agent",
  "skippedArchived": { "scenarios": [], "targets": [] },
  "items": [
    {
      "scenarioRunId": "run_1",
      "scenarioId": "scn_1",
      "target": { "type": "http", "referenceId": "agent_abc123" },
      "name": "Cancel on the free plan"
    },
    {
      "scenarioRunId": "run_2",
      "scenarioId": "scn_2",
      "target": { "type": "http", "referenceId": "agent_abc123" },
      "name": "Cancel on the enterprise plan"
    }
  ]
}
```

`batchRunId` addresses the batch. `jobCount` is the number of runs the batch gets. `created` says whether this run created the plan or joined one that already carried 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 the configuration it holds.

<Note>
  The `/api/suites` family still answers and is deprecated. Move CI jobs to `/api/v1/test-suites` and `/api/v1/run-plans`.
</Note>

## From Python

```python theme={null}
import langwatch

langwatch.setup()

result = langwatch.test_suites.run(
    "suite_abc123",
    targets=[{"type": "http", "referenceId": "agent_abc123"}],
    note="checkout rewrite",
)
print(result["batchRunId"])
```

`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

```typescript theme={null}
import { LangWatch } from "langwatch";

const langwatch = new LangWatch();

const result = await langwatch.testSuites.run("suite_abc123", {
  targets: [{ type: "http", referenceId: "agent_abc123" }],
  note: "checkout rewrite",
});
console.log(result.batchRunId);
```

`langwatch.runPlans.run({ ... })` takes the same body as `POST /api/v1/run-plans/run`.

## Poll the batch

`GET /api/simulation-runs/batches/{batchRunId}` answers with the counts of one batch:

```bash theme={null}
curl "https://app.langwatch.ai/api/simulation-runs/batches/batch_xyz789" \
  -H "X-Auth-Token: ${LANGWATCH_API_KEY}"
```

```json theme={null}
{
  "batchRunId": "batch_xyz789",
  "totalCount": 2,
  "passCount": 1,
  "failCount": 0,
  "runningCount": 1,
  "settledCount": 1,
  "stalledCount": 0,
  "lastRunAt": 1755691200000,
  "lastUpdatedAt": 1755691260000,
  "firstCompletedAt": 1755691230000,
  "allCompletedAt": null,
  "isComplete": false
}
```

| Field          | Meaning                                                                                      |
| -------------- | -------------------------------------------------------------------------------------------- |
| `totalCount`   | Runs the batch holds now.                                                                    |
| `runningCount` | Runs that are queued or in progress.                                                         |
| `settledCount` | Runs that are neither queued nor in progress: passed, failed, errored, cancelled or stalled. |
| `isComplete`   | True when `settledCount` equals `totalCount`.                                                |

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

```bash theme={null}
# Poll until the batch is complete, then set the exit code.
deadline=$(( $(date +%s) + 1800 ))
while [ "$(date +%s)" -lt "$deadline" ]; do
  body=$(curl -s -w '\n%{http_code}' --connect-timeout 10 --max-time 30 \
    "https://app.langwatch.ai/api/simulation-runs/batches/${BATCH_RUN_ID}" \
    -H "X-Auth-Token: ${LANGWATCH_API_KEY}")
  status=$(printf '%s' "$body" | tail -n1)
  summary=$(printf '%s' "$body" | sed '$d')

  case "$status" in
    200)
      if [ "$(printf '%s' "$summary" | jq '.isComplete')" = "true" ] \
        && [ "$(printf '%s' "$summary" | jq ".totalCount >= ${JOB_COUNT}")" = "true" ]; then
        printf '%s' "$summary" | jq -e '.failCount == 0 and .stalledCount == 0'
        exit $?
      fi
      ;;
    # 404 is a batch whose first run is not stored yet, 000 is a request that
    # did not complete. Both are read again.
    404|000) ;;
    *)
      echo "The batch read answered ${status}: ${summary}"
      exit 1
      ;;
  esac
  sleep 10
done

echo "The batch did not finish before the timeout."
exit 1
```

The request carries its own time limits, so a read that hangs cannot hold the job open. The deadline is read between requests, so the job stops within one request and one sleep of it, which is 40 seconds with the values above.

`langwatch test-suite run ... --wait` does the same poll, and its exit code follows the results.

## Give the batch a note

`--note` keeps one short line with the batch: why it was run, or what changed. Every run in the batch carries it, and the platform shows it beside the run.

```bash theme={null}
langwatch test-suite run suite_abc123 --target http:agent_abc123 \
  --note "$(git log -1 --pretty=%s | cut -c1-200)" --wait
```

The note is free text, up to 200 characters, and a longer one is refused before the batch is queued. The `cut` above keeps a long commit subject inside that limit. `langwatch simulation-run list` and `langwatch simulation-run get` show the note back.
