# Draft a test from a conversation (/docs/api/voice-agents/agent-tests/draftAgentTestFromConversation)

> For the complete documentation index, see [llms.txt](https://docs.tandemcx.ai/llms.txt). Any page on this site can be fetched as Markdown by appending `.md` to its URL.

`POST /v1/agent-media/agents/{agentId}/tests/draft-from-conversation`

Draft a test from a conversation

Build an unsaved scripted test from a finished call or chat this agent handled. The draft is deterministic and uses no AI: its turns are what the caller said, each tool is stubbed with the last result it returned, and its checks are that each tool was called and that the conversation ended with its disposition. Nothing is saved; send the `test` to the create operation to keep it.

A chat's stored history holds only its last 60 messages, so a draft from a long chat starts partway through. `truncated` is true when the draft was cut to fit the test limits (40 turns, 20 checks, 4000 characters per turn) or to stay under 80 KB, which drops trailing turns first.

## Request

**Base URL:** `https://api.tandemcx.ai`

**Authentication:** `Authorization: Bearer <token>` — A Tandem bearer token. Requests are scoped to the organization the token belongs to; no organization header is required.

### Path parameters

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `agentId` | string | Yes | The agent's unique ID. |

### Body (`application/json`) — required

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `conversationId` | string | Yes | The call's or chat's conversation id, as Call Logs shows it. |

## Responses

### 200 — The draft test.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `test` | object | Yes | A test definition. `scripted` tests need `scripted` and no `simulated`; `simulated` tests need `simulated` and no `scripted`. |
| `test.name` | string | Yes | — |
| `test.description` | string | No | — |
| `test.mode` | enum: `scripted` \| `simulated` | Yes | `scripted` sends fixed caller messages in order. `simulated` has a model play the caller from a persona and a goal. |
| `test.scripted` | object | No | — |
| `test.scripted.turns` | array of one of: string \| object | Yes | The caller's messages and keypad entries, in order. A keypad entry right after a message is what the caller types when the agent's reply calls `capture_dtmf`; one no capture takes is sent as its own turn, the way a call delivers keypresses with Keypad Input on. |
| `test.simulated` | object | No | — |
| `test.simulated.persona` | string | Yes | — |
| `test.simulated.goal` | string | Yes | — |
| `test.simulated.maxTurns` | integer | No | Agent turns before the case ends with `max_turns`. |
| `test.stubs` | object | No | Keyed by tool name. A tool is stubbed by default: with no entry it returns `{}`, and with an entry it returns `response`. `live: true` calls the real tool instead. |
| `test.checks` | array of one of: object \| object \| object \| object \| object \| object \| object | No | — |
| `test.criteria` | array of string | No | Statements an LLM judge grades against the transcript. |
| `test.repeat` | integer | No | Attempts per run. Absent means the settings' `defaultRepeat`. |
| `test.caller` | object | No | Who is calling. `ani` fills `{{sys.telephony.ani}}` and is the number the contact and memory belong to. `contact` is a contact found for that number with these attributes; it exists only in the test, and no real contact is ever read or written. `contact` requires `ani`. |
| `test.caller.ani` | string | No | E.164, such as +15551234567. |
| `test.caller.contact` | object | No | — |
| `test.caller.contact.attributes` | object | Yes | — |
| `test.workflowVariables` | object | No | What the calling workflow passed in, read as `{{vars.name}}`. |
| `test.memory` | object | No | A returning caller's memory record, read at the version's memory depth. Requires `caller.ani`. Without it, memory is off during the test. |
| `test.memory.callCount` | integer | Yes | — |
| `test.memory.synopsis` | string | No | — |
| `test.memory.rollingSummary` | string | No | — |
| `test.memory.openThreads` | string | No | — |
| `truncated` | boolean | Yes | True when turns, turn text, or checks were cut to fit the test limits. |

### 400 — `error` is `invalid_request`: `conversationId` is missing or malformed.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | string | No | A stable machine-readable code. |
| `message` | string | No | A human-readable explanation, safe to show to an end user. Present on errors an operator can act on; browser clients display this field in preference to a generic fallback. |
| `limit` | integer | No | Present on some errors (e.g. `prompt_too_long`) that reject a value against a fixed bound: the bound itself. |
| `length` | integer | No | Present on some errors (e.g. `prompt_too_long`) that reject a value against a fixed bound: the measured size of the rejected value. |
| `reason` | string | No | Present on some errors (e.g. `invalid_avatar_image`) that have more than one cause: which one applied. |

### 401 — Missing, malformed, or expired bearer token.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | string | No | A stable machine-readable code. |
| `message` | string | No | A human-readable explanation, safe to show to an end user. Present on errors an operator can act on; browser clients display this field in preference to a generic fallback. |
| `limit` | integer | No | Present on some errors (e.g. `prompt_too_long`) that reject a value against a fixed bound: the bound itself. |
| `length` | integer | No | Present on some errors (e.g. `prompt_too_long`) that reject a value against a fixed bound: the measured size of the rejected value. |
| `reason` | string | No | Present on some errors (e.g. `invalid_avatar_image`) that have more than one cause: which one applied. |

### 404 — `error` is `conversation_not_found` when the conversation does not exist in your organization or has no transcript. A call no voice agent handled has no call record, so it also reads as `conversation_not_found`. `no_agent` means a chat a workflow answered.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | string | No | A stable machine-readable code. |
| `message` | string | No | A human-readable explanation, safe to show to an end user. Present on errors an operator can act on; browser clients display this field in preference to a generic fallback. |
| `limit` | integer | No | Present on some errors (e.g. `prompt_too_long`) that reject a value against a fixed bound: the bound itself. |
| `length` | integer | No | Present on some errors (e.g. `prompt_too_long`) that reject a value against a fixed bound: the measured size of the rejected value. |
| `reason` | string | No | Present on some errors (e.g. `invalid_avatar_image`) that have more than one cause: which one applied. |

### 409 — `error` is `conversation_agent_mismatch`: a different agent handled the conversation. `agentId` names that agent.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `error` | string | No | A stable machine-readable code. |
| `message` | string | No | A human-readable explanation, safe to show to an end user. Present on errors an operator can act on; browser clients display this field in preference to a generic fallback. |
| `limit` | integer | No | Present on some errors (e.g. `prompt_too_long`) that reject a value against a fixed bound: the bound itself. |
| `length` | integer | No | Present on some errors (e.g. `prompt_too_long`) that reject a value against a fixed bound: the measured size of the rejected value. |
| `reason` | string | No | Present on some errors (e.g. `invalid_avatar_image`) that have more than one cause: which one applied. |
| `agentId` | string | No | — |
