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

# Conversation Continuity

> Resume Research Agent conversations with chat_id and checkpoints, and Workflows runs with execution_id.

A research session rarely ends after the first answer. Users follow up, refine
their question, or branch into a new line of inquiry. Both the Research Agent and
Workflows support multi-turn continuation, but the surfaces differ slightly and
involve four related identifiers that are easy to confuse. This guide
disambiguates them, shows how persistence interacts with continuity, and provides
worked examples for the common multi-turn and branching patterns.

## The four identifiers

| ID                   | Endpoint       | Direction            | What it identifies                                                                                                                                                                |
| -------------------- | -------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chat_id`            | Research Agent | request and response | A persistent conversation thread. Auto-created if absent on the first request; pass the same value on subsequent requests to continue the same thread.                            |
| `execution_id`       | Workflows      | request and response | A specific workflow execution. Pass a previous `execution_id` to resume the conversation produced by that execution.                                                              |
| `from_checkpoint_id` | Research Agent | request only         | The point in an existing conversation to resume from. Lets you rewrite or branch a conversation by going back to an earlier state.                                                |
| `checkpoint_id`      | Research Agent | response only        | A cursor in the `COMPLETE` event marking the conversation state after this turn. Store it and pass as `from_checkpoint_id` on the next request to continue from this exact point. |

In practice:

* A vanilla follow-up turn uses `chat_id` (or `execution_id`) alone to identify
  the conversation.
* A branching turn additionally uses `from_checkpoint_id` to specify *where* in
  the conversation to resume.
* The `checkpoint_id` you store today is what becomes a `from_checkpoint_id`
  tomorrow.

## `persistence_mode`

The Research Agent request accepts `persistence_mode` with two values. Both
allow multi-turn follow-ups; they differ in how long the conversation
survives.

* `disabled` (default) -- creates an **ephemeral** conversation thread.
  Follow-up turns against the same `chat_id` work for approximately one
  hour. After that, the underlying checkpoint is evicted and a resume
  attempt returns HTTP 404 with the detail `"Cannot follow up: chat expired"`.
* `enabled` -- creates a **persistent** conversation thread. The `chat_id`
  and `checkpoint_id` returned in `COMPLETE` can be used to continue or
  branch the conversation indefinitely.

The mode is **immutable for the life of a conversation**. It is fixed when
the conversation is created on turn 1; every resume request must pass the
same value. A mismatch returns HTTP 400 on the Research Agent (with detail
`"persistence_mode mismatch"`) and HTTP 409 on Workflows.

**Choosing a mode**: leave the default `disabled` for one-off questions
and short sessions that wrap up within an hour. Set `enabled` on turn 1
when the conversation needs to survive longer -- branching workflows,
research threads spanning a working session, anything you want a user to
resume the next day.

Workflows expose `persistence_mode` on the synchronous execute endpoint
with the same semantics (default `disabled`, immutable, mismatch returns
409\). Asynchronous workflows always persist because the events blob is the
client's only way to read execution state after the SSE connection closes;
they do not expose `persistence_mode`.

## The `"INITIAL"` sentinel

Passing `from_checkpoint_id: "INITIAL"` is a special instruction that truncates
the entire conversation and starts over with the new `message`. Use it when the
user explicitly says "let's start over" or when you detect a context-poisoning
mistake (a typo in a name, a wrong company id, a stale assumption) that you want
to clear cleanly while keeping the same `chat_id`.

```python theme={null}
payload = {
    "chat_id": current_chat_id,
    "from_checkpoint_id": "INITIAL",
    "message": "Let's start fresh: analyze NVIDIA's data center segment.",
    "persistence_mode": "enabled",
    "research_effort": "standard",
}
```

`"INITIAL"` deletes all prior interactions for that `chat_id`. There is no undo.

## Worked example: multi-turn conversation

The pattern below persists `chat_id` and `checkpoint_id` between turns, letting the
user ask follow-up questions on top of the agent's previous answers.

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


def turn(api_key: str, message: str, chat_id: str | None, checkpoint_id: str | None):
    endpoint = "https://agents.bigdata.com/v1/research-agent"
    headers = {"X-API-KEY": api_key, "Content-Type": "application/json"}
    payload: dict = {
        "message": message,
        "research_effort": "standard",
        "persistence_mode": "enabled",
    }
    if chat_id:
        payload["chat_id"] = chat_id
    if checkpoint_id:
        payload["from_checkpoint_id"] = checkpoint_id

    answer = ""
    new_chat_id: str | None = None
    new_checkpoint_id: str | None = None

    with requests.post(endpoint, headers=headers, json=payload, stream=True, timeout=300) as r:
        r.raise_for_status()
        for raw_line in r.iter_lines(decode_unicode=True):
            if not raw_line or not raw_line.startswith("data: "):
                continue
            try:
                event = json.loads(raw_line[6:])
            except json.JSONDecodeError:
                continue

            new_chat_id = event.get("chat_id") or new_chat_id
            msg = event.get("message", {})
            if msg.get("type") == "ANSWER":
                answer += msg.get("content", "")
            elif msg.get("type") == "COMPLETE":
                new_checkpoint_id = msg.get("checkpoint_id")
            elif msg.get("type") == "ERROR":
                raise RuntimeError(msg.get("error"))

    return answer, new_chat_id, new_checkpoint_id


# First turn: no chat_id yet, no checkpoint.
api_key = "..."
answer1, chat_id, ckpt = turn(api_key, "Summarize NVIDIA's Q2 FY26 results.", None, None)

# Follow-up turn: reuse chat_id; pass the checkpoint from the previous turn.
answer2, chat_id, ckpt = turn(api_key, "How does that compare to AMD?", chat_id, ckpt)

# Another follow-up: same pattern.
answer3, chat_id, ckpt = turn(api_key, "Which one has better margins?", chat_id, ckpt)
```

