Skip to main content
The conversation API is how you talk with an agent over several messages. It lives under /v1/agents/{agent_id}/conversations/... and takes your normal API key (x-api-key) or an OAuth2 JWT. Xpander Chat uses these same routes. Use Invoke Agent for a single one-shot request. Use a conversation when you want a thread: send a message, get the reply, send the next message with the same conversation_id, add follow-ups while a reply is being written, steer or stop the current turn, edit an earlier message, or answer a question the agent asked.

How a conversation works

Each message you send is one turn: the agent reads your message, works, and replies. The first message creates the conversation and returns its conversation_id; every later message carries that id and continues the same thread. The agent keeps the full history, so you never resend earlier messages. Within a conversation you can:
  • Send a message and get the reply with Run Conversation Turn (waits for the reply) or its streaming variant (events as they happen). Pass id = your conversation_id to continue; omit it to start a new conversation.
  • Add a follow-up while a turn runs with Send Conversation Message: the message waits in the conversation’s queue for the next turn, or steers the turn that is already running.
  • Check the state with Run State: is a turn running, can it take a steer, how many follow-ups are queued.
  • Run queued follow-ups with Drain Queue when the conversation is idle.
  • Stop the running turn with Stop; queued follow-ups stay unless you clear them.
  • Edit an earlier message with Edit Message: everything after it is dropped and the rewritten message runs as a new turn.
  • Answer a question card with Submit Answers, or approve or decline a confirmation card with Confirm.

The conversation record

A conversation has one record. Get Conversation returns it together with the full thread, token usage and the record of every sub-task the agent created. The same record is also reachable with Get Task on the conversation_id, because the record uses the Task schema: id on that object is the conversation_id. To continue a conversation recorded before September 2026, start a new conversation with the same agent.

Sub-tasks

A turn runs on the agent you addressed. When that agent hands part of the work to another agent, it creates a sub-task: a separate record with its own thread, listed under the conversation in the Task Manager and on Get Conversation. Whether the agent waits for the sub-task or lets it run on is decided when the agent creates it. A sub-task that outlives the turn reports into the conversation when it finishes (a sub_task_finished event on a live stream). A sub-task never creates background sub-tasks of its own. The sub_tasks query parameter on Run Conversation Turn and its streaming variant sets how the agent’s sub-tasks run inside the turn, not whether your call waits: sync runs every sub-task inline, async runs every sub-task in the background, auto lets the agent choose per sub-task. The setting sticks to the conversation until you change it. Turns started from Slack, Teams, WhatsApp or email always use sync, because a result landing after the channel reply has no way back to the channel.

Sending messages while a turn runs (queue and steer)

A conversation runs one turn at a time. A follow-up sent while a turn is running never runs in parallel: it either waits in the queue for the next turn, or it steers the turn that is already running. Send Conversation Message takes a mode:
  • mode=auto (default): queue the message if a turn is running. If the conversation is idle, the response is started, which means you run the turn yourself with Run Conversation Turn passing id = conversation_id.
  • mode=queue: always queue, even when idle.
  • mode=steer: hand the message to the running turn so the agent takes it into account at its next skill call, without waiting for the turn to finish. Run State tells you whether a steer is possible right now (is_steerable); the response echoes steer_target.
  • mode=interrupt: stop the running turn and run this message as the next turn.
The response is { action, message_id, queue_depth, steer_target }: queued with the new depth, steered, interrupted, or started when idle. How queued messages run:
  • If a stream is open on the running turn, the queue drains on that stream: when the turn finishes, the next message runs as the following turn. Each drain is announced with a gateway_queue_updated event (last_action: "drain").
  • If no stream is open (you queued over plain REST and disconnected, or queued while idle), call Drain Queue to run the pending messages and stream them. A queue left without a stream is also picked up on its own within a few minutes.
Manage the queue with Run State (queue_depth and previews), Cancel Queued Message (drop one) and Clear Queue (drop all). Stop cancels the running turn and keeps the queue by default.

Reading the reply

Three ways, depending on how you called:
  1. From the stream. On any streaming endpoint, turn_finished closes the turn and carries the reply in result. Sub-task results arrive as sub_task_finished events.
  2. From the conversation. Get Conversation returns the record (with status and result of the latest turn), the full thread with every turn’s messages and skill calls, token usage, and every sub-task’s record, thread and usage.
  3. From the task routes. The conversation_id and every sub-task id work with Get Task (status and result) and Get Task Thread (the thread).

Without holding a stream

For a request/response integration:
  1. Send the message with Run Conversation Turn. It waits for the reply and returns the record with result; its id is the conversation_id to continue with.
  2. Add follow-ups with Send Conversation Message using mode=queue, then run them with Drain Queue.
  3. Check progress any time with Run State (is_running, queue_depth) and read replies with Get Conversation.

Streaming

The streaming endpoints (run a turn, drain, answers, confirm, edit-stream) return Server-Sent Events. The stream opens with a connected event as soon as the request lands; each following data: line is a JSON TaskUpdateEvent, the same shape as Invoke Agent (Stream). In order: Every event carries task_id = conversation_id. The reply text streams as chunk events for every agent. A client that opens the stream without Last-Event-ID receives the latest turn from its first event and the stream ends at that turn’s turn_finished; reconnect with Last-Event-ID set to the last id: you received to get the events after it. sequence on an event grows across the whole conversation. Chunks are not stored; your messages, skill calls, plan and process updates and one agent message per turn are kept in the thread in order, so reading the conversation back later shows the same text, skill call, text sequence the stream showed.

Auth and identity

Every route is scoped to the agent in the path and authorized by your API key’s access to that agent, like the rest of the v1 API. There is no end-user session, so message and answer bodies accept an optional user object when you want to attribute the turn to a person; without it the turn runs unattributed, and a continued conversation keeps whatever user it already carries.