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

# Stop a turn

> Let a user stop the assistant, show what happened, and edit a message while a reply runs.

A user can stop the assistant while it works. Stop ends the turn cleanly. Nothing new starts. A write that already started finishes and its result is kept. A model reply or a read in progress is cut short: text already streamed is kept, and the step gets a result that says it was stopped. The conversation stays ready for the next message.

<Note>
  Stop is served only when turn control is on for your environment. You can tell it is on in two ways: the conversation detail response carries the `turn` key, or the `conversation` frame of a turn stream carries `"turn_control": true`. Until then the Stop and turn routes answer `404 not_found`. Show Stop only after you have seen one of the two, and never call these routes before that.
</Note>

## Terms

* A **turn** is one user message and the work the assistant does for it. A turn can span several requests.
* A **segment** is one request's part of a turn: the stream you read until its `turn_end` frame.
* A turn is **at rest** when its status is `completed`, `stopped` or `failed`. Nothing more happens to it.
* A turn is **stopping** when Stop was pressed and the turn has not come to rest yet, for example because a write that already started is finishing. The platform does not store this status. It reads it from the stop request and the turn's state.

## Show Stop

Show one button. It replaces Send while the assistant works, in the same place and at the same size.

* Show Stop from the moment you send a message until the turn is at rest.
* Keep showing it while the turn waits for a client tool result or for an approval. Stop works in every status that is not at rest.
* If the user presses Stop before you know the turn id, keep the press. Send Stop as soon as the `conversation` frame gives you `turn_id`.
* Show Send again once the turn is at rest.

## Call Stop

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

Credential: an organization API key, or a scoped key with `conversations:use`. The body carries the same `end_user` as the turn.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.usenexio.com/api/v1/conversation-instances/workspace-assistant/conversations/9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93/turns/5f2a8c61-3e7b-4d09-b1c4-8a6e2f9d0c75/stop \
    -H "Authorization: Bearer $NEXIO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"end_user": "u_dana_ortiz"}'
  ```

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

  resp = requests.post(
      "https://api.usenexio.com/api/v1/conversation-instances/workspace-assistant/conversations/9e4c7a12-5b3d-4f81-a6e0-2d8b1c4f7a93/turns/5f2a8c61-3e7b-4d09-b1c4-8a6e2f9d0c75/stop",
      headers={"Authorization": "Bearer " + os.environ["NEXIO_API_KEY"]},
      json={"end_user": "u_dana_ortiz"},
      timeout=10,
  )
  turn = resp.json()["turn"]
  print(turn["status"])
  ```

  ```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/5f2a8c61-3e7b-4d09-b1c4-8a6e2f9d0c75/stop",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.NEXIO_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ end_user: "u_dana_ortiz" }),
    },
  )
  const { turn } = await resp.json()
  console.log(turn.status)
  ```
</CodeGroup>

Stop answers within two seconds with the turn:

```json theme={null}
{
  "turn": {
    "id": "5f2a8c61-3e7b-4d09-b1c4-8a6e2f9d0c75",
    "status": "stopping",
    "started_at": "2026-09-28T14:02:11.418Z",
    "stop_requested_at": "2026-09-28T14:02:19.906Z",
    "ended_at": null,
    "stop_reason": null
  }
}
```

* On a turn that was not at rest, `status` is `stopped` when Stop ended it, or `stopping` when a write that already started still has to finish. A turn already at rest is returned unchanged, whatever its status (`completed`, `stopped` or `failed`).
* Stop is safe to repeat. On a turn at rest it changes nothing and returns the turn as it is.
* Stop works on an archived conversation.
* A turn that started before turn control was on has no record. Stop on it answers `404 turn_not_found`.

## While the turn is stopping

* Change the button to a disabled "Stopping" state. Do not show Send yet.
* Keep reading the stream if you have one open. It ends with `turn_end` and `stop_reason: "stopped"`.
* Without an open stream, read the turn every two seconds until it is at rest:

  `GET /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/turns/{turn_id}?end_user=...`

  It answers `{"turn": {...}}` in the same shape as Stop. The read changes nothing.
* If the turn reads `stopping` for more than three seconds, call Stop again. A repeat ends the turn when nothing else holds it, for example after a lost signal. A write that is still running keeps the turn stopping until it finishes, so keep reading until the turn is at rest before you show Send.

## Read a stopped turn

### The turn object

| Field | Meaning |
| - | - |
| `id` | The turn id, the same value as `turn_id` on the `conversation` frame and on every stored message of the turn. |
| `status` | `running`, `waiting_on_tools`, `waiting_on_confirmation`, `stopping`, `completed`, `stopped` or `failed`. The last three are at rest. |
| `started_at` | When the user message was stored. |
| `stop_requested_at` | When Stop was first pressed, or `null`. A repeated Stop does not move it. |
| `ended_at` | When the turn came to rest, or `null`. |
| `stop_reason` | `null` until the turn is at rest. Then the model's reason (such as `end_turn`), `stopped`, `handoff_lost`, `segment_lost`, or the error code of a failed turn. |

