---
title: "Create several eval cases"
method: POST
path: "/projects/{projectId}/eval-suites/{suiteId}/cases/batch"
tags: ["Eval runs"]
---

# Create several eval cases

`POST /projects/{projectId}/eval-suites/{suiteId}/cases/batch`

Adds up to 100 cases to the suite in one call — the bulk form of the single create, and the way to import a suite or convert a repo's test files without a round trip per case. Every entry needs a `title` and a non-empty `steps` array. Cases are validated together and reported individually: a `failed` entry does not roll back its committed siblings, so the response is `201` even when some entries were refused — read `failed` rather than branching on the status alone. Send an `Idempotency-Key` header to make a retry land on the same rows.

## Path parameters

- `projectId` string, required
- `suiteId` string, required

## Headers

- `Idempotency-Key` string

## Request body

- EvalCasesBatchCreateRequest — Author several cases in one call. Each entry takes the same fields as a single create. The 100-case cap is deliberately smaller than a suite file's 500-case limit, so a maximal file uploads in several calls.
  - `cases` EvalCaseCreateRequest[], required
    - `title` string, required
    - `id` string — Stable declared id for the case — the id it answers to in a suite file, an import or a CLI argument. Minted for you when omitted, and unique within the suite. Distinct from the `id` on the response, which is the case's platform row id.
    - `steps` EvalTestStep[], required — REPLACES the case's test definition wholesale when provided.
      - `id` string, required
      - `kind` 'prompt' | 'toolCall' | 'interact' | 'assert', required
      - `prompt` string — User message (`kind: prompt`).
      - `serverName` string — Server that owns the tool (`kind: toolCall`).
      - `toolName` string — Tool name (`kind: toolCall` / `interact`).
      - `arguments` object — Tool-call arguments (`kind: toolCall`).
      - `action` object — Widget action (`kind: interact`).
      - `assertion` object — Predicate or widget assertion (`kind: assert`).
    - `expectedOutput` string
    - `iterations` integer
    - `isNegative` boolean
    - `scenario` string
    - `models` object[]
      - `model` string, required
      - `provider` string — Derived from a `provider/model` id when omitted.
    - `matchOptions` object, nullable — `null` clears the case override and inherits the suite's.
    - `checks` object, nullable — `null` clears the case override.
      - `mode` 'inherit' | 'replace' | 'extend'
      - `list` object[]
  - `duplicatePolicy` 'block' | 'warn' | 'create_anyway' — What to do with a case whose definition already matches one in the suite. An unrecognized value coerces to `block` and reports the coercion in the response's `duplicatePolicy` rather than failing the call.
  - `overrideReason` string — Required by `warn` and `create_anyway`. Recorded on the case's revision.

## Response `201`

Per-case outcomes. Some entries may have failed; see `failed`.

- EvalCasesBatchResult — A partial outcome by design: the cases in `created` were written even when `failed` is non-empty. Both arrays carry the `index` of the request entry they describe.
  - `created` object[], required
    - `index` integer, required — Position in the request's `cases` array.
    - `id` string, required — Platform case id — the path parameter for the per-case routes.
    - `declaredId` string — The effective declared id. On a replay this is the stored case's, not the one just sent.
    - `title` string, required
    - `replayed` boolean, required — True when an idempotent retry landed on an already-authored case; nothing new was written.
    - `warnings` EvalCaseWarning[]
      - `code` string, required
      - `message` string, required
  - `failed` object[], required
    - `index` integer, required
    - `title` string
    - `declaredId` string
    - `code` 'INVALID_ITEM' | 'INVALID_CASE_ID' | 'DUPLICATE_CASE_ID' | 'DUPLICATE_IDEMPOTENCY_KEY' | 'IDEMPOTENCY_CONFLICT' | 'DUPLICATE_CONTENT' | 'OVERRIDE_REASON_REQUIRED' | 'INVALID_CASE' | 'UNSTORABLE_ITEM' | 'ITEM_TOO_LARGE', required — Stable machine-readable reason.
    - `message` string, required
  - `duplicatePolicy` object, required — What policy actually applied, so a coercion is never silent.
    - `requestedPolicy` string
    - `effectivePolicy` string, required
    - `coerced` boolean, required
  - `warnings` EvalCaseWarning[]
    - `code` string, required
    - `message` string, required

## Other responses

- `400` — Malformed body or parameters.
- `401` — Missing, invalid, revoked, or orphaned key (`UNAUTHORIZED`) — or the **target MCP server** needs an OAuth grant (`OAUTH_REQUIRED`), which is a property of the server, not your key.
- `403` — Key is valid but not allowed to do this.
- `404` — Unknown project, server, or resource.
- `429` — Per-key rate limit exceeded (60 requests/minute sustained, bursts up to 10). Honor `Retry-After` and back off with jitter.
- `500` — Something failed on MCPJam's side.
- `502` — Could not connect to the target MCP server.

---

[API](https://skmtc.net/mcpjam/apis/mcpjam-api.md) · [All operations](https://skmtc.net/mcpjam/apis/mcpjam-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/mcpjam/mcpjam-api/revisions/548954252807/schema)
