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

# Turns and streaming

> Send a turn, parse every stream frame, and recover from each stop reason and error.

A **turn** is one user message plus the model loop it starts: the model may call platform tools, hand a call to your application, or pause for a confirmation, and it usually ends with an answer. One turn can span several requests. Each request runs one **segment** of the turn and streams its frames back as Server-Sent Events (SSE).

`POST /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/turns`

Credential: an organization API key, or a scoped key with `conversations:use`.

## How it works

1. You post `message`. The platform resolves the instance's latest published version, checks guardrails, stores the user message, and runs model rounds.
2. Each round may call platform tools, which run on the server and loop back to the model.
3. The segment ends with a `turn_end` frame. Its `stop_reason` says whether the turn ended (for example `end_turn`), handed client tool calls to you (`tool_use`), or paused for a confirmation (`pending_confirmation`). A failure ends the segment with an `error` frame instead.
4. After a handoff you post `tool_results`. After a confirmation pause you post `confirmations`. Each resume is a new request that runs the next segment of the same turn.

## Request

Send exactly one of `message`, `tool_results`, or `confirmations`.

| Field                | Type          | Required     | Rules                                                                                                                                                                                |
| -------------------- | ------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `end_user`           | string        | Yes          | Must match the conversation's end user. Missing or blank is `400 invalid_request`; a mismatch is `404 conversation_not_found`.                                                       |
| `message`            | string        | One of three | New user input. Not blank, at most 32,000 characters. Starts a new turn.                                                                                                             |
| `tool_results`       | array         | One of three | Results for the client tool calls of a handoff. See [Client tools](/conversations/client-tools).                                                                                     |
| `confirmations`      | array         | One of three | Decisions for the calls of a confirmation pause. See [Client tools](/conversations/client-tools).                                                                                    |
| `edit_of_message_id` | UUID          | No           | With `message` only. Revises an earlier user message. See [Branching](/conversations/branching).                                                                                     |
| `attachment_ids`     | array of UUID | No           | With `message` only. Files uploaded to this conversation that ride this message. No duplicates. See [Attachments](/conversations/attachments).                                       |
| `page_context`       | any JSON      | No           | What the user is looking at in your application, at most 16,384 bytes. Sent to the model as a delimited block marked as untrusted data on every round of this segment. Never stored. |

Sending the retired `depth` field returns `400 invalid_request` with "depth is no longer supported; every assistant turn uses gpt-6-sol." Unknown fields other than `depth` are ignored.

<CodeGroup>
  ```bash curl theme={null}
  curl -N -X POST https://api.usenexio.com/api/v1/conversation-instances/workspace-assistant/conversations/9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93/turns \
    -H "Authorization: Bearer $NEXIO_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Accept: text/event-stream" \
    -d '{
      "end_user": "u_dana_ortiz",
      "message": "Summarize the runs that failed today.",
      "page_context": {"screen": "runs", "filter": "failed"}
    }'
  ```

  ```python Python theme={null}
  import os
  import json
  import requests

  with requests.post(
      "https://api.usenexio.com/api/v1/conversation-instances/workspace-assistant/conversations/9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93/turns",
      headers={"Authorization": "Bearer " + os.environ["NEXIO_API_KEY"], "Accept": "text/event-stream"},
      json={
          "end_user": "u_dana_ortiz",
          "message": "Summarize the runs that failed today.",
          "page_context": {"screen": "runs", "filter": "failed"},
      },
      stream=True,
      timeout=(10, 660),
  ) as resp:
      if resp.status_code != 200:
          raise RuntimeError(resp.json())
      event = None
      for line in resp.iter_lines(decode_unicode=True):
          if line.startswith("event: "):
              event = line[7:]
          elif line.startswith("data: "):
              print(event, json.loads(line[6:]))
  ```

  ```typescript TypeScript theme={null}
  const resp = await fetch(
    "https://api.usenexio.com/api/v1/conversation-instances/workspace-assistant/conversations/9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93/turns",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.NEXIO_API_KEY}`,
        "Content-Type": "application/json",
        Accept: "text/event-stream",
      },
      body: JSON.stringify({
        end_user: "u_dana_ortiz",
        message: "Summarize the runs that failed today.",
        page_context: { screen: "runs", filter: "failed" },
      }),
    },
  )
  if (resp.status !== 200) throw new Error(JSON.stringify(await resp.json()))
  const reader = resp.body!.getReader()
  const decoder = new TextDecoder()
  let buffer = ""
  for (;;) {
    const { value, done } = await reader.read()
    if (done) break
    buffer += decoder.decode(value, { stream: true })
    let sep: number
    while ((sep = buffer.indexOf("\n\n")) !== -1) {
      const frame = buffer.slice(0, sep)
      buffer = buffer.slice(sep + 2)
      const event = frame.match(/^event: (.*)$/m)?.[1]
      const data = frame.match(/^data: (.*)$/m)?.[1]
      if (event && data) console.log(event, JSON.parse(data))
    }
  }
  ```
</CodeGroup>

## The stream

A `200` response has these headers: `Content-Type: text/event-stream`, `Cache-Control: no-cache`, `Connection: keep-alive`, `X-Accel-Buffering: no`. Each frame is:

```text theme={null}
event: <frame type>
data: <one line of JSON>

