Use threads with the Cortex Agent REST API

This guide explains how to create, continue, and manage threaded conversations using the Cortex Agent REST API.

The workflow for using threads includes the following steps:

  1. Create a new thread and use it as part of an agent:run request to the Cortex Agent REST API.
  2. Read the message IDs for the thread.
  3. Choose a message to continue the thread from.

Start a new thread and use it with Agent Run

You must create a new thread, then pass it as part of a request to agent:run.

  1. Create a new thread using Create thread.
  2. Pass the ID of the newly created thread as part of a request to one of the agent:run REST API endpoints.
  • agent:run with Agent Object:

    /api/v2/databases/{database}/schemas/{schema}/agents/{name}:run
    
  • agent:run without Agent Object:

    /api/v2/cortex/agent:run
    

As part of the request, pass the following:

  • parent_message_id must be 0. This indicates that this request is the start of the thread.
  • Exactly one user message in messages.
POST <agent run endpoint>
{
  "thread_id": 1234,
  "parent_message_id": 0,
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "What is the total revenue for 2025?"
        }
      ]
    }
  ],
}

Read the returned message IDs

The Agent API streams back metadata events for each message in the conversation. The following output shows the structure of the metadata. Always listen for both user and assistant metadata events.

event: metadata
data: {"metadata": {"role":"user","message_id":123}}

event: metadata
data: {"metadata": {"role":"assistant","message_id":456}}

In this output, the message IDs correspond to the following in the conversation:

  • 123: the persisted user message ID
  • 456: the persisted assistant message ID

Together, these IDs form the following thread:

0 -> 123 (user) -> 456 (assistant)

Continue the conversation

For the next turn in the conversation, set parent_message_id to the last successful assistant message ID and pass new values in messages. In this example, the parent message ID is 456.

Note

You must pass an assistant message ID as the parent_message_id to ensure the LLM functions as expected. You cannot pass a user message ID. If you have lost track of the last message ID, use Create thread to list all messages in the thread.

{
  "thread_id": 1234,
  "parent_message_id": 456,
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "What about last year?"
        }
      ]
    }
  ],

}

Continue using the latest successful assistant message ID as the parent_message_id in subsequent requests.

Fork a conversation

You can also fork the conversation by continuing from any earlier assistant message. To fork the conversation, pass the desired assistant message ID as the parent_message_id in a new request. In the following example, 3 (user) -> 4 (assistant) and 5 (user) -> 6 (assistant) represent two different forks from the same assistant response.

0 -> 1 (user) -> 2 (assistant) -> 3 (user) -> 4 (assistant)
0 -> 1 (user) -> 2 (assistant) -> 5 (user) -> 6 (assistant)

Retrieve the latest summary and subsequent messages

When a thread grows long, Cortex Agents periodically compacts older turns into a summary message and stores it in the thread with a message type of compaction. By default, the Describe thread endpoint returns only conversation messages, so summaries stay hidden unless you explicitly filter for them. If you only need conversation history, this default response is sufficient; you can also request it explicitly with message_type=conversation. To rehydrate the recent thread state for a new interaction, you often want the latest summary along with every conversation message that was created after it.

The endpoint returns messages in descending order of creation, and the last_message_id query parameter acts as an exclusive cursor: it returns messages older than the specified ID. The endpoint doesn’t expose a forward cursor, but message IDs are monotonically increasing across both message types, so a numeric ID comparison is a reliable proxy for chronological ordering. You retrieve this view in two steps:

  1. Fetch the latest summary message.
  2. Walk backward through conversation messages, keeping every message whose message_id is greater than the summary’s message_id.

Step one: Fetch the latest summary

Set message_type=compaction and page_size=1 to return only the newest summary message.

curl -X GET "$SNOWFLAKE_ACCOUNT_BASE_URL/api/v2/cortex/threads/<thread_id>?message_type=compaction&page_size=1" \
--header 'Accept: application/json' \
--header "Authorization: Bearer $PAT"

Record the message_id from the returned summary. This value is the anchor for Step two.

{
  "metadata": { "thread_id": 1234567890, "thread_name": "Support Chat", "origin_application": "my_app", "created_on": 1717000000000, "updated_on": 1717000050000 },
  "messages": [
    {
      "message_id": 42,
      "parent_id": 41,
      "created_on": 1717000050000,
      "role": "assistant",
      "message_payload": "Summary of the earlier turns.",
      "request_id": "req_042",
      "message_type": "compaction"
    }
  ]
}

Note

If the response returns an empty messages array, the thread hasn’t been compacted yet. In that case, you can skip the anchor and fetch every conversation message in Step two.

Step two: Fetch conversation messages after the summary

Set message_type=conversation and page backward until you cross the anchor from Step one.

curl -X GET "$SNOWFLAKE_ACCOUNT_BASE_URL/api/v2/cortex/threads/<thread_id>?message_type=conversation&page_size=50" \
--header 'Accept: application/json' \
--header "Authorization: Bearer $PAT"

For each returned page:

  1. Keep messages whose message_id is greater than the anchor from Step one.
  2. If any message on the page has a message_id less than or equal to the anchor, or if the page contains fewer than page_size messages, stop paging.
  3. Otherwise, take the smallest message_id on the page, which is the last entry because results are in descending order, and pass it as last_message_id on the next request.

The following Python function shows the full pattern:

import os
import requests

BASE_URL = os.environ["SNOWFLAKE_ACCOUNT_BASE_URL"]
HEADERS = {
    "Authorization": f"Bearer {os.environ['PAT']}",
    "Accept": "application/json",
}
PAGE_SIZE = 50
TIMEOUT_SECONDS = 30


def latest_summary_and_after(thread_id: int):
    endpoint = f"{BASE_URL}/api/v2/cortex/threads/{thread_id}"

    # Step one: fetch the newest summary, if any.
    resp = requests.get(
        endpoint,
        headers=HEADERS,
        params={"message_type": "compaction", "page_size": 1},
        timeout=TIMEOUT_SECONDS,
    )
    resp.raise_for_status()
    summaries = resp.json()["messages"]
    anchor = summaries[0]["message_id"] if summaries else -1

    # Step two: walk conversation messages backward until we cross the anchor.
    collected = []
    last_message_id = None
    while True:
        params = {"message_type": "conversation", "page_size": PAGE_SIZE}
        if last_message_id is not None:
            params["last_message_id"] = last_message_id
        resp = requests.get(endpoint, headers=HEADERS, params=params, timeout=TIMEOUT_SECONDS)
        resp.raise_for_status()
        page = resp.json()["messages"]
        if not page:
            break

        new_messages = [m for m in page if m["message_id"] > anchor]
        collected.extend(new_messages)

        # Stop when the page crosses the anchor or the thread ends.
        if len(new_messages) < len(page) or len(page) < PAGE_SIZE:
            break
        last_message_id = page[-1]["message_id"]

    # Reverse to chronological order (oldest first).
    collected.reverse()
    return summaries + collected

The returned list starts with the latest summary (if one exists), followed by every conversation message created after it, in chronological order. To continue the thread from this context, pass the newest assistant message_id from the returned list as parent_message_id in a subsequent request; see Continue the conversation.

Troubleshooting

In rare cases, the Agent API might fail to store the assistant message. If assistant metadata is missing from the response, ignore the failed turn and continue from the last successful assistant message.

For example, consider the following thread:

0 -> 1 (user) -> 2 (assistant) -> 3 (user) -> [assistant failed]

To continue the conversation, pass message ID 2 as part of a new request because that is the last successful assistant message.

0 -> 1 (user) -> 2 (assistant) -> 5 (user) -> 6 (assistant)