Skip to content

Comment

Try the interactive example ↓

Universal threaded comments that attach to any resource by a typed reference: nested reply trees, a fixed reaction set, moderation states with an audit trail, anonymous and spoiler flags, soft deletion that preserves thread structure, @mention extraction that lands in Inbox, and hidden agent-audit notes.

When to reach for it: feedback on timeline events, items, worlds, or support cases; nested discussions with reactions; moderated community threads; hidden technical notes agents attach to resources.

When not to: chat-style realtime messaging belongs to Pub/Sub; persistent per-user notifications belong to Inbox (Comment already pushes mention and reply notifications there for you).

Concepts

  • Targets and threads - a comment attaches to a {type, id} target reference, like item / docs4-item-1. All comments on one target share a thread id of the form thr_{type}_{id}. The target is not validated against the owning service; any type/id pair opens a thread.
  • Replies form a tree - a reply carries parent_comment_id, a depth (0 for top-level), and a materialized path of ancestor ids ending in itself. Nesting is capped at depth 10 by default.
  • Moderation states - pending, visible, flagged, hidden, removed, deleted. New comments start in pending (automatic toxicity scoring is not yet wired) and still appear in threads; hidden and removed are tombstoned for regular viewers. See Moderation and visibility.
  • Reactions are a fixed set - like, insightful, laugh, question, celebrate; anything else is 400 invalid_reaction. Counts are per-comment, and each viewer sees their own reactions in viewer_reactions.
  • Anonymous and spoiler flags - an anonymous comment hides author from other users (served as null) while moderators and the author still see it. spoiler is carried on the body for clients to blur.
  • Mentions notify - @identifier tokens are extracted (deduplicated, alphanumeric plus _ and -), returned in mentions, and delivered as Inbox notifications to the mentioned users; replies notify the parent's author the same way.

Tenant, workspace and sharing boundary

Every request resolves a live tenant member or service credential. A signed token's moderator/admin label alone grants no comment access. Named workspaces are optional: creation without workspace_id uses the account root, while a reply inherits its parent's workspace unless an explicit matching placement is supplied. Reads expose workspace_id; parent, target and workspace cannot be rewired after creation.

Comment retains its existing public default. At the account root, public comments are readable by live human members. In a named workspace, public means that workspace's readers, not every organization member. Private comments require the author, an explicit audience grant, or an authorized moderator. A reply also requires read access to its parent, so sharing a child does not bypass a private ancestor. The arbitrary target reference is not a typed authorization link to another service; applications must separately authorize access to the target resource.

Use the existing sharing endpoint for direct users, groups or service accounts:

POST /api/v1/access/comments/{comment_id}/grants
Content-Type: application/json

{"subject":{"kind":"user","id":"reviewer-id"},"role":"reader"}

The grant roles are reader, manager and moderator. Managers administer sharing; moderators transition/pin/delete comments and inspect moderation history. Neither sharing nor moderation grants authorship or permission to edit someone else's content. Content edits remain author-only. Reaction records and moderation actions are written by their domain commands, not arbitrary scoped SDK writes.

Lists, totals, full-text search, reaction reads and child records use the same scoped boundary. HTTP responses redact anonymous attribution and hidden/removed contents; these are response projections, not conditional masking of the generated Rust row. Notification delivery rechecks the recipient's current access, live identity and current row: deleted, hidden and revoked content cannot leak through an old queue payload, and anonymous delivery does not reveal the author ID.

Post, reply, read

snug comment post --target docs4-item-1 --type item \
  --body "First pass looks good, @docs4-other should confirm."
snug comment reply --parent hvTsEKZZQmqhxTfuYxDP --target docs4-item-1 \
  --type item --body "Confirmed, the fix holds."

snug --output json comment thread --target docs4-item-1 --type item
snug --output json comment get --comment-id hvTsEKZZQmqhxTfuYxDP

post also takes --workspace-id, --spoiler, --anonymous, and --idempotency-key; reply and audit accept --workspace-id too; an omitted reply placement inherits the parent. thread takes the shared list options (--limit, --cursor or --page, --sort -created_at for newest first, --sort -pinned,created_at for pinned comments first, --filter 'depth[lte]=1'), plus --max-depth. Over HTTP, creation is one endpoint - a reply is just a create with parent_comment_id:

curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"target": {"type": "item", "id": "docs4-item-1"},
       "body": {"text": "Shipping this today."}}' \
  http://localhost:4000/api/v1/comments
{
  "status": 201,
  "msg": "Created",
  "data": {
    "comment_id": "0199e100-0000-7000-8000-00000000000a",
    "thread_id": "thr_item_docs4-item-1",
    "moderation_state": "pending",
    "mentions": [],
    "created_at": "2026-08-28T05:02:38.060063Z"
  }
}

A comment in a thread or get response carries the full record: body, author, depth and path, moderation_state, pinned, reaction_counts, and viewer_reactions. Bodies are markdown by default (plain is the alternative), up to 10,000 characters - a blank or oversized body is rejected with 400.

Passing an idempotency_key on create makes retries safe: repeating the same key returns the original comment_id instead of posting a duplicate (the dedup window is 24 hours).

Reactions

snug comment react --comment-id hvTsEKZZQmqhxTfuYxDP --reaction like
snug comment unreact --comment-id hvTsEKZZQmqhxTfuYxDP --reaction like
snug comment toggle --comment-id hvTsEKZZQmqhxTfuYxDP --reaction insightful

toggle adds the reaction if absent and removes it if present, returning reacted plus the updated reaction_counts - convenient for a reaction button. Reaction names are case-insensitive (INSIGHTFUL counts as insightful), and one actor can hold several distinct reactions on the same comment, up to a configured cap.

Edit and delete

snug comment edit --comment-id hvTsEKZZQmqhxTfuYxDP \
  --body "First pass looks good, @docs4-other and @docs4-third please confirm."
snug comment delete --comment-id hvTsEKZZQmqhxTfuYxDP

Only the author can edit - anyone else, moderators included, gets 403 insufficient_permissions. An edit re-extracts mentions and notifies only newly added ones. Deletion is available to the author or a moderator and is a soft delete: the node stays in the tree with moderation_state: "deleted", deleted_at set, the body redacted to [deleted], and mentions cleared, so replies below it keep their context.

Deleted is terminal:

  • editing a deleted comment fails with 409 comment_deleted,
  • replying under a deleted parent fails with 409 comment_deleted,
  • moderating it fails with 400 invalid_moderation_transition.

Moderation, pinning, agent audits

Moderator-gated operations - state transitions, the flagged queue, pinning, hidden-comment visibility, and agent-audit notes - have their own page: Moderation and visibility.

Limits and configuration

Body length (10,000 chars), reply depth (10), distinct reactions per actor (5), thread page size cap (200), and the auto-flag toxicity threshold are tunable via the COMMENT_* variables.

Reference

  • Comment API - every endpoint, callable
  • Related: Inbox receives mention and reply notifications, Pub/Sub for chat-style realtime messaging

Identifiers

Comment and moderation-action IDs are opaque UUIDv7 strings. Target IDs, thread names and reaction labels keep their application-defined meanings.

Interactive example

Add conversations to a document or activity with threaded replies and reactions. Moderation state stays with the comments your app displays.

An illustrative example that runs in your browser. No account or API calls required.