Conversations
Durable message threads — the transcript a session records into, readable and writable on its own.
Overview
A conversation is an ordered list of messages scoped to a
project. Every session has one underneath it
(conversation_id on the session), which is why the transcript of a session is
read through this module rather than off the session itself. A conversation can
also be used directly: create one, append messages, and generate the next reply
through an actor's linked agent.
This module is a verbatim mirror of the runtime: every field, method, status code and error shape is the runtime's own, re-rooted under the project in the path.
See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Conversations.
Data Model
Conversation
| Field | Type | Description |
|---|---|---|
id | string | Public conversation ID. |
project_id | string | The owning project. |
name | string, nullable | Optional human-readable name. |
status | string | open or closed. |
actor_id | string, nullable | The actor this conversation belongs to. |
created_at | string (date-time) | |
updated_at | string (date-time) |
ConversationMessage
| Field | Type | Description |
|---|---|---|
document_id | string | Message ID — each message is stored as a document. |
role | string | Who sent it. |
content | string | The full text. |
position | integer | Zero-based position in the thread; the ordering key. |
actor_id | string, nullable | The actor the message is attributed to. |
agent_id | string, nullable | Set on assistant messages produced by a generation. |
metadata | object, nullable | Caller-owned structured annotations. |
Key Concepts
Position is the ordering
GET /v1/projects/{project_id}/conversations/{conversation_id}/messages
returns messages ordered by position, and paginates with limit/offset over
that order.
POST …/messages takes
message and role, and appends at the end unless you name a position — so
inserting into history is possible, but deliberate.
Replay a bad answer
reads a session's thread this way to pick the position a fork branches after.
Generating the next message
POST /v1/projects/{project_id}/conversations/{conversation_id}/generate
takes the agent_id that should answer and generates the next message into the
thread. Like every generate call it is background by default: it answers
202 Accepted and the reply lands as a new message when the run finishes. Poll
the generation for status, or pass wait=true to block and
get the reply in the response.
On a managed provider the model the named agent would run
must carry a price, or the call answers 503 model_not_priced and generates
nothing; a project that owes for usage already served answers
402 insufficient_credit until it is topped up. See
A managed model must carry a price
and A negative balance stops managed generation.
A Free account that has used its plan's monthly runs answers
403 plan_limit_reached with resource: "runs" here as well, on its own
credential too. See
A plan's run allowance can stop generation.
Sessions versus conversations
A session adds agent binding, lifecycle (open/closed/
expired), inactivity expiry, message debouncing and forking on top of one
conversation. Reach for a session when you want that machinery, and for a bare
conversation when you only need a thread you control.
Tags
GET / PUT / PATCH …/tags read, replace and merge a conversation's tag map.
PUT replaces the whole map; PATCH merges keys into it.
Examples
Create a conversation and append a message
- CLI
- SDK
- curl
CONVERSATION_ID=$(naturali create-conversation \
--project-id proj_V1StGXR8Z5jdHi6B \
--name "support thread" | jq -r .id)
naturali add-conversation-message \
--project-id proj_V1StGXR8Z5jdHi6B \
--conversation-id "$CONVERSATION_ID" \
--role user \
--message "My order never arrived."
const { data: conversation } =
await naturali.conversations.createConversation({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
body: { name: 'support thread' },
});
await naturali.conversations.addConversationMessage({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
conversation_id: conversation!.id,
},
body: { role: 'user', message: 'My order never arrived.' },
});
CONVERSATION_ID=$(curl -s -X POST \
https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/conversations \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "support thread" }' | jq -r .id)
curl -X POST \
"https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/conversations/$CONVERSATION_ID/messages" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "role": "user", "message": "My order never arrived." }'
Read a session's transcript
- CLI
- SDK
- curl
naturali list-conversation-messages \
--project-id proj_V1StGXR8Z5jdHi6B \
--conversation-id conv_V1StGXR8Z5jdHi6B \
--limit 50
// `conversation_id` comes from the session record.
const { data: messages } =
await naturali.conversations.listConversationMessages({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
conversation_id: 'conv_V1StGXR8Z5jdHi6B',
},
query: { limit: 50 },
});
curl "https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/conversations/conv_V1StGXR8Z5jdHi6B/messages?limit=50" \
-H "Authorization: Bearer $NATURALI_TOKEN"
Generate the next reply and wait for it
- CLI
- SDK
- curl
naturali generate-conversation-message \
--project-id proj_V1StGXR8Z5jdHi6B \
--conversation-id conv_V1StGXR8Z5jdHi6B \
--agent-id agent_V1StGXR8Z5jdHi6B \
--wait true
const { data: reply } =
await naturali.conversations.generateConversationMessage({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
conversation_id: 'conv_V1StGXR8Z5jdHi6B',
},
query: { wait: true },
body: { agent_id: 'agent_V1StGXR8Z5jdHi6B' },
});
curl -X POST \
"https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/conversations/conv_V1StGXR8Z5jdHi6B/generate?wait=true" \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "agent_id": "agent_V1StGXR8Z5jdHi6B" }'