```

The JSON always carries `type`, equal to the event name.

### Frames

| Frame                  | Payload                                                              | When                                                                                                                                             |
| ---------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `attachment`           | `{type, message_id, attachments: [{id, name, media_type, notice?}]}` | First, only when the message carries files. `notice` says how a format was read when there is a limit, such as the spreadsheet row cap.          |
| `conversation`         | `{type, conversation_id, turn_id, user_message_id?}`                 | First frame of every segment (after `attachment` when present). `user_message_id` is the id of the message this turn stored; absent on a resume. |
| `guardrail`            | `{type, rule_id, decision}`                                          | A guardrail rule fired. `decision` is `refused`, `escalated`, or `output_check_triggered`. See [Guardrails](/conversations/guardrails).          |
| `text_delta`           | `{type, text}`                                                       | A piece of assistant prose. Concatenate in order.                                                                                                |
| `tool_activity`        | `{type, tool_call_id, name, execution, phase}`                       | A tool call's progress, for display. `execution` is `platform` or `client`. `phase` is `started`, `completed`, or `failed`.                      |
| `tool_use`             | `{type, tool_call_id, name, input}`                                  | A client tool call handed to you. Always preceded by a `tool_activity` with `phase: "started"`.                                                  |
| `pending_confirmation` | `{type, tool_call_id, name, input, reason, execution}`               | A call that needs approval before it runs. `execution` says where it runs once approved.                                                         |
| `component`            | `{type, component, version, props}`                                  | A declared UI component with props that passed its schema. No platform tool emits one today.                                                     |
| `turn_end`             | `{type, stop_reason, usage}`                                         | Terminal success frame of the segment.                                                                                                           |
| `error`                | `{type, code, message, reason?}`                                     | Terminal failure frame, sent only after the stream has started.                                                                                  |

Every stream ends with exactly one `turn_end` or one `error` frame. Nothing follows it.

Example frames:

```text theme={null}
event: conversation
data: {"type":"conversation","conversation_id":"9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93","turn_id":"5f2a8c61-3e7b-4d09-b1c4-8a6e2f9d0c75","user_message_id":"c7d1e5a3-2f84-4b6c-9e0a-1b3d5f7a9c28"}

event: tool_activity
data: {"type":"tool_activity","tool_call_id":"call_Rk3v8QmT2xLp","name":"runs.list","execution":"platform","phase":"started"}

event: tool_activity
data: {"type":"tool_activity","tool_call_id":"call_Rk3v8QmT2xLp","name":"runs.list","execution":"platform","phase":"completed"}

event: text_delta
data: {"type":"text_delta","text":"Three runs failed today. Two timed out, and one was rejected for a missing required input."}

