Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.wellapp.ai/llms.txt

Use this file to discover all available pages before exploring further.

A ChatConversation record represents a single AI chat session within a workspace, persisting the full message history, model-side context snapshot, UI tab state, and runtime metadata needed to resume the conversation exactly where it left off. It is workspace-scoped (every row carries a mandatory workspace FK) and is used by both the web app and the browser extension, with the source column preventing each surface from polluting the other’s recency list. The entity belongs to the Activity category and is notable for being soft-delete-capable but not exposed through the standard data-views pipeline; it has no entry in overrides.yml or composites.yml.
NamingValue
ObjectChat Conversation
Resource type (JSON:API type)chat_conversation
Collection / records rootchat_conversations
REST base/v1/chat-conversations
Entity classChatConversation

API operations

OperationMethod & pathStatus
ListGET /v1/chat-conversations✅ Implemented
RetrieveGET /v1/chat-conversations/{id}✅ Implemented
CreatePOST /v1/chat-conversations🟡 Planned
UpdatePATCH /v1/chat-conversations/{id}🟡 Planned
DeleteDELETE /v1/chat-conversations/{id}🟡 Planned

Data model

Attributes

FieldTypeRequiredConstraintsAllowed valuesDescription
thread_idstring, UUID✅ Yesunique; defaultRaw: gen_random_uuid()Public stable identifier for the conversation. Used as the resource id in all API responses and by the frontend to resume a specific session. Never changes after creation.
titlestring⚪ Novarchar(255), nullableHuman-readable label for the conversation, typically auto-generated from the first user message or set manually. NULL until the AI or user assigns one.
modestring⚪ Novarchar(20), nullableask | agent | task_flowChat mode active when the conversation was last persisted. Determines which set of AI tools and prompts the assistant uses. NULL for rows created before mode was introduced.
sourcestring⚪ Novarchar(20), nullable; partial composite index idx_chat_conversations_workspace_source_updated on (workspace_pk, source, updated_at DESC) WHERE deleted_at IS NULLweb | extensionSurface that created the conversation. The web app’s recent-conversations popover and the extension sidepanel history sheet each default-filter to their own source so neither clutters the other’s list. Conversations remain reachable from both surfaces by thread_id regardless of source. Existing rows were backfilled by Migration20260427160000 using a page_path heuristic (non-null page_path not starting with /workspaces/ → extension, otherwise → web); only API-created rows that omit the field are NULL.
page_pathstring⚪ Novarchar(512), nullableFull pathname (without origin) of the page from which the conversation was initiated or last active. Used by the frontend to restore navigation context on resume.
total_tokensinteger✅ Yesdefault 0Cumulative token count across all AI model calls in the conversation. Used for usage tracking and to surface cost attribution per conversation.
messagesjsonb (ConversationMessage[])✅ Yesdefault ’[]’; each element: { role: ‘user’|‘assistant’, content: string, parts?: PersistedPart[], ts: string (ISO) }Ordered array of all messages exchanged in the conversation. Each message carries a role, a plain-text content field, an optional structured parts array (for rich assistant output such as tool-call results, images, or embedded data), and an ISO timestamp. This is the canonical source of truth for conversation replay.
contextjsonb (ConversationContext | null)⚪ Nonullable; all sub-fields optional and nullableSnapshot of the UI and query state at the time of the last persist. Enables full-state restoration when a conversation is resumed: includes the active records root, where-clause, order-by, field selection, selection type (cells/rows/columns/none), selected cells and columns, the last executed GraphQL query and its result summary, a document-analysis summary, and a GCS path for any uploaded document.
tabsjsonb (PersistedTab[])✅ Yesdefault ’[]’; each element: { key: string, destinationId: WorkspaceDestinationId, subPath?: string }; max 50 entries (MAX_PERSISTED_TABS); subPath validated by isSafeSubPath (no ’..’ traversal, no absolute URLs, no protocol-relative paths)Open tab state at the time of last persist. Each tab carries a unique key (UUID for new_tab instances; destinationId for all other destinations) so multiple new_tab launchers survive hydration as distinct instances, and an optional subPath to carry per-tab in-flight state such as a detail route or picker selection. Replaces the legacy string-array tabs column as of Migration20260522150000.
active_tab_keystring | null⚪ Novarchar(64), nullable; must reference a key present in tabs[] — the DB does not enforce this as a FK, but the API and migration maintain the invariantPointer into tabs[] identifying which tab was active when the conversation was last persisted. References tabs[i].key (a UUID for new_tab instances; the destinationId string for all other destinations). Replaced the legacy active_path URL string in Migration20260522150000.
deleted_attimestamptz | null⚪ NonullableSoft-delete timestamp. When set, the conversation is treated as deleted and excluded from normal queries. Unlike most other entities in the platform, ChatConversation is listed in CLAUDE.md as an exception to the standard soft-delete filter pattern (alongside DataView and SessionEvent).
created_attimestamptz, 🔒 system✅ Yesset once via onCreate lifecycle hookTimestamp of conversation creation. Set automatically by the MikroORM onCreate hook; never writable via the API.
updated_attimestamptz, 🔒 system✅ Yesset on every write via onCreate + onUpdate lifecycle hooks; indexedTimestamp of the most recent update to any field on the conversation. Used by the frontend recent-conversations list to sort by recency. Carries an explicit @Index decorator for performant ORDER BY updated_at DESC queries.

Relationships