### The transcript

* Text the assistant streamed before the stop is kept as the reply's text, when it passes the instance's output checks. Show it with a short "Stopped" line under it.
* Every step that started gets a stored result. Show it as a stopped step. The result says what is known:

| Stored result | Meaning |
| - | - |
| `{"stopped": true, "message": "Stopped by the user before a result was recorded. It may have run. Check before repeating it."}` | The step may have run. No result came back before the stop. |
| `{"stopped": true, "message": "Stopped by the user. It did not run."}` | The step never ran: it waited for an approval, or it was never started. |
| `{"lost": true, "message": "No result was recorded."}` | The turn was lost (below). The step may have run. |

A server step that finished before the stop keeps its real result.

* The model sees the same stored results on the next message, so it can tell the user what is known about each stopped step.

### After a reload

Conversation detail carries two keys while turn control is on:

* `turn` is the latest turn of the version the conversation serves, or `null` when that turn has no record. Use it to decide whether to show Stop or Send.
* `turns` lists every turn with a message in the returned transcript window that did not end normally, oldest first: `[{"id": "...", "status": "stopped", "stop_reason": "stopped"}]`. It is an empty list when there is none. Each message carries its `turn_id`, so mark the last message of each listed turn as stopped.

The version switch answer carries both keys too.

## What Stop does not do

* It does not undo a step that ran.
* It never cuts a write. A write that already started finishes, and its result is kept.
* It cancels no run. A run the assistant submitted keeps going and its own status shows that. Runs your own tools started are yours to cancel. See [Client tools](/conversations/client-tools#when-a-turn-is-stopped).

## Edit a message while a turn runs

Send the edit with `stop_active_turn: true`:

```json theme={null}
{
  "end_user": "u_dana_ortiz",
  "message": "Only the runs from the last hour.",
  "edit_of_message_id": "c7d1e5a3-2f84-4b6c-9e0a-1b3d5f7a9c28",
  "stop_active_turn": true
}
```

* The platform stops the running turn, then starts the reply to the edited message as a new version, in the same request.
* The old version keeps its stopped work. The user can switch back to it.
* Without `stop_active_turn`, an edit while a turn runs answers `409` as it always has.
* If the running turn does not come to rest within 10 seconds, the request answers `409 turn_in_progress`. The stop stays recorded. Send the edit again once the turn is at rest.
* Disable Save while the request is in flight. A repeat after a success is a new edit.
* A version switch while the latest turn is not at rest answers `409 turn_active`. Disable the version switcher while the assistant works.

## Late results

Your app may finish a step after the turn stopped. Post its result as usual, with `tool_results` on the turns route.

* The platform keeps it. The result replaces the step's stored placeholder once. No model call follows.
* The answer is a short stream: a `conversation` frame, then `turn_end` with the turn's `stop_reason`. This is not an error.
* A second post for the same step is ignored, with the same answer.
* Only a step that may have run takes a late result. A step stored as "It did not run" never does.
* The platform matches a late post against the conversation's five most recent turns that did not end normally.
* If the post answers `409 turn_in_progress`, a newer turn holds the conversation. Retry with backoff for up to 60 seconds, then keep the result and retry later.

## Lost turns

The platform ends a turn nobody can finish, so a conversation never gets stuck:

* A **lost handoff**: your app took client tool calls and posted no results within 10 minutes. The turn rests as `failed` with `stop_reason: "handoff_lost"`. Its open steps get the "No result was recorded." result. The next message is accepted. A result posted later is kept as a late result.
* A **lost segment**: the segment running the turn died, for example in a deploy. The next request ends the turn as `failed` with `stop_reason: "segment_lost"`, with the same result on its open steps.

## Errors

| Status | Code | Cause |
| - | - | - |
| 400 | `invalid_request` | The body is not JSON, or `end_user` is missing, blank or over 256 characters. On the turn read, `end_user` is missing. |
| 401 | `unauthorized` | Missing or invalid key. |
| 403 | `insufficient_capability` | Scoped key without `conversations:use`. |
| 403 | `instance_archived` | The instance is archived. |
| 404 | `not_found` | Turn control is not on for this environment. |
| 404 | `instance_not_found` | Unknown slug. |
| 404 | `conversation_not_found` | Unknown id, or a different end user or environment. |
| 404 | `turn_not_found` | Unknown or malformed turn id, a turn of another conversation, or a turn with no record. |
| 409 | `turn_in_progress` | On an edit with `stop_active_turn`: the running turn did not come to rest within 10 seconds. |
| 409 | `turn_active` | On a version switch: the latest turn is not at rest. |
| 500 | `internal_error` | The stop could not be recorded. Press Stop again. |
| 503 | `converse_unavailable` | Conversations are not enabled in this deployment. |

<CardGroup cols={2}>
  <Card title="Turns and streaming" href="/conversations/turns-and-streaming">
    Frames, turn states, and stop reasons.
  </Card>

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.