event: turn_end
data: {"type":"turn_end","stop_reason":"end_turn","usage":{"input_tokens":6120,"output_tokens":58,"total_tokens":6178,"cached_input_tokens":4096,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}
```

### Usage

`turn_end.usage` has `input_tokens`, `output_tokens`, `total_tokens`, `cached_input_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`. It is cumulative across the segments of the turn, so the `turn_end` of the final segment reports the whole turn. `cached_input_tokens` is the part of `input_tokens` the provider served from its cache. Configured assistants run on an OpenAI model, so the two `cache_*` fields, which only Anthropic models fill, are `0`.

## Turn states

This is the one state model for a turn. Other pages link here.

| State           | Meaning                                                                                                  | How it is reached                                                                             | What happens next                                                                                            |
| --------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Running         | A segment is executing.                                                                                  | A `message`, `tool_results`, or `confirmations` request was accepted.                         | Ends in one of the states below. Another valid turn request on the conversation gets `409 turn_in_progress`. |
| Paused: handoff | Client tool calls were handed to you: calls that need no confirmation, or calls that were just approved. | Segment ended with `stop_reason: "tool_use"` after `tool_use` frames.                         | Post `tool_results` covering exactly the handed-off call ids.                                                |
| Paused: confirm | The model called one or more tools that need approval.                                                   | Segment ended with `stop_reason: "pending_confirmation"` after `pending_confirmation` frames. | Post `confirmations` covering exactly the paused call ids.                                                   |
| Ended           | The turn is over.                                                                                        | `turn_end` with any other stop reason.                                                        | Post the next `message`.                                                                                     |

After an `error` frame, post the next `message`. If a pause is still open, that request returns `409 pending_turn`, described below.

A new `message` while a turn is paused returns `409 pending_turn`. Its `details` say which resume is owed:

```json theme={null}
{
  "code": "pending_turn",
  "message": "This conversation has a paused turn awaiting tool_results or confirmations; resolve it before sending a new message.",
  "details": {
    "pause_kind": "handoff",
    "pending_call_ids": ["call_9pLw2cXv7NqB"]
  }
}
```

`pause_kind: "confirm"` means show the approval prompts again for the listed ids and post `confirmations`. `pause_kind: "handoff"` means post `tool_results` for the listed ids. A resume of the wrong kind, or a resume when nothing is paused, returns `409 turn_state_conflict`.

### Stop reasons

| `stop_reason`            | Meaning                                                                                                                                                                                        | Turn state after |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| `end_turn`               | The model finished its answer.                                                                                                                                                                 | Ended            |
| `max_tokens`             | The model call hit its output cap: `model_policy.max_tokens`, or the turn's remaining `limits.max_turn_output_tokens` when that is smaller. Other provider stop values pass through unchanged. | Ended            |
| `tool_use`               | Client tool calls were handed to you.                                                                                                                                                          | Paused: handoff  |
| `pending_confirmation`   | Calls are waiting for approval.                                                                                                                                                                | Paused: confirm  |
| `refusal`                | A refusal rule fired before any model round. The answer is fixed text naming the rule.                                                                                                         | Ended            |
| `escalated`              | An escalation rule fired on the message. The assistant still answered; route the conversation to a person.                                                                                     | Ended            |
| `output_check_triggered` | An output check matched the final answer. The answer was withheld and replaced by fixed text.                                                                                                  | Ended            |
| `max_output_tokens`      | The turn spent `limits.max_turn_output_tokens` across its rounds.                                                                                                                              | Ended            |

When the model uses every one of `limits.max_tool_rounds`, it gets one more round with no tools to write its answer.

### Which config a turn runs

A new `message` runs the latest published version of the instance; the instance must have one (`409 instance_not_published`). A resume (`tool_results` or `confirmations`) runs the version the turn started on, even if a newer version was published during the pause. Every stored message carries the `config_version_hash` it ran under.

## Errors before and after the first frame

The response status is decided when the first frame is written.

* **Before the first frame**, a failure is a normal HTTP error with the JSON envelope `{code, message, details?}`. Nothing was streamed.
* **After the first frame**, the status is already `200`. A failure arrives as a terminal `error` frame with `code`, `message`, and, for `provider_error`, `reason`. Assistant text already streamed in that segment may be incomplete.

A turn writes its `conversation` frame before it calls the model, so a model provider failure arrives as an `error` frame, not as an HTTP status. Its code is `provider_error` with a `reason` from a closed list (`rate_limited`, `context_overflow`, `model_unavailable`, `invalid_request`, `provider_auth`, `provider_fault`), or `provider_unavailable` when the provider circuit is open after repeated failures. A model call that fails with a transient error (a provider rate limit or 5xx, or a network failure) is attempted up to three times in total, waiting at most 5 seconds between attempts, as long as the provider has streamed nothing for that call. Once the provider has streamed output for that call, the failure is final.

## Disconnects and timeouts

* The turn keeps running when your client disconnects. It completes and is stored; fetch the conversation to see the result.
* One segment may run for up to 10 minutes on the server. Set your client read timeout above that.
* Turns are exempt from the 30-second request timeout most routes have.
* One segment runs per conversation at a time. A hold left by a segment that died without releasing it (a crash or deploy) can be taken over after 10 minutes.
* Turn requests have their own rate limit bucket per org, separate from the default bucket other routes use. Its limit is 300 requests per minute, or the deployment's default limit when that is higher. A `429 rate_limited` carries `Retry-After`.
* If a previous turn was interrupted between a tool call and its result, the platform answers the orphaned calls with errors before the next message turn runs.

## Errors

| Status | Code                                                                                                                                                      | Cause                                                                                                                                                                                                         |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `invalid_request`                                                                                                                                         | Body is not JSON or is over 1 MB, `end_user` is missing, or `depth` was sent.                                                                                                                                 |
| 400    | `invalid_turn_request`                                                                                                                                    | Not exactly one of the three starters; blank `message`; `edit_of_message_id` without `message` or not a UUID; `attachment_ids` without `message`, not UUIDs, or duplicated; `page_context` over 16,384 bytes. |
| 400    | `message_too_long`                                                                                                                                        | `message` over 32,000 characters.                                                                                                                                                                             |
| 400    | `invalid_tool_result`                                                                                                                                     | Empty `tool_call_id`, a duplicate, or results that do not cover exactly the pending calls.                                                                                                                    |
| 400    | `invalid_confirmation`                                                                                                                                    | Empty `tool_call_id`, missing `approved`, a duplicate, or decisions that do not cover exactly the paused calls.                                                                                               |
| 400    | `invalid_edit_target`                                                                                                                                     | `edit_of_message_id` names an assistant message.                                                                                                                                                              |
| 400    | `attachments_not_enabled`, `too_many_attachments`, `attachment_not_accepted`                                                                              | See [Attachments](/conversations/attachments).                                                                                                                                                                |
| 401    | `unauthorized`                                                                                                                                            | Missing or invalid key.                                                                                                                                                                                       |
| 403    | `insufficient_capability`                                                                                                                                 | Scoped key without `conversations:use`.                                                                                                                                                                       |
| 403    | `instance_archived`                                                                                                                                       | The instance is archived.                                                                                                                                                                                     |
| 404    | `instance_not_found`                                                                                                                                      | Unknown slug.                                                                                                                                                                                                 |
| 404    | `conversation_not_found`                                                                                                                                  | Unknown id, or a different end user or environment.                                                                                                                                                           |
| 404    | `message_not_found`                                                                                                                                       | `edit_of_message_id` is not in this conversation.                                                                                                                                                             |
| 404    | `attachment_not_found`                                                                                                                                    | A named attachment is missing or out of scope.                                                                                                                                                                |
| 409    | `conversation_archived`                                                                                                                                   | The conversation is archived. Reactivate it with `PATCH` and `status: active`.                                                                                                                                |
| 409    | `instance_not_published`                                                                                                                                  | No published version.                                                                                                                                                                                         |
| 409    | `turn_in_progress`                                                                                                                                        | Another segment is running.                                                                                                                                                                                   |
| 409    | `pending_turn`                                                                                                                                            | A paused turn must be resumed first. See `details`.                                                                                                                                                           |
| 409    | `turn_state_conflict`                                                                                                                                     | The resume does not match the paused state.                                                                                                                                                                   |
| 409    | `confirmation_environment_unpinned`                                                                                                                       | The paused action has no recorded environment. See [Client tools](/conversations/client-tools).                                                                                                               |
| 409    | `attachment_changed`                                                                                                                                      | A file on the message was removed while the turn ran. Arrives in an `error` frame. Attach the file again and resend.                                                                                          |
| 413    | `attachments_too_large`                                                                                                                                   | The files on the message exceed the per-turn limit.                                                                                                                                                           |
| 429    | `rate_limited`                                                                                                                                            | Turn bucket exhausted.                                                                                                                                                                                        |
| 500    | `guardrail_evaluation_failed`                                                                                                                             | The guardrail classifier failed. The turn did not run.                                                                                                                                                        |
| 500    | `guardrail_config_invalid`                                                                                                                                | An output check pattern does not compile.                                                                                                                                                                     |
| 500    | `instance_model_unavailable`                                                                                                                              | The platform model is not available. No fallback model is used.                                                                                                                                               |
| 500    | `instance_config_missing`, `instance_config_invalid`, `invalid_conversation_history`, `attachment_read_failed`, `streaming_unsupported`, `internal_error` | Platform faults. Retry; report if it persists.                                                                                                                                                                |
| 503    | `converse_unavailable`                                                                                                                                    | Conversations are not enabled in this deployment.                                                                                                                                                             |
| 503    | `attachments_unavailable`                                                                                                                                 | File storage is not configured in this deployment.                                                                                                                                                            |

A code raised after the first frame, such as `attachment_changed`, `invalid_conversation_history`, or `internal_error`, arrives in an `error` frame on the `200` stream instead of as this status. The full error envelope is described on [Errors](/reference/errors).

<CardGroup cols={2}>
  <Card title="Client tools and confirmations" href="/conversations/client-tools">
    Resume a handoff or a confirmation pause.
  </Card>

  <Card title="Take a turn (API reference)" href="/api-reference/conversations/turns/take-turn">
    The generated endpoint contract.
  </Card>
</CardGroup>