The `chat_id` returned by each event identifies the conversation thread; the
`checkpoint_id` returned in `COMPLETE` is your cursor for the next turn. Persist
both server-side or in client storage if you need conversations to survive across
sessions.

## Branching a conversation

To "rewind" a conversation -- for example, when the user edits an earlier message
or wants to explore a different follow-up question from a known good state -- pass
the `checkpoint_id` from the turn you want to resume *from*, with the same
`chat_id`. Any interactions after that checkpoint are discarded, and the new
message takes their place.

```python theme={null}
# Suppose we have a stored checkpoint after the first turn (`ckpt_after_turn_1`)
# and we want to branch off in a different direction.
branch_answer, chat_id, branch_ckpt = turn(
    api_key,
    "Actually, focus on the gaming segment instead.",
    chat_id,
    ckpt_after_turn_1,
)
```

After this call, the conversation's history consists of: the system prompt, the
first user message, the first answer, and the new branched turn. The follow-ups
that originally came after `ckpt_after_turn_1` no longer exist on the server.

## Workflows: resuming by `execution_id`

Workflows do not expose checkpoints. Instead, each execution has an `execution_id`
that the streaming response carries on every event. Pass that id back in a later
request to continue the same conversation.

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


def workflow_turn(api_key: str, template_id: str, message: str, execution_id: str | None):
    endpoint = "https://agents.bigdata.com/v1/workflow/execute"
    headers = {"X-API-KEY": api_key, "Content-Type": "application/json"}
    payload: dict = {"template": template_id, "input": {"message": message}}
    if execution_id:
        payload["execution_id"] = execution_id

    answer = ""
    new_execution_id: str | None = None

    with requests.post(endpoint, headers=headers, json=payload, stream=True, timeout=600) as r:
        r.raise_for_status()
        for raw_line in r.iter_lines(decode_unicode=True):
            if not raw_line or not raw_line.startswith("data: "):
                continue
            try:
                event = json.loads(raw_line[6:])
            except json.JSONDecodeError:
                continue

            new_execution_id = event.get("execution_id") or new_execution_id
            delta = event.get("delta", {})
            if delta.get("type") == "ANSWER":
                answer += delta.get("content", "")
            elif delta.get("type") == "ERROR":
                raise RuntimeError(delta.get("error"))

    return answer, new_execution_id
```

Resuming a workflow by `execution_id` continues the conversation but does not
re-run the original template's research plan from scratch; the agent treats the
new `input` (or follow-up message) as a continuation, with prior interactions in
context.

## Common pitfalls

* **Treating the default `disabled` mode as suitable for long sessions.**
  Ephemeral threads expire after about an hour; a follow-up that arrives
  later returns HTTP 404 with detail `"Cannot follow up: chat expired"`.
  Set `persistence_mode: "enabled"` on turn 1 if the conversation needs to
  survive longer than that.
* **Switching modes mid-conversation.** The mode is immutable once a
  conversation is created. Resume requests must pass the same value used on
  turn 1 or the server returns HTTP 400 (Research Agent) / 409 (Workflows).
* **Storing `chat_id` but not `checkpoint_id`.** A `chat_id` alone is enough
  for the simple "continue the latest turn" case, but if you want stable
  replay or branching, you also need the `checkpoint_id` returned in
  `COMPLETE`.
* **Mixing `chat_id` between users.** The `chat_id` is a server-side
  conversation identifier; do not share it across users or sessions you
  want isolated.
* **Treating `checkpoint_id` as introspectable.** It is an opaque string;
  do not parse, compare, or derive meaning from its contents.
* **Calling `from_checkpoint_id: "INITIAL"` accidentally.** It deletes the
  entire thread. Guard this code path behind an explicit user action.

## Next steps

<CardGroup cols={2}>
  <Card title="Streaming responses" icon="bolt" href="/how-to-guides/agents/concepts/streaming-responses">
    Reference for every message type, including how `checkpoint_id` arrives in `COMPLETE`.
  </Card>

  <Card title="Error handling" icon="triangle-exclamation" href="/how-to-guides/agents/concepts/error-handling">
    Handle 404s from invalid checkpoint IDs and other continuity failures.
  </Card>
</CardGroup>
