Comment
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, likeitem/docs4-item-1. All comments on one target share a thread id of the formthr_{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, adepth(0 for top-level), and a materializedpathof ancestor ids ending in itself. Nesting is capped at depth 10 by default. - Moderation states -
pending,visible,flagged,hidden,removed,deleted. New comments start inpending(automatic toxicity scoring is not yet wired) and still appear in threads;hiddenandremovedare tombstoned for regular viewers. See Moderation and visibility. - Reactions are a fixed set -
like,insightful,laugh,question,celebrate; anything else is400 invalid_reaction. Counts are per-comment, and each viewer sees their own reactions inviewer_reactions. - Anonymous and spoiler flags - an anonymous comment hides
authorfrom other users (served asnull) while moderators and the author still see it.spoileris carried on the body for clients to blur. - Mentions notify -
@identifiertokens are extracted (deduplicated, alphanumeric plus_and-), returned inmentions, 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 hvTsEKZZQmqhxTfuYxDPpost 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 insightfultoggle 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 hvTsEKZZQmqhxTfuYxDPOnly 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.
