Conversations

The universal substrate for dialogue — human↔human, human↔agent, and agent↔agent — with identity, permissions, audit, paginated history, agent-readable notes, and real-time streaming built in.

Key Concepts

Conversations models every kind of dialogue with the same primitives, so an agent chat, a threaded email, and a multi-party group all share one API, one permission model, and one audit trail. Conversation documents use folder-scoped Buckets search, so retrieval cannot walk into sibling conversations or unrelated folders.

Projects & Conversations

A conversation is a stateful thread of messages between participants. Conversations can be grouped into projects for topic organization. Every conversation has a channel (e.g. direct, widget, or an email thread) and a type that reflects its participant mix.

Participants

Each participant is a first-class principal — user, agent, operator, or contact. The set of human participants (everything but agents) is what privacy rules are evaluated against. Participants can leave a conversation; a left_at timestamp preserves the audit trail without granting ongoing access.

Messages

A message carries a role, optional text content, rich content_blocks, attachments, and — for agent turns — tool-call metadata. A kind distinguishes triggering messages (message) from non-triggering ones (progress, thinking).

Notes

A background worker periodically summarizes new stretches of a conversation into structured notes — topic, what happened, decisions, open questions, and entities, each stamped with the message window it covers. Notes give agents a compact, searchable memory of long threads without replaying every raw message.

Conversation Lifecycle

Create a conversation, add participants and messages, then read it back. Fetching the conversation returns its metadata and participants; message history is loaded separately through the paginated messages endpoint (see below).

# Create a conversation
curl -X POST https://platform.ergondata.ai/api/v1/conversations/conversations \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "direct",
    "title": "Q3 planning",
    "starter_mode": "infer",
    "participants": [
      { "principal_type": "user", "principal_id": "{user_id}" },
      { "principal_type": "agent", "principal_id": "{agent_id}" }
    ]
  }'

# Fetch metadata + participants (no message payload)
curl "https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}?include_messages=false" \
  -H "Authorization: Bearer {token}"

GET /conversations/{id} returns an empty messages array by default. Pass include_messages=true only for small threads; for anything real, page through /conversations/{id}/messages.

Channel Mode

To open an outbound email thread, create the conversation with channel="email", a from_channel_address_id, to[], and an optional first_message. Non-workflow create and reply operations resolve the address through Channels and require channels:addresses:send.

Inbound email is bound one-to-one to a conversation by its Channels thread. A new conversation is created only when the inbound route snapshot has at least one usable notify route or auto-reply route. Notify teams expand to human participants; at most one auto-reply agent is selected.

Messages & Pagination

Message history is served through a keyset-paginated endpoint. Results are always oldest-first within a page; you walk backwards through history with the before cursor and catch up on new messages with after. Each response includes has_more plus oldest_cursor / newest_cursor message ids to use as cursors.

# Latest page (newest 50, returned oldest-first)
curl "https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/messages?limit=50" \
  -H "Authorization: Bearer {token}"

# Older page: pass the previous page's oldest_cursor as `before`
curl "https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/messages?limit=50&before={oldest_cursor}" \
  -H "Authorization: Bearer {token}"

# Time-ranged + skip heavy tool payloads
curl "https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/messages?since_ts=2026-06-01T00:00:00Z&include_tool_payloads=false" \
  -H "Authorization: Bearer {token}"
  • limit — page size (default 50, max 200; clamped server-side).
  • before / after — a message id cursor, resolved within the conversation. An unknown cursor returns 404 rather than silently falling back to the latest page.
  • since_ts / until_ts — inclusive time bounds (naive timestamps are treated as UTC).
  • include_tool_payloads — set to false to null out heavy tool_result / tool_args / content_blocks fields for a lighter scan.

Notes, Digests & Starters

Whenever an agent participates, a background worker automatically summarizes new windows of the conversation into structured notes. Each note records a topic, what_happened, decisions, open_questions, and entities, plus the window_start / window_end message range it covers.