NameTypeRequiredDescription
workspaceto-one (workspace)✅ YesThe workspace this conversation belongs to. Every chat_conversations row is workspace-scoped; the foreign key is NOT NULL. The compound index idx_chat_conversations_workspace_deleted covers (workspace_pk, deleted_at) to accelerate the Hasura permission filter and the recency-sorted list query.

System-computed

  • thread_id is the public resource identifier, generated via defaultRaw: gen_random_uuid() at INSERT time. The internal pk (auto-increment integer) is never exposed via the API.
  • created_at is set once by the MikroORM onCreate lifecycle hook (new Date()) and is never subsequently modified.
  • updated_at is set by both onCreate and onUpdate lifecycle hooks, reflecting the wall-clock time of every PATCH or write. It carries an explicit @Index decorator for ORDER BY performance.
  • deleted_at uses soft-delete semantics consistent with the rest of the platform, but ChatConversation is an explicitly listed exception in apps/api/CLAUDE.md to the standard soft-delete filter (‘append-only audit log’ category).
  • messages defaults to an empty array [] at entity construction. Parts within each message element use the PersistedPart discriminated union (type-tagged), making round-tripped history messages safely hydrable from JSONB.
  • tabs defaults to an empty array [] at entity construction. As of Migration20260522150000, tabs are keyed objects (PersistedTab[]) rather than the legacy string[]. The migration backfilled all existing rows: new_tab elements received gen_random_uuid() keys; all other destinations used their destinationId as the key.
  • active_tab_key replaced the legacy active_path varchar(512) column in Migration20260522150000. The migration extracted the active destination from active_path URLs via regex prefix matching against the known WORKSPACE_DESTINATIONS segment list, then dropped the active_path column.
  • total_tokens defaults to 0 at construction and is incremented by the AI chat service after each model call. It is never reset.
  • The compound index idx_chat_conversations_workspace_deleted (workspace_pk, deleted_at) was added in Migration20260416000000 specifically to accelerate the Hasura hot-path permission filter (workspace_pk = $1 AND deleted_at IS NULL).
  • The partial composite index idx_chat_conversations_workspace_source_updated (workspace_pk, source, updated_at DESC) WHERE deleted_at IS NULL was added in Migration20260427160000 to accelerate surface-filtered recency-sorted list queries (the web app and extension history panels each filter by their own source value).
  • source was backfilled by Migration20260427160000 for all pre-existing rows using a page_path heuristic: rows with a non-null page_path not starting with /workspaces/ were set to ‘extension’; all others were set to ‘web’. Only rows created via the API without a source value remain NULL.
  • There is no sourceWorkspaceConnector relation. ChatConversation rows are created exclusively by the chat service in response to user actions; they are not produced by connector syncs.
  • source is nullable (rows created via the API without a source field). The frontend’s surface-filtering logic treats NULL as ‘unknown’ and shows those conversations on both surfaces.

Example

{
  "data": {
    "type": "chat_conversation",
    "id": "c7e1a2f3-88d4-4b9e-9c6a-3f0e1d2b5a7c",
    "attributes": {
      "thread_id": "c7e1a2f3-88d4-4b9e-9c6a-3f0e1d2b5a7c",
      "title": "Q1 invoice reconciliation",
      "mode": "agent",
      "source": "web",
      "page_path": "/workspaces/9f3e2d1a-7b4c-4e5f-8a2b-1c0d3e6f9a8b/records/invoices",
      "total_tokens": 14832,
      "messages": [
        {
          "role": "user",
          "content": "Show me all unpaid invoices from March",
          "ts": "2026-05-14T09:12:33.000Z"
        },
        {
          "role": "assistant",
          "content": "I found 7 unpaid invoices from March totalling EUR 34,200.",
          "parts": [
            { "type": "text", "text": "I found 7 unpaid invoices from March totalling EUR 34,200." }
          ],
          "ts": "2026-05-14T09:12:36.000Z"
        }
      ],
      "context": {
        "root": "invoices",
        "lastGqlQuery": "{ invoices(where: { status: { _eq: \"unpaid\" } }) { id amount } }",
        "queryResult": null,
        "columns": ["id", "amount", "status", "due_date"],
        "rowCount": 7,
        "sampleRow": { "id": "inv_001", "amount": 4800, "status": "unpaid" },
        "whereClause": { "status": { "_eq": "unpaid" } },
        "orderBy": { "field": "due_date", "direction": "asc" },
        "fields": [["id"], ["amount"], ["status"]],
        "selectionType": "rows",
        "selectedCells": null,
        "selectedColumns": null,
        "documentAnalysisSummary": null,
        "gcsPath": null
      },
      "tabs": [
        { "key": "invoices", "destinationId": "invoices" },
        { "key": "3a9c1e2d-44b7-4f0e-8c6a-2b1d3e5f7a9c", "destinationId": "new_tab", "subPath": "/workspaces/9f3e2d1a/new-tab" }
      ],
      "active_tab_key": "invoices",
      "deleted_at": null,
      "created_at": "2026-05-14T09:12:30.000Z",
      "updated_at": "2026-05-14T09:12:36.000Z"
    },
    "relationships": {
      "workspace": {
        "data": { "type": "workspace", "id": "9f3e2d1a-7b4c-4e5f-8a2b-1c0d3e6f9a8b" }
      }
    }
  }
}
Source: apps/api/src/database/entities/ChatConversation.ts · domain: workspace · tier: Activity