Skip to main content

Bidirectional comments in Work Objects

Overview

Bidirectional commenting allows a Slack app to present comments attached to external resources. Users in Slack can view, post, edit, or delete a comment depending on which capabilities the app chooses to support. To accomplish this, bidirectional commenting uses standard interactive elements along with a dedicated event (entity_comments_requested) and API method (entity.presentComments). Comments are not stored in Slack, though they are cached by the Slack client. They are presented ephemerally when a user opens the comments tab in the flexpane.

To take advantage of the features outlined below, your app needs to have basic support for one or more supported entity types.

Showing available comments

The entity_comments field in the entity metadata payload adds an entry point to the comment flexpane from the unfurl card. The presence of this field indicates whether or not the Work Object entity supports comments. If your entity does not support comments, do not include the field. Conversely, if your entity has 0 comments, including this field with a value of 0 renders an unfurl card that links to the comment flexpane.

Include the entity_comments property in the unfurl implementation:

{
"metadata": {
"entities": [
{
"entity_comments": {
"count": 5,
"latest_timestamp": 1741165000
},
"app_unfurl_url": "https://example.com/document/123?eid=123456&edit=abcxyz",
"url": "https://example.com/document/123",
"external_ref": {
"id": "123",
"type": "document"
},
"entity_type": "slack#/entities/file",
"entity_payload": {
}
}
]
}
}

Include the entity_comments property in the flexpane implementation:

{
"metadata": {
"entity_type": "slack#/entities/file",
"entity_payload": {}, // entity schema
"entity_comments": {
"count": 5,
"latest_timestamp": 1741165000
}
}
}

Viewing comments in Slack

Comments use a dedicated event flow so that they can be bidirectionally shared. Flexpane details are returned by using the entity.presentDetails API method. A separate event flow for comments allows pagination on comments to be handled without having to reload all the flexpane details.

ComponentNameDescription
API evententity_comments_requestedRequest to the app to request comment data. Includes cursor for pagination.
Slack Web APIentity.presentCommentsCalled by the app; returns the requested list of comments.

The entity_comments_requested event

This event payload is dispatched when a user opens the comments tab of the flexpane, refreshes the comments, or scrolls to request the next page of comments. Set up an event handler for it similar to the entity_details_requested event, which is used for the flexpane. When the cursor field is present, return the next page of comments starting from that cursor.

{
"type": "entity_comments_requested",
"user": "U0123456", // user who opened the flexpane
"external_ref": {
"id": "123",
"type": "my-type" // optional
},
"entity_url": "https://example.com/document/123",
"link": {
"url": "https://example.com/document/123",
"domain": "example.com" // domain of the URL
},
"cursor": "your_app_sourced_pagination_cursor",
"limit": 100,
"thread_root_id": "comment-unique-identifier-001", // optional, present when requesting thread replies for a specific comment
"event_ts": "123456789.1234566", // event timestamp
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689" // event trigger ID
}

The entity.presentComments API method

This is the API method your app should call as a response to an end user in Slack opening the flexpane and receiving the entity_comments_requested event. Refer to entity.presentComments for more details.

Authenticating users

Comments are shown in the flexpane, so a user who reaches the comments tab has typically already passed the flexpane's authentication. If your app requires the user to authenticate before viewing an entity's details, that same flow currently guards its comments. See handling authentication for more details.

The entity.presentComments API method also accepts user_auth_required and user_auth_url parameters so your app can signal an authentication requirement on the comments response itself. If the user must authenticate before comments can load, respond to the entity_comments_requested event with user_auth_required set to true and a valid user_auth_url, omitting comments:

{
"user_auth_required": true,
"user_auth_url": "https://example.com/onboarding?user_id=xxx",
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689"
}

When you set user_auth_required to true, you must also provide user_auth_url. After the user authenticates with your service, respond to the next entity_comments_requested event with the user's comments as usual. We recommend applying the same authentication logic here that you use for the flexpane so that comments remain consistent as this flow evolves.

Managing comments in Slack

ComponentNameDescription
Interactivity payload typeview_submission events for comment post/editsA view_submission event is published to the app with a special view type entity_comment. A trigger_id will be present on each of these to allow the app to acknowledge the action via the entity.acknowledgeCommentAction API method.
Interactivity payload typeblock_actions events for comment deletionDeletions will dispatch a block_actions event to your app containing the delete_action_id and value supplied by the app indicating which comment to delete.
Slack Web APIentity.acknowledgeCommentAction API methodThe app acknowledges a post, edit, or delete action by calling this API method with the trigger_id and the resulting comment data (for post/edit) or an error message.

Posting or editing a comment

The view_submission interactivity payload is published when a post or edit is submitted by an end user.

The callback_id will be either work-object-post-comment or work-object-edit-comment, depending on the action the user took. The external_id will be an empty string when a new comment is posted from Slack, but for an edit, the value returned will be the comment identifier you supplied via the entity.presentComments API method.

Be sure to ackknowledge the interactivity request or users will see an error in the flexpane. After processing the comment, call the entity.acknowledgeCommentAction API method with the trigger_id and the resulting comment data to confirm the action to the user.