Notes are searchable with hybrid lexical and semantic ranking plus time-window filters. Agents use this same endpoint (as the conversations.notes.search tool) to recall earlier context, and the agent runtime automatically swaps old note-covered raw messages for a compact summary when a thread grows past its context budget.

Completed notes are folded into one living digest that captures the thread's current state without discarding the underlying note history. New conversations support starter_mode:user-talks-first,preset, orinfer. Presets are deterministic; inferred starters are generated from authorized participant-scoped digest context.

# Search notes by keyword within a time window
curl "https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id}/notes?q=billing&since_ts=2026-06-01T00:00:00Z&limit=20" \
  -H "Authorization: Bearer {token}"

Notes point back at the exact message window they summarize. To drill in from a note, call the messages endpoint with the note's window_start / window_end as since_ts / until_ts.

Privacy & Note-Taking

Every conversation has a privacy_level of private (default) or public. Privacy governs whether an agent may surface this conversation's content into a different conversation it is acting in. Private content may only be surfaced when every human participant of the source is also present in the destination (destination-subset):

surface(source → acting) allowed iff humans(source) ⊆ humans(acting)

In-thread reads are never restricted, and public conversations fall back to normal permission checks. This is enforced by the agent runtime and, as defense-in-depth, by the Conversations service itself. The same destination-subset rule also gates cross-conversation writes: an agent may only post into (or add participants to) another private thread when that thread's humans are a subset of the acting conversation's — otherwise the write is refused before it happens.

Always-public conversations & the privacy lock

All-agent conversations (every participant is an agent) are forced public — there are no humans to scope to — and attempts to set them private are rejected. An individual agent can also be privacy-locked (an always-public toggle governed by agents:privacy:manage); every conversation a locked agent joins is forced public. This keeps autonomous agent activity observable rather than hidden.

Granular read permissions

Beyond full thread access, two finer grants exist. conversations:notes:view lets a principal read a conversation's notes without seeing every message — useful for observers who only need the summarized memory. agents:conversations:view (on an agent) grants derived read-only access to that agent's public conversations even without a direct grant on the thread, so a supervisor watching an agent can follow its public dialogues.

Agent-to-agent dialogue & the pause signal

Autonomous agent-to-agent (A2A) dialogue is off by default and enabled per company via the A2A settings. When enabled, an agent-sent message is relayed only to the other agent participants (never back to the sender), bounded by two loop-safety budgets: a cap on consecutive agent turns and a rate cap on agent messages per rolling window. Both are clamped to platform hard limits. When a budget is hit, the thread receives a single non-triggering status message ("Agent-to-agent exchange paused — waiting for a human.") and the exchange stops; a human reply resets the budget and the agents can resume.

Automatic note-taking

Notes run automatically whenever an agent participates in a conversation. There is no enablement setting at the company or conversation level. Cadence controls how often a note is cut — first-to-fire across every_messages, every_minutes, and every_tokens, with a min_messages floor.

# Make a conversation public and override its note cadence
curl -X PATCH https://platform.ergondata.ai/api/v1/conversations/conversations/{conversation_id} \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  -d '{
    "privacy_level": "public",
    "note_taking_cadence": { "every_messages": 15 }
  }'

The company-wide default cadence is managed under IAM settings via the company note-taking settings endpoints. A conversation can override that cadence; clearing the override restores the company default.

Real-Time Stream

Subscribe to a conversation's WebSocket stream to receive new messages, streaming agent tokens, turn-completion events, and message.updated attachment progress as they happen. Inbound attachment refs expose status and process_skip_reason. Combine the stream with paginated messages for backfill.

const ws = new WebSocket(
  `wss://platform.ergondata.ai/api/v1/conversations/ws/conversations/${conversationId}/stream?token=${token}`
);

ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  // e.g. "message.created", "message.updated", "token", "done"
  console.log(msg.type, msg);
};

Ready to build? See the Conversations API Reference for every endpoint, or the Agent Hub guide for how agents read and write conversations.