---
title: "Create a comment thread or reply."
method: POST
path: "/v1/dashboards/{id}/comments"
tags: ["comments"]
---

# Create a comment thread or reply.

`POST /v1/dashboards/{id}/comments`

## Request

- Base URL: `http://localhost:8080`
- URL: `http://localhost:8080/v1/dashboards/{id}/comments`
- Auth: one of:
  - API key in cookie `dvt_session`
  - HTTP bearer

Creates a new top-level thread (optionally anchored to a page, or a page + element)
or, with `parentId` set, a reply to an existing top-level thread. The author fields
(`authorId`, `actorType`) are always taken from the authenticated caller — never
from the request body. Requires dashboard read-level access on this dashboard (any
viewer may comment) plus the `comment:write` capability.

## Request body

- CreateCommentRequest
  - `body` string, required: Non-empty after trimming whitespace.
  - `pageId` string: Anchors a new top-level thread to this page — its UUID or its slug. Must be a live page of this dashboard. Omit for an unanchored thread, or when parentId is set (a reply inherits its parent's anchor).
  - `elementId` string: Anchors a new top-level thread to this element within pageId — its UUID or its slug. Requires pageId. Must resolve to a live element on that page.
  - `parentId` string, uuid: Makes this a reply to the named top-level thread. Mutually exclusive with pageId/elementId (a reply inherits its parent's anchor). The parent must be a live top-level comment in this dashboard — a missing, cross-tenant, or reply parentId is rejected with 400.

## Response `201`

Comment created.

- CommentEnvelope
  - `data` Comment, required: A dashboard comment (DVT-1179): a top-level, optionally page/element-anchored, resolvable thread starter, or a reply that inherits its parent thread's anchor and resolved state rather than carrying its own. Threading is single-level — a reply may not itself be replied to.
    - `id` string, uuid, required
    - `dashboardId` string, uuid, required
    - `pageId` string, uuid, nullable: The page this top-level thread is anchored to, if any (always the page's canonical UUID, regardless of whether it was created by UUID or slug). Null for an unanchored thread or for a reply — see parentId.
    - `elementId` string, uuid, nullable: The element (panel) this top-level thread is anchored to, if any (always the element's canonical UUID). Only set together with pageId. Null for a reply.
    - `pageName` string, nullable: The anchored page's current title, resolved from the dashboard's live state at read time (DVT-1188) — never stored, so it always reflects the page's latest title rather than whatever it was when the comment was created. Omitted when pageId is null (unanchored), or when pageId no longer resolves to a live page (e.g. the page was since deleted).
    - `elementName` string, nullable: The anchored element's current title, resolved from the dashboard's live state at read time (DVT-1188) — never stored, so it always reflects the element's latest title rather than whatever it was when the comment was created. Omitted when elementId is null, or when elementId no longer resolves to a live element on that page (e.g. the panel was since deleted). A reply inherits its parent's elementName exactly as it inherits elementId (see replies below).
    - `parentId` string, uuid, nullable: Set only on a reply — the id of the top-level thread it belongs to.
    - `authorId` string, uuid, required
    - `actorType` 'user' | 'agent' | 'system', required: Distinguishes humans from agents, matching the audit log convention.
    - `body` string, required
    - `editedAt` string, date-time, nullable: Set when the author has edited body since creation.
    - `resolvedAt` string, date-time, nullable: Top-level threads only; always null on a reply.
    - `resolvedBy` string, uuid, nullable
    - `resolved` boolean, required: Convenience derived field — true iff resolvedAt is set.
    - `replyCount` integer, required: Number of live replies under this thread. Populated by GET (list); 0 on a create/update/resolve response (those never batch-load replies).
    - `replies` Comment[]: Live replies, oldest-first. Populated by GET (list) only; omitted elsewhere. A reply's own replies/replyCount are always empty/0 (single-level threading).
    - `createdAt` string, date-time, required
    - `updatedAt` string, date-time, required
  - `meta` Meta, required: Response metadata.
    - `requestId` string, required: Correlates with the X-Request-Id header.
    - `total` integer: Item count, present on collection responses.
    - `nextCursor` string, nullable: Opaque keyset-pagination cursor, present on paginated responses (e.g. GET /v1/dashboards) when more rows exist beyond this page. Pass it back as the `cursor` query param to fetch the next page; absent or null on the last page.
    - `roleResolveDegraded` boolean: Snowflake mode only (DVT-2260), on GET /v1/dashboards: present and true when the caller's live Snowflake role resolution failed or was unavailable — role-shared dashboards may be missing from this page. Omitted (not merely false) whenever resolution succeeded or the role-share arm does not apply (off snowflake mode, an org admin/owner, or off the ingress listener).

## Other responses

- `400`: Malformed request.
- `401`: Missing, invalid, or expired session.
- `403`: The caller's role does not permit this action.
- `404`: Resource does not exist.

---

[API](https://skmtc.dev/dvt/apis/dvt-rest-api.md) · [All operations](https://skmtc.dev/dvt/apis/dvt-rest-api/llms.txt) · [OpenAPI document](https://skmtc.dev/dvt/apis/dvt-rest-api/revisions/00016f504165?raw)