{
"type": "view_submission",
"team": {
"id": "T123456789",
"domain": "mydomain"
},
"user": {
"id": "U123456789",
"username": "jennifer.hynes",
"name": "Jennifer Hynes",
"team_id": "E123456789"
},
"api_app_id": "A123456789",
"token": "2LyBB2zdTQHi9b23weFcwQbM",
"trigger_id": "1295630157075.1135707055634.6fa830f3a0cad10be08e716ba268d9c4",
"view": {
"id": "V018PJJ4M1R",
"team_id": "E123456789",
"type": "entity_comment",
"blocks": [],
"private_metadata": "",
"callback_id": "work-object-post-comment", // or work-object-edit-comment
"state": {
"values": {
"comment_input_block": {
"comment_input_action": {
"type": "rich_text_input",
"rich_text_value": {
"type": "rich_text",
"elements": [...]
}
}
}
}
},
"hash": "1765178309.JjuYBCgV",
"external_ref": {
"id": "ITEM-123",
"type": "work_item"
},
"entity_url": "your_entity_url",
"app_id": "A123456789",
"external_id": "", // for the edit use case this will be hydrated with the comment identifier supplied in entity.presentComments
"app_installed_team_id": "T123456789",
"bot_id": "B018PJ75EF5"
},
"response_urls": [
],
"is_enterprise_install": false,
"enterprise": {
"id": "E123456789",
"name": "example-org"
}
}

Deleting a comment

The block_actions interactivity request will be emitted when an end user chooses to delete a comment.

The action_id of this block action is configurable and set in the entity.presentComments API method request. If comments[].can_delete=true but you neglected to set a delete_action_id, the delete button will be shown to end users but will do nothing when clicked (the block action will not fire).

{
"type": "block_actions",
"user": {
"id": "U123ABC456",
"username": "jennifer.hynes",
"name": "Jennifer Hynes",
"team_id": "T123ABC456"
},
"api_app_id": "A123ABC456",
"token": "abc123",
"container": {
"type": "entity_comment",
"external_ref": {
"id": "ITEM-123",
"type": "work_item"
},
"entity_url": "your_entity_url"
},
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689",
"team": null,
"enterprise": {
"id": "E123ABC456",
"name": "Acme Corp"
},
"is_enterprise_install": true,
"actions": [
{
"action_id": "your_delete_action_id", // The delete_action_id you passed in entity.presentComments
"block_id": "TPdc",
"value": "your_comment_identifier", // The comment's id (external_ref) that you passed in entity.presentComments
"action_ts": "1748809126.803329"
}
]
}

The entity.acknowledgeCommentAction API method

After your app processes a comment post, edit, or delete, call this API to confirm the result to the user. The trigger_id from the view_submission or block_actions payload must be passed back.

For a successful post or edit, include the persisted comment in the response so Slack can update the UI:

{
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689",
"comment": {
"id": "comment-unique-identifier-003",
"sender": {
"user_id": "U123456789"
},
"comment": {
"value": "The saved comment text",
"format": "markdown"
},
"timestamp": 1741164500,
"url": "your.app/item/1/comment/3",
"thread_root_id": "comment-unique-identifier-001"
}
}

For a successful delete, only the trigger_id is required:

{
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689"
}

If the action failed in your app, return an error message instead of a comment:

{
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689",
"error": "You do not have permission to edit this comment"
}
PropertyTypeDescriptionRequired?
trigger_idstringThe trigger_id from the view_submission or block_actions interactivity payload.Required
commentCommentThe persisted comment data. Required for successful post and edit actions. Same comment schema as the entity.presentComments API method.Optional
errorstringAn error message to display to the user if the action failed in your app. Provide either comment or error, but not both.Optional

Threaded replies

The comments system supports threaded replies. When a user opens a thread in Slack, your app receives an entity_comments_requested event with a thread_root_id field indicating which comment's replies are being requested. Respond with the entity.presentComments API method containing the reply comments.

To indicate that a comment supports threading:

  • Set can_reply: true on the parent comment via the entity.presentComments API method.
  • Include reply_count and, optionally, reply_users (up to two) and latest_reply_ts to render thread previews.

When returning replies, set thread_root_id on each reply comment to the id of the parent comment.

Top-level comments and their threaded replies are returned in separate entity.presentComments API method calls. When responding to a request for top-level comments (an entity_comments_requested event with no thread_root_id), return only the parent comments and advertise their threads using the preview fields. Do not include the replies themselves in this response. Replies are only returned when Slack requests them for a specific thread.

Responding to a request for top-level comments (event has no thread_root_id). The parent comment advertises its thread with can_reply, reply_count, reply_users, and latest_reply_ts, but its replies are not included:

{
"comments": [
{
"id": "comment-unique-identifier-001",
"sender": {
"user_id": "U123456789"
},
"timestamp": 1741164235,
"comment": {
"value": "Can someone take a look at the failing test?",
"format": "markdown"
},
"can_reply": true, // allows users to reply to this comment in a thread
"reply_count": 2, // number of replies in the thread
"reply_users": [ // up to two users who have replied, shown in the thread preview
{ "user_id": "U234567890" },
{ "text": "External User", "icon": { "url": "https://example.com/avatar.png" } }
],
"latest_reply_ts": 1741165000 // timestamp of the most recent reply
}
],
"can_post_comment": true,
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689"
}

Responding to a request for a thread's replies (event has thread_root_id set to comment-unique-identifier-001). Return the replies, each pointing back to the parent via thread_root_id:

{
"comments": [
{
"id": "comment-unique-identifier-002",
"sender": {
"user_id": "U234567890"
},
"timestamp": 1741164800,
"thread_root_id": "comment-unique-identifier-001", // the id of the parent comment
"comment": {
"value": "On it, taking a look now.",
"format": "markdown"
}
},
{
"id": "comment-unique-identifier-003",
"sender": {
"text": "External User",
"icon": { "url": "https://example.com/avatar.png" }
},
"timestamp": 1741165000,
"thread_root_id": "comment-unique-identifier-001",
"comment": {
"value": "Thanks, appreciate the quick turnaround!",
"format": "markdown"
}
}
],
"can_post_comment": true,
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689"
}