Working with Comments
What the UI calls a comment is an Expression asset inside a
Conversation — and "commenting on" a workitem is not a special
relationship: it is a mention.
The Model
| Type | Role |
|---|---|
Conversation | The thread container. Has Join/Leave operations — and no Delete: conversations cannot be removed through the API. |
Expression | One comment. Author and Content are required; AuthoredAt is marked required but the server fills it in when omitted. |
The relations that make it work:
Expression.BelongsTo→ its Conversation.Expression.InReplyTo→ the Expression it replies to (Repliesis the read-only reverse).Expression.Mentions→ any assets the comment references — typedBaseAsset, so workitems, members, projects, anything. Mentioning an asset is what places the comment in that asset's conversation stream. The reverse isMentionedInExpressions, available on every asset.
One rule the server enforces: an Expression must have either a
BelongsTo conversation or an InReplyTo parent. Creating one with
neither fails with
Violation'AssignExpressionConversation'ConversationOrInReplyToRequired —
a top-level comment needs a thread; a reply inherits its parent's.
Comment on a Workitem (with a Cross-Reference and a Reply)
One Bulk API request can create the thread, the comment and a reply —
aliases carry the links, including InReplyTo referencing the sibling
command:
[
{ "@conv": { "AssetType": "Conversation" } },
{
"@comment": {
"AssetType": "Expression",
"BelongsTo": "@conv",
"Author": "Member:20",
"Content": "This duplicates the login work — see S-01002.",
"Mentions": ["Story:1207", "Story:1208"]
}
},
{
"AssetType": "Expression",
"Author": "Member:20",
"Content": "Agreed — closing this one.",
"InReplyTo": "@comment"
}
]
The first entry in Mentions anchors the comment to the workitem being
commented on; the second is the cross-reference. The reply carries no
Mentions — and therefore does not appear in either story's stream,
only in the thread.
Starting from Numbers
A create cannot resolve a Number inline
(alias limitations) — resolve
first, then create:
[
{ "from": "Story", "filter": ["Number=$A"], "select": ["Number"], "with": { "$A": "S-01001" } },
{ "from": "Story", "filter": ["Number=$B"], "select": ["Number"], "with": { "$B": "S-01002" } }
]
Each result's _oid feeds the Mentions array above.
Reading Comments
From the asset's side — every asset carries MentionedInExpressions:
{
"from": "Story",
"where": { "ID": "Story:1207" },
"select": [{ "from": "MentionedInExpressions", "select": ["Content", "Author.Name", "AuthoredAt"] }]
}
Or from the comment side, filtering the mention relation by OID:
{
"from": "Expression",
"filter": ["Mentions='Story:1207'"],
"select": ["Content", "Author.Name", "AuthoredAt", "InReplyTo"],
"sort": ["AuthoredAt"],
"page": { "size": 50, "start": 0 }
}
Remember the reply nuance: replies without their own mentions appear only
through the thread (InReplyTo/Replies), not in the asset-side view.
Deleting
Expressions delete normally (?op=Delete, recoverable like everything
else). Conversations do not — an emptied thread simply remains. Plan
accordingly if your integration creates threads programmatically.
Related Topics
-
Asset permalinks — include an
assetdetail.v1link with the mention so the notified member can click straight to the asset. -
Product feature: Using Conversations — what the comments this guide writes look like to users.
-
Relations — mentions are ordinary relations
-
Operations — the delete/recovery model