> ## Documentation Index
> Fetch the complete documentation index at: https://velt.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Data models

> Reference Velt data entities, fields, events, and documented absences for customization work.

> [!NOTE]
>
> This page focuses on what you need while customizing UI. The complete canonical reference is [Data models](https://velt.dev/docs/api-reference/sdk/models/data-models.md).

> [!NOTE]
>
> This list is generated from the Velt SDK and is complete for what it covers: a name that isn't listed here doesn't exist; don't guess names. The comments + notifications + core data layer is covered exhaustively (every documented field, the hook/event that surfaces it, and the API that mutates it); other-feature entities (recorder, presence, cursor, CRDT, suggestions) are summarized, and internals (deep anchor/xpath structures, transcription, full document-user shape) are documented as absences rather than fully enumerated.

The **data layer** behind every Velt feature: what Velt stores, the field names and types on each entity, the hook/event that surfaces each field at runtime, and the API that mutates it. Use this to decide whether a requirement is feasible with primitives/headless: a UI need is only achievable if the field it depends on actually exists, is exposed by a hook/event, and (if it must change) is writable via an API. Where a field or capability does **not** exist, this file says so plainly; see [`## Absences & limits`](#absences--limits).

How to read each table:

* **Field**: the property name as it appears on the object you receive.
* **Type**: its shape.
* **Meaning**: what it holds.
* **Exposed by**: the read hook (or event payload) that carries it live.
* **Writable via**: the API that changes it. `read-only` means there is no client API to set it (server-derived or auto-generated).

Cross-references: [`Hooks`](https://velt.dev/docs/ui-customization/reference/hooks) for hook signatures, [`Events`](https://velt.dev/docs/ui-customization/reference/events) for event names/payloads, [`APIs`](https://velt.dev/docs/ui-customization/reference/apis) for imperative element methods, [`Layout config`](https://velt.dev/docs/ui-customization/reference/props#part-3-layout-config-and-custom-data) for custom-data prop shapes (`customStatus`/`customPriority`/`customCategory`/`customReactions`).

The two foundational entities are `CommentAnnotation` (a **thread**: one pin, with a list of comments) and `Comment` (a single **reply** inside that thread). Most other entities hang off these.

***

## CommentAnnotation (thread)

One comment pin / thread. The root object returned by `useCommentAnnotations()`. Its `comments` array holds the individual replies.

| Field | Type | Meaning | Exposed by | Writable via |
| - | - | - | - | - |
| `annotationId` | `string` | Unique thread id. | `useCommentAnnotations`, `useCommentAnnotationById` | read-only (auto-generated) |
| `annotationNumber` | `number` | Human-facing thread number (shown on the pin). | same | read-only (auto-generated) |
| `annotationIndex` | `number` | 1-based position in the current annotation list. | same | read-only (computed) |
| `comments` | `Comment[]` | The replies in this thread (see [Comment](#comment-reply)). | same | `useAddComment` / `useUpdateComment` / `useDeleteComment` |
| `from` | `User` | The thread's creator (see [User](#user--usercontact)). | same | read-only (set at create) |
| `status` | `CustomStatus` | Workflow status object (`{id,color,name,type}`). Defaults to the built-in OPEN status. | same | `useUpdateStatus` (`updateStatus({annotationId,status})`) |
| `statusUpdatedByUserId` | `string` | Id of the user who last changed status. | same | read-only |
| `priority` | `CustomPriority` | Priority object (`{id,color,name}`). Absent until set. | same | `useUpdatePriority` |
| `commentCategories` | `CustomCategory[]` | Categories assigned to the thread. | same | set via `customCategory` config + auto/assign flows |
| `assignedTo` | `User` | The assignee, if assigned. | same | `useAssignUser` (`assignUser({annotationId,assignedTo})`) |
| `approved` | `boolean` | Whether the thread was approved (approval flow). | same | `useApproveCommentAnnotation` |
| `iam.accessMode` | `CommentAccessMode` | Public vs private access mode. | same | `useUpdateAccess` (`updateAccess({annotationId,accessMode})`) |
| `visibilityConfig` | `{type,organizationId?,userIds?}` | Visibility scope (`public` / `organizationPrivate` / `restricted`). | same | `useUpdateAccess` + `visibility` on add requests; private mode APIs |
| `resolvedByUserId` / `resolvedByUser` | `string` / `User` | Who resolved the thread. | same | set by `useResolveCommentAnnotation` |
| `context` | `any` | **Custom app data** attached to the thread (see [storage patterns](#custom-data-storage-patterns)). | same | context provider / `addContext` on the add event / `context` prop |
| `contextId` | `string` | Hash id derived from `context`. | same | read-only (derived) |
| `customList` | `CustomAnnotationDropdownItem[]` | Selected items from a custom annotation dropdown. | same | `customListDataOnAnnotation` prop + dropdown UI |
| `location` / `locationId` | `Location` / `number` | Sub-location this thread belongs to (see [Location](#location)). | same | set at create via `location`/`locationId` props |
| `targetElement` / `targetElementId` | `TargetElement` / `string` | DOM anchor the pin attaches to. | same | set at create |
| `targetTextRange` | `TargetTextRange` | Text selection range for inline/text comments. | same | set at create (text comments) |
| `position` / `positionX` / `positionY` | `CursorPosition` / `number` | Pin coordinates. | same | read-only (set at placement; drag updates) |
| `screenWidth` / `screenHeight` / `screenScrollHeight` / `screenScrollTop` | `number` | Author's screen/scroll dimensions at capture time. | same | read-only (auto-generated) |
| `pageInfo` | `PageInfo` | Page metadata (path/url/title) at creation. | same | set via `useSetPageInfo` |
| `createdAt` / `lastUpdated` | timestamp | Thread create / last-update times. | same | read-only (auto-generated) |
| `unread` | `boolean` | Whether the current user has unread content here. | same; counts via `useUnreadCommentCountByAnnotationId` | read (mark-as-read UI / actions) |
| `views` / `viewedByUserIds` / `viewedBy` | `CommentAnnotationViews` / `string[]` / `User[]` | Read-receipt data (who has seen it). See [Views](#views). | same | read-only (server-derived) |
| `subscribedUsers` / `unsubscribedUsers` / `subscribedGroups` | maps | Notification subscription state, keyed by user/group. | same | `useSubscribeCommentAnnotation` / `useUnsubscribeCommentAnnotation` |
| `involvedUserIds` / `mentionedUserIds` | `string[]` | Server-derived sets of participants / @mentioned users. | same | read-only (server-derived) |
| `ghostComment` | `GhostComment` | Present when the anchor element is gone (orphaned pin). | same | read-only |
| `isDraft` | `boolean` | Thread is an unsent draft. | same | read-only |
| `multiThreadAnnotationId` | `string` | Owning multi-thread pin id, when in multi-thread mode (see [MultiThreadCommentAnnotation](#multithreadcommentannotation)). | same | set at create |
| `color` | `string` | Pin/thread color used to render the marker. | same | read-only (SDK-assigned) |
| `metadata` | `CommentMetadata` | Thread-level metadata bag (`extends BaseMetadata`; `[key]: any`). **Distinct from `context`**: a separate per-thread bag, not the custom-context store. | same | set at create |
| `areaAnnotationId` | `string` | Linked area-annotation id (area comments). | same | read-only |
| `isPageAnnotation` | `boolean` | Thread is a page-level comment (not anchored to a DOM element). | same | read-only |
| `isDocumentLevel` | `boolean` | When `true`, the thread belongs to every location of its document: listed on every location, its pin drawn only where its target element is found and visible. Only `true` counts; never on a text comment (`targetTextRange`). See [`CommentAnnotation`](https://velt.dev/docs/api-reference/sdk/models/data-models#commentannotation). | same | read-only (set by the checks engine on site comments) |
| `checkInfo` | `CheckInfo` | The checks engine's metadata on a thread that belongs to a check (see [`CheckInfo`](https://velt.dev/docs/api-reference/sdk/models/data-models#checkinfo) and [Checks Overview](https://velt.dev/docs/api-reference/rest-apis/v2/agents/checks/overview.md)). | same | read-only (written only by the checks engine) |
| `targetInlineCommentElementId` | `string` | Target element id for an inline-comment section. | same | set at create |
| `inlineCommentSectionConfig` | `{ id: string, name?: string }` | The inline-comment section this thread belongs to. | same | set at create |
| `selectAllContent` | `boolean` | Text comment spans all text content of the `targetElementId`. | same | set at create (text comments) |
| `sourceId` | `string` | Caller-supplied source id stamped on the thread (e.g. via `addManualComment`). | same | set at create |
| `commentType` / `type` | `string` | Comment kind / annotation type (`'comment'` default). | same | set at create |
| `sourceType` | `string` | `'agent'` marks an AI-authored thread (blue pin + agent header). | same | read-only (stamped server-side) |
| `agent` | `AgentData` | AI agent identity + output (see [AgentData](#agentdata)). Present on agent threads. | same | read-only (written upstream) |
| `suggestion` | `SuggestionData` | Suggestion-mode data; present iff `type === 'suggestion'`. | same | Suggestion element API (not a direct setter) |
| `resolved` / `inProgress` / `documentParams` / `documentParamsId` | None | **Deprecated.** Use `status` and `location`/`locationId` instead. | None | |

### MultiThreadCommentAnnotation

The grouping pin that holds **multiple comment threads** at one anchor, used when multi-thread mode is on (`enableMultiThreadMode()`). Each child `CommentAnnotation` points back to it via [`CommentAnnotation.multiThreadAnnotationId`](#commentannotation-thread). It is not a thread itself: it is a container whose `commentAnnotationsMap` holds the child threads.

| Field | Type | Meaning |
| - | - | - |
| `annotationId` | `string` | Unique multi-thread pin id (the value child threads carry in `multiThreadAnnotationId`). |
| `commentAnnotationsMap` | `{ [commentAnnotationId]: CommentAnnotation }` | The child threads grouped under this pin, keyed by their id. |
| `commentAnnotations` | `{ [commentAnnotationId]: boolean }` | Membership map of child thread ids (presence flags). |
| `from` | `User` | Creator of the multi-thread pin. |
| `location` / `locationId` | `Location` / `number` | Sub-document scope. |
| `position` | `CursorPosition` | Pin coordinates. |
| `targetElement` / `targetElementId` | `TargetElement` / `string` | DOM anchor the pin attaches to. |
| `targetTextRange` | `TargetTextRange` | Text selection range (inline/text multi-thread). |
| `targetInlineCommentElementId` / `inlineCommentSectionConfig` | `string` / `{ id, name? }` | Inline-comment section anchoring. |
| `areaAnnotationId` | `string` | Linked area-annotation id (area multi-thread). |
| `type` | `string` | Annotation kind. |
| `isDraft` | `boolean` | Pin is an unsent draft. |
| `sourceId` | `string` | Caller-supplied source id. |
| `metadata` | `MultiThreadMetadata` | Metadata bag (`extends BaseMetadata`). |
| `ghostComment` / `context` | `GhostComment` / `any` | Orphaned-pin marker / custom context bag. |

All fields are read-only from the SDK (set at placement / managed); children are mutated through the normal comment APIs on each `CommentAnnotation`.

## Comment (reply)

A single message inside a thread's `comments` array. Not fetched on its own as a reactive stream: read it from the parent `CommentAnnotation.comments`, or imperatively via `useGetComment()` (`await getComment({annotationId})` → `Comment[]`).

| Field | Type | Meaning | Exposed by | Writable via |
| - | - | - | - | - |
| `commentId` | `number` | Unique id within the thread. | `CommentAnnotation.comments[]`, `useGetComment` | read-only (auto-generated) |
| `commentText` | `string` | Plain-text body. | same | `useUpdateComment` |
| `commentHtml` | `string` | Rich-text (HTML) body. | same | `useUpdateComment` |
| `from` | `User` | The reply's author. | same | read-only (set at create) |
| `to` | `User[]` | Users @mentioned in this reply. | same | set at create (mentions) |
| `taggedUserContacts` | `AutocompleteUserContactReplaceData[]` | Resolved mention contacts. | same | set at create |
| `status` | `'added' \| 'updated'` | Lifecycle marker of the reply (newly added vs subsequently updated). | same | read-only (set by add/update) |
| `customList` | `AutocompleteReplaceData[]` | Selected items from a custom autocomplete list on the reply. | same | `createCustomListDataOnComment` + composer UI |
| `toOrganizationUserGroup` | `AutocompleteGroupReplaceData[]` | Group `@mentions` (org/team mentions) on the reply. | same | set at create (group mentions) |
| `type` | `'text' \| 'voice'` | Content kind. | same | set at create |
| `commentVoiceUrl` | `string` | Voice-note URL (voice comments). | same | set at create |
| `attachments` | `Attachment[]` | Files on this reply (see [Attachment](#attachment)). | same | `useAddAttachment` / `useDeleteAttachment` |
| `recorders` | `RecordedData[]` | Recordings on this reply (see [RecordedData](#recordeddata)). | same | `useGetRecording` / `useDeleteRecording` |
| `reactionAnnotations` / `reactionAnnotationIds` | `ReactionAnnotation[]` / `string[]` | Reactions on this reply (see [ReactionAnnotation](#reaction--reactionannotation)). | same | `useAddReaction` / `useToggleReaction` / `useDeleteReaction` |
| `createdAt` | timestamp | When the reply was posted. | same | read-only (auto-generated) |
| `lastUpdated` | `Date` | When the reply was last touched. | same | read-only (auto-generated) |
| `isEdited` | `boolean` | Whether the reply was edited after posting. | same | read-only (set by edit) |
| `editedAt` | timestamp | When it was last edited. | same | read-only (set by edit) |
| `isDraft` | `boolean` | Reply is an unsent draft. | same | read-only |
| `metadata` | `any` | **Customer-supplied per-reply data**, persisted as-is (see [storage patterns](#custom-data-storage-patterns)). Only present when a caller provides it. | same | set on the `Comment` passed to `useAddComment`/`useUpdateComment` |
| `context` | `any` | Per-reply context bag. | same | set on the `Comment` at add time |
| `replaceContentHtml` / `replaceContentText` | `string` | Replacement content for accepted suggestions. | same | set at create (suggestion flows) |
| `sourceType` | `string` | `'agent'` for agent-authored replies. | same | read-only (server-side) |
| `agent` | `AgentData` | Agent identity for an agent reply. | same | read-only (written upstream) |

> **No per-edit author identity.** `Comment` records `isEdited`/`editedAt` but **not who** edited it: see [Absences](#absences--limits).

## User / UserContact

`User` is the identified user (yourself, or any `from`/`assignedTo`/`to` actor on a thread). `UserContact` is a lighter shape used for mention/assignee lists.

`User` fields: `userId` (your id), `name`, `email`, `photoUrl`, `color`, `textColor` (avatar colors), `initial`, `plan`, `groupId`, `organizationId`, `type`, `isAdmin`, `isReadOnly`, `isAnonymous`, `isGuest`, `userSnippylyId` (internal id used as the key in `views`), `contacts` (`UserContact[]`).

`UserContact` fields: `userId`, `name`, `email` (required), `photoUrl`, `groupId`, `visibility` (`'group' \| 'private'`), `color`, `textColor`, `initial`.

| Read | Exposed by | Writable via |
| - | - | - |
| Current identified user | `useCurrentUser()` → `User \| null` | `useIdentify()` (`identify(user)`) |
| Current user's permissions | `useCurrentUserPermissions()` | permission provider |
| Mention/contact list | `useContactList()`, `useContactUtils()` | `customAutocompleteSearch` + `ContactElement` |
| Selected contact (mention/assign) | `useContactSelected()` → `SelectedUserContact` | None |

`SelectedUserContact` wraps a `UserContact` with `isOrganizationContact`, `isDocumentContact`, `documentAccessType`. The `assignUser` API takes a `UserContact` as `assignedTo`. All user identity originates from `identify()`; the SDK adds `color`/`textColor`/`userSnippylyId`/`initial`.

## Reaction / ReactionAnnotation

A `ReactionAnnotation` is the container pinned to a comment (or element); it holds a `reactions` array of individual `Reaction`s.

`ReactionAnnotation` fields: `annotationId`, `commentAnnotationId` (the comment it's attached to), `reactions` (`Reaction[]`), `from` (`User`), `targetElement`/`targetElementId`, `position` (`CursorPosition`), `location`/`locationId`, `type` (`string`), `annotationIndex` (`number`, 1-based), `pageInfo` ([`PageInfo`](#pageinfo)), `icon`/`iconUrl`/`iconEmoji`, `lastUpdated`, `metadata`, `involvedUserIds` (server-derived), and `context` (typed `Context`: `{ access: {[key]: string|number}, accessFields?: string[] }`: **not** a free-form `any` bag, unlike `CommentAnnotation.context`).

`Reaction` fields: `variant` (emoji variant), `from` (`User`), `lastUpdated`.

| Read | Exposed by | Writable via |
| - | - | - |
| Reactions on a comment | `CommentAnnotation.comments[].reactionAnnotations` | `useAddReaction` / `useDeleteReaction` / `useToggleReaction` |
| Reaction events | `useCommentEventCallback('addReaction' \| 'deleteReaction' \| 'toggleReaction')` | None |

The emoji set is defined by the `customReactions` prop (`ReactionMap`: `{ [reactionId]: { url?, svg?, emoji? } }`). Reaction add/delete/toggle requests reference a `reactionId` and optional `customReaction` (`ReactionItem`).

## Attachment

A file on a comment.

| Field | Type | Meaning | Exposed by | Writable via |
| - | - | - | - | - |
| `attachmentId` | `number` | Unique id. | `Comment.attachments[]`, `useGetAttachment` | read-only (auto-generated) |
| `name` | `string` | File name. | same | set on upload |
| `size` | `number` | File size in bytes. | same | read-only |
| `type` | `RecorderFileFormat` | File type. | same | read-only |
| `mimeType` | `any` | MIME type. | same | read-only |
| `url` | `string` | Download URL. | same | read-only (server-set) |
| `bucketPath` | `string` | Storage path. | same | read-only |
| `thumbnail` / `thumbnailWithPlayIconUrl` / `previewImages` | `string` / `string[]` | Preview images. | same | read-only |
| `metadata` | `any` | Arbitrary attachment metadata. | same | set on upload (`metadata` in upload options) |
| `isAttachmentResolverUsed` | `boolean` | Whether this attachment was served via the attachment resolver (self-hosting). | same | read-only (server-derived) |

Mutate via `useAddAttachment` (`addAttachment({annotationId,files})`), `useDeleteAttachment`, `useGetAttachment`. `allowedFileTypes` prop whitelists types.

## CustomStatus / CustomPriority / CustomCategory

Definition objects you supply via props/APIs to populate the status/priority/category controls. They share a base `{id, color, name}` (`CustomFilter`).

| Type | Fields | Defined via | Lands on |
| - | - | - | - |
| `CustomStatus` | `id, color, name, type:'default'\|'ongoing'\|'terminal', lightColor?, svg?, iconUrl?` | `customStatus` prop or `commentElement.setCustomStatus([...])` | `CommentAnnotation.status` |
| `CustomPriority` | `id, color, name, lightColor?, svg?, iconUrl?` | `customPriority` prop | `CommentAnnotation.priority` |
| `CustomCategory` | `id, color, name` | `customCategory` prop | `CommentAnnotation.commentCategories[]` |

The `type` on `CustomStatus` drives lifecycle semantics (`terminal` ≈ resolved/done). See [`Layout config`](https://velt.dev/docs/ui-customization/reference/props#part-3-layout-config-and-custom-data) for the exact prop shapes and examples. **Right-tool note:** statuses/priorities are a small fixed workflow vocabulary; for arbitrary per-thread labels prefer `customCategory` or the thread `context` bag rather than overloading status.

## Views

Read-receipt / view-tracking data. Surfaced on `CommentAnnotation.views` and through the views feature.

`CommentAnnotationViews` = `{ views: { [userSnippylyId]: { timestamp } }, comments: { [commentId]: { views: {...} } }, metadata? }`. So per-thread and per-comment "seen by whom and when" is keyed by `userSnippylyId` (the internal user id on `User`). `Views` also has `documentViews` and `locationViews` (aggregated by user and by date).

| Read | Exposed by | Writable via |
| - | - | - |
| Per-thread / per-comment views | `CommentAnnotation.views`, `.viewedBy`, `.viewedByUserIds` | read-only (server-derived) |
| Aggregate analytics | `useUniqueViewsByUser`, `useUniqueViewsByDate`, `useViewsUtils` | read-only |

## Location

A sub-document scope. Threads, reactions, presence and cursors all carry a `location`.

`Location` fields: `id`, `locationName`, `version` (`{id, name}`), **plus arbitrary custom keys** (`[key: string]: any`): see [storage patterns](#custom-data-storage-patterns). Set via `useSetLocation`/`useSetLocations`/`useSetRootLocation` (or the `location`/`locationId` props on comment surfaces). Filterable in the sidebar via the location filter. The SDK derives a numeric `locationId` hash from the `Location` object.

## PageInfo

Page metadata captured when an annotation is created, plus per-feature deep-link URLs. Surfaced on [`CommentAnnotation.pageInfo`](#commentannotation-thread) and `ReactionAnnotation.pageInfo` (read); written via `useSetPageInfo()` (`{ setPageInfo }`) / cleared via `useClearPageInfo()`.

| Field | Type | Meaning |
| - | - | - |
| `url` | `string` | Full URL of the page. |
| `path` | `string` | Path, excluding the base URL. |
| `queryParams` | `string` | Query-string of the page. |
| `baseUrl` | `string` | Base URL (domain). |
| `title` | `string` | Page title. |
| `screenWidth` | `number` | Author's screen width (auto-generated). |
| `deviceInfo` | `IDeviceInfo` | Device/screen info. |
| `commentUrl` / `recorderUrl` / `tagUrl` / `areaUrl` / `arrowUrl` | `string` | Deep-link reference URLs to the respective annotation type. |

All fields are optional. Read-only except via `useSetPageInfo` (you supply the override; the SDK auto-fills `screenWidth`/`deviceInfo`).

## CustomAnnotationDropdownData / CustomAnnotationDropdownItem

The shape behind a thread's [`CommentAnnotation.customList`](#commentannotation-thread): a custom dropdown attached to an annotation via `createCustomListDataOnAnnotation(data)` (see [`APIs`](https://velt.dev/docs/ui-customization/reference/apis)).

* `CustomAnnotationDropdownItem` = `{ id: string, label: string }`: a single selectable option.
* `CustomAnnotationDropdownData` = `{ type: 'multi' | 'single', placeholder?: string, data: CustomAnnotationDropdownItem[] }`: the dropdown definition (`type` controls single vs multi-select). The selected items land on `CommentAnnotation.customList` as `CustomAnnotationDropdownItem[]`.

> **Not the same as `Comment.customList`.** That one is a per-reply autocomplete list (`AutocompleteReplaceData[]`), set via `createCustomListDataOnComment`. The annotation-level dropdown is `CustomAnnotationDropdownItem[]`.

## TargetTextRange

The anchored text selection on a text/inline comment ([`CommentAnnotation.targetTextRange`](#commentannotation-thread)). Most fields are xpath internals (`commonAncestorContainer`, `commonAncestorContainerFXpath`, `commonAncestorContainerCFXpath`, `commonAncestorContainerAnchor`, `occurrence`) used to re-resolve the selection in the DOM. The one field you will typically use:

* **`text`** (`string`): the actual selected text the comment was anchored to. Read this to show "commented on: …". The remaining fields are anchor-resolution internals; treat them as opaque.

## AgentData

Identity + output of an AI agent that authored a thread/reply. Present on `CommentAnnotation.agent` and `Comment.agent` when `sourceType === 'agent'`.

| Field | Type | Meaning |
| - | - | - |
| `agentName` | `string` | Canonical agent display name (card header). |
| `name` | `string` | Legacy display name; superseded by `agentName`. |
| `avatar` | `string` | Agent avatar URL (falls back to a default icon). |
| `result` | `AgentResult` | Structured output; `result.title` is the bold headline on the suggestion card. |
| `agentFields` | `string[]` | Queryable agent attributes; matchable via `CommentRequestQuery.agentFields`. |

**Read-only from the SDK**: written upstream by the agent; there is no client API to author or edit `agent`. The agent-suggestion UI renders only when `agent` is present, `type === 'suggestion'`, and the viewer is an admin.

## Notification

A single notification row. Read with `useNotificationsData(query?)` → `Notification[]`.

| Field | Type | Meaning | Writable via |
| - | - | - | - |
| `id` | `string` | Notification id. | read-only |
| `notificationSource` | `string` | Source feature (e.g. `comment`). | read-only |
| `actionType` | `string` | What happened (e.g. added/updated). | read-only |
| `isUnread` | `boolean` | Unread for this user. | read (mark-read flows on the panel) |
| `isNotificationResolverUsed` | `boolean` | Whether this notification's copy was produced by the notification resolver. | read-only |
| `isCommentResolverUsed` | `boolean` | Whether the comment text shown came via the comment resolver (self-hosting). | read-only |
| `actionUser` | `User` | Who triggered it. | read-only |
| `timestamp` | `number` | When it occurred. | read-only |
| `forYou` | `boolean` | Whether it targets the current user ("For You" tab). | read-only |
| `displayHeadlineMessage` / `displayBodyMessage` | `string` | Rendered text. | read-only (template-driven) |
| `displayHeadlineMessageTemplate` / `displayBodyMessageTemplate` (+ `…TemplateData`) | `string` / object | Message templates + variables (override copy). | resolver / template config |
| `targetAnnotationId` | `string` | The annotation this notification points to. | read-only |
| `metadata` | `NotificationMetadata` | `documentId`, `locationId`, `location`, org ids. | read-only |
| `notifyUsers` / `notifyUsersByUserId` | maps | Recipients by email / user id. | read-only |
| `notificationSourceData` | `any` | Raw source payload. | read-only |

### NotificationRawData (the notification resolver input)

When you customize notification copy through the **notification resolver** (or templates), the SDK builds a raw notification object first: `NotificationRawData`: which is the shape the resolver round-trip is formed from. This is what your resolver receives. Key fields you can read/branch on:

| Field | Type | Meaning |
| - | - | - |
| `oldStatus` / `newStatus` | `string` | Status before/after a status-change notification. |
| `oldAccess` / `newAccess` | `string` | Access mode before/after an access-change notification. |
| `annotationData` | `any` | The triggering annotation object (`targetAnnotationData` holds the specific comment). |
| `customData` | `any` | Arbitrary custom data carried through to the resolver/template. |
| `notifyUsers` | `{ [email]: boolean }` | Recipient map (by email hash). |
| `actionUser` / `actionType` / `notificationSource` | `User` / enum / enum | Who acted, what happened, and the source feature. |
| `displayHeadlineMessageTemplate` / `displayBodyMessageTemplate` (+ `…TemplateData`) | `string` / object | Template + variables you override to customize copy. |

This is the raw, server-shaped input: distinct from the `Notification` row above (the resolved object you read in the UI via `useNotificationsData`).

Counts via `useUnreadNotificationsCount()` → `{ forYou, all }`. Settings via `useNotificationSettings()` (`settings`, `setSettings`, `setSettingsInitialConfig`). Panel tabs configured by `tabConfig` (`forYou`/`documents`/`all`/`people`). Notification content itself is **not** mutated by a client write API: customize copy through templates/resolvers, behavior through props/settings.

## PresenceUser

A user currently present on the document. Read with `usePresenceData(query?)` → `GetPresenceDataResponse` (or the deprecated `usePresenceUsers()` → `PresenceUser[]`).

| Field | Type | Meaning |
| - | - | - |
| `userId` | `string` | User id. |
| `name` / `email` / `photoUrl` | `string` | Identity (from `identify`). |
| `onlineStatus` | `string` | `active` / `inactive` / `offline` (auto-managed). |
| `color` / `textColor` / `initial` | `string` | Avatar colors / initial. |
| `timestamp` | any | Last heartbeat time. |
| `location` / `locationId` | `Location` / `number` | Where the user is present. |
| `isTabAway` / `isUserIdle` | `boolean` | Activity flags. |
| `isReadOnly` / `isAnonymous` | `boolean` | Access flags. |
| `localOnly` | `boolean` | Custom user added via `addUser`, not persisted. |
| `userSnippylyId` | `string` | Internal id. |

All presence fields are **read-only** (server-/SDK-managed); identity comes from `identify()`. Change events: `usePresenceEventCallback('multipleUsersOnline' \| 'userStateChange')`. Inactivity thresholds set via `VeltPresence` props (`inactivityTime`, `offlineInactivityTime`).

## CursorUser

A user's live cursor. Read with `useCursorUsers()` → `CursorUser[]`.

| Field | Type | Meaning |
| - | - | - |
| `userId` / `name` / `email` / `photoUrl` | None | Identity. |
| `positionX` / `positionY` | `number` | Live mouse position. |
| `comment` | `string` | Short note attached to the cursor. |
| `onlineStatus` | `string` | `active` / `inactive` / `offline`. |
| `color` / `textColor` / `initial` | `string` | Cursor colors / initial. |
| `screenWidth` / `screenHeight` / `screenScrollHeight` | `number` | Author screen dimensions. |
| `targetElement` / `position` | `TargetElement` / `CursorPosition` | Anchor for the cursor. |
| `location` / `locationId` | `Location` / `number` | Scope. |
| `timestamp` | any | Last update. |

All **read-only** (auto-generated live). Change event: `onCursorUserChange` on `VeltCursor`. Avatars on cursors via the `avatarMode` prop.

## RecordedData

A finished recording attached to a comment. Read with `useGetRecording()` (imperative) or from `Comment.recorders[]`; also delivered by `useRecorderAddHandler()`.

| Field | Type | Meaning |
| - | - | - |
| `id` | `string` | Recorder annotation id. |
| `type` | `string` | `audio` / `video` / `screen`. |
| `videoUrl` / `audioUrl` | `string` | Media URLs. |
| `thumbnailUrl` / `thumbnailWithPlayIconUrl` | `string` | Poster images. |
| `videoPlayerUrl` | `string` | Hosted player URL. |
| `tag` | `string` | Embeddable player tag. |
| `chunkUrls` | `{ [n]: string }` | Per-chunk upload URLs. |

Mutate via `useDeleteRecording` / `useGetRecording`. Recorder change events via `useRecorderEventCallback(...)` and the recorder events in [`Events`](https://velt.dev/docs/ui-customization/reference/events) (`recordingDone`, `transcriptionDone`, …). Recording media URLs are server-set (read-only).

## ActivityRecord

A single activity-feed event. Read with `useAllActivities(config?)` → `ActivityRecord[]`.

| Field | Type | Meaning |
| - | - | - |
| `id` | `string` | Activity id. |
| `featureType` | `'comment'\|'reaction'\|'recorder'\|'crdt'\|'custom'` | Originating feature. |
| `actionType` | `string` | `entity_type.action` (e.g. `comment_annotation.status_change`, `comment.add`). |
| `actionUser` | `User` | Who acted. |
| `timestamp` | `number` | When (server-set). |
| `targetEntityId` | `string` | Parent entity id (e.g. `annotationId`). |
| `targetSubEntityId` | `string \| null` | Sub-entity id (e.g. `commentId`); null for entity-level actions. |
| `changes` | `{ [key]: { from?, to? } }` | From/to change pairs (e.g. `status`, `assignedTo`). |
| `entityData` / `entityTargetData` | snapshot | Full parent / sub-entity snapshot at action time. |
| `displayMessage` | `string` | Computed display text (never stored). |
| `displayMessageTemplate` (+ `…TemplateData`) | `string` / object | **Custom activities only**: your own message template. |
| `actionIcon` | `string` | Icon URL/identifier. |
| `immutable` | `boolean` | If true, cannot be updated/deleted via REST. |

Scope/filter via `ActivitySubscribeConfig` (`documentIds`, `featureTypes`, `actionTypes`, `userIds`, `maxDays`, …). Reading is reactive; activities are written by the features themselves (custom activities are created server-side / via REST, not through a React mutation hook).

## LiveStateData

Arbitrary application state synced across collaborators. Read/write with `useLiveStateData` / `useSetLiveStateData` (and `useUiState` for shared UI state).

| Field | Type | Meaning | Writable via |
| - | - | - | - |
| `id` | `string` | MD5 of `liveStateDataId`. | read-only (derived) |
| `liveStateDataId` | `string` | Your sync key. | set at write |
| `data` | `string\|number\|boolean\|JSON` | The synced value (this is your free-form payload). | `useSetLiveStateData` (`merge?` to deep-merge) |
| `updatedBy` | `User` | Who last wrote it. | read-only (auto) |
| `lastUpdated` | any | Last write time. | read-only (auto) |
| `tabId` | `string \| null` | Originating tab. | auto |

This is the **general-purpose escape hatch** for syncing custom data that has no home on any other entity. Single-editor-mode state (`SingleEditorLiveStateData`: `editor`, `requestEditorAccess`) rides the same feature; access events are in [`Events`](https://velt.dev/docs/ui-customization/reference/events) under Live State Sync.

***

## Custom-data storage patterns

Four places hold arbitrary application data. Pick by lifetime and scope:

| Mechanism | Shape | Set via | Surfaces on | Use for |
| - | - | - | - | - |
| **Thread `context`** | `context?: any` | `useSetContextProvider()` (a `(documentId, location?) => context` provider), the `addContext(context)` callback on the `addCommentAnnotation` event, or the `context` prop on comment surfaces (`VeltComments`, `VeltCommentTool`, `VeltCommentPin`, `VeltCommentBubble`, sidebars, inline sections) | `CommentAnnotation.context` (+ derived `contextId`) | App state a thread was created against (record id, route, selection metadata). Filterable via the sidebar custom filter. |
| **Reply `metadata`** | `metadata?: any` | the `metadata` field on the `Comment` you pass to `useAddComment` / `useUpdateComment` | `Comment.metadata` | Per-reply app data persisted as-is (only present when you set it). |
| **`Location` custom fields** | `{ id, locationName, version?, [key]: any }` | `useSetLocation` / `useSetLocations` | `CommentAnnotation.location`, `PresenceUser.location`, `CursorUser.location` | Sub-document scoping + extra attributes that travel with the scope. |
| **`CustomCategory`** | `{ id, color, name }` | `customCategory` prop | `CommentAnnotation.commentCategories[]` | A fixed, filterable label vocabulary (the right tool for tag-like categorization). |

For larger/looser app state with no entity home, use **`LiveStateData`** (above). The `context` bag is the most common choice for "attach my record id to this thread."

## Absences & limits

State these as confident "not supported" verdicts:

* **Per-edit editor identity is not stored.** `Comment` has `isEdited` and `editedAt` but no field for *who* edited a reply. "Who edited this reply" **cannot** be shown client-side from the model. Only the original author (`Comment.from`) is available.
* **No per-reply `createdBy`-vs-`editedBy` distinction.** `from` is the author; there is no separate last-editor user object on `Comment` or `CommentAnnotation` (only `statusUpdatedByUserId` and `resolvedByUserId` exist, scoped to those specific actions).
* **`agent`, `suggestion`, `involvedUserIds`, `mentionedUserIds`, `views`/`viewedBy` are read-only.** They are written upstream/server-side; no client API mutates them. UI that *displays* them is feasible; UI that *edits* them is not (for suggestions, use the Suggestion element API, not a field write).
* **Notification content has no client write API.** You cannot mutate a `Notification` object directly. Change copy through message templates / the notification resolver, and behavior through `tabConfig` / settings, not by writing the entity. This forces a template/resolver (server-side) path for custom notification text.
* **Presence/cursor fields are fully managed.** `onlineStatus`, positions, colors, timestamps are auto-generated; you influence them only through identity (`identify`) and config props (`inactivityTime`, `avatarMode`, …), never by writing the field.
* **Activity records are not mutated from React.** Reading is reactive (`useAllActivities`); creating/updating custom activities and deleting records is a server-side/REST path, and `immutable` records reject REST mutation entirely.
* **`context` is set, not patched per-field client-side.** It is provided wholesale (provider/`addContext`/prop) and hashed into `contextId`; there is no React hook to mutate one key of an existing thread's `context` in place (server REST is the path for editing stored context).

## Runtime-access map

Quick lookup: entity → the hook that reads it live → the event that fires on change.

| Entity | Read hook | Change event (`.on(...)` / `use…EventCallback`) |
| - | - | - |
| `CommentAnnotation` (all) | `useCommentAnnotations()` | `addCommentAnnotation`, `deleteCommentAnnotation`, `updateStatus`, `updatePriority`, `assignUser`, `updateAccess`, `resolveComment` |
| `CommentAnnotation` (one) | `useCommentAnnotationById({annotationId})` | same |
| `CommentAnnotation` (filtered) | `useGetCommentAnnotations(query?)` | same |
| `Comment` (reply) | `CommentAnnotation.comments[]` / `useGetComment()` | `addComment`, `updateComment`, `deleteComment` |
| `User` (current) | `useCurrentUser()` | `userUpdate` (core) |
| `UserContact` (lists) | `useContactList()` / `useContactSelected()` | `autocompleteSearch` |
| `ReactionAnnotation` / `Reaction` | via `Comment.reactionAnnotations` | `addReaction`, `deleteReaction`, `toggleReaction` |
| `Attachment` | via `Comment.attachments` / `useGetAttachment()` | `addAttachment`, `deleteAttachment`, `attachmentDownloadClicked` |
| `RecordedData` | `useGetRecording()` / `useRecorderAddHandler()` | recorder events (`recordingDone`, `transcriptionDone`, `deleteRecording`) |
| `Notification` | `useNotificationsData()` / `useUnreadNotificationsCount()` | `settingsUpdated` |
| `PresenceUser` | `usePresenceData()` | `multipleUsersOnline`, `userStateChange` |
| `CursorUser` | `useCursorUsers()` | `onCursorUserChange` (`VeltCursor` prop) |
| `ActivityRecord` | `useAllActivities(config?)` | (reactive stream; no separate change event) |
| `LiveStateData` | `useLiveStateData()` / `useUiState()` | live-state-sync events (`editorAssigned`, `accessRequested`, …) |
| `Views` | `CommentAnnotation.views` / `useUniqueViewsByUser` | (reactive; server-derived) |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.