---
title: "Bidirectional comments in Work Objects"
summary: "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 (entitycommentsrequested) 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."
url: "https://docs.slack.dev/messaging/work-objects-comments"
type: "guide"
---

Source: https://docs.slack.dev/messaging/work-objects-comments

# Bidirectional comments in Work Objects

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](https://docs.slack.dev/interactivity/handling-user-interaction.md) elements along with a dedicated event ([`entity_comments_requested`](#entity-comments-requested-event)) and API method ([`entity.presentComments`](#entity-present-comments-method)). 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](https://docs.slack.dev/messaging/work-objects-implementation.md#supported-entity-types).

Your app is responsible for authorizing access to each comment and entity

Comments are not stored in Slack, so Slack has no record of the external entity or comment behind an identifier and cannot verify that the acting user is permitted to act on it. Slack tells your app which user is acting, but authorizing that user against the referenced object is your app's responsibility. The `external_ref` and comment identifiers in these events and payloads are supplied by the caller, so treat them as untrusted input. Before you return, post, edit, or delete a comment, confirm that the acting user is authorized for that specific external object in your own system. Refer to [authorizing comment access](#authorizing-access) for details.

## Showing available comments {#showing-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](https://docs.slack.dev/messaging/work-objects-implementation.md#implementation-unfurl):

```
{  "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](https://docs.slack.dev/messaging/work-objects-implementation.md#implementation-flexpane):

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

## Viewing comments in Slack {#view-comments}

Comments use a dedicated event flow so that they can be bidirectionally shared. Flexpane details are returned by using the [`entity.presentDetails`](https://docs.slack.dev/reference/methods/entity.presentdetails.md) API method. A separate event flow for comments allows pagination on comments to be handled without having to reload all the flexpane details.

Component

Name

Description

API event

[`entity_comments_requested`](https://docs.slack.dev/reference/events/entity_comments_requested.md)

Request to the app to request comment data. Includes `cursor` for pagination.

Slack Web API

[`entity.presentComments`](https://docs.slack.dev/reference/methods/entity.presentcomments.md)

Called by the app; returns the requested list of comments.

### The entity_comments_requested event {#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`](https://docs.slack.dev/reference/events/entity_details_requested.md) 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 {#entity-present-comments-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`](https://docs.slack.dev/reference/events/entity_comments_requested.md) event. Refer to [`entity.presentComments`](https://docs.slack.dev/reference/methods/entity.presentcomments.md) for more details.

### Authenticating users {#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](https://docs.slack.dev/messaging/work-objects-implementation.md#actions-request-authentication) for more details.

The [`entity.presentComments`](https://docs.slack.dev/reference/methods/entity.presentcomments.md) 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`](https://docs.slack.dev/reference/events/entity_comments_requested.md) 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`](https://docs.slack.dev/reference/events/entity_comments_requested.md) 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.

### Authorizing comment access {#authorizing-access}

Authentication confirms _who_ the acting user is. Authorization confirms _what_ that user is allowed to do. Your app is responsible for the authorization step.

Because comments are not stored in Slack, Slack has no record of the external entity or comment that an identifier points to and cannot verify that the acting user is permitted to act on it. Only your app holds that data, so only your app can make the authorization decision.

The `external_ref`, `entity_url`, and comment identifiers in these events and payloads are supplied by the caller. A user who knows or guesses a valid identifier can reach any of these flows, so treat these values as untrusted input. Before your app acts on a request, verify against your own system that the acting user is authorized for that specific external object. Apply this check to each of the following:

*   **Viewing comments.** When you handle the [`entity_comments_requested`](https://docs.slack.dev/reference/events/entity_comments_requested.md) event, confirm the user may read comments on the referenced entity before you return them via the [`entity.presentComments`](https://docs.slack.dev/reference/methods/entity.presentcomments.md) API method. If they are not authorized, return an empty `comments` list rather than another user's comments.
*   **Posting a comment.** When you handle the `work-object-post-comment` submission, confirm the user may comment on the referenced entity before you persist the comment.
*   **Editing a comment.** When you handle the `work-object-edit-comment` submission, confirm the user may edit that specific comment before you persist the change. Do not rely on `can_edit` from your earlier [`entity.presentComments`](https://docs.slack.dev/reference/methods/entity.presentcomments.md) API method response to enforce this, as that field only controls whether Slack renders the edit affordance.
*   **Deleting a comment.** When you handle the delete `block_actions` request, confirm the user may delete that specific comment before you remove it. As with editing, `can_delete` controls only the UI and is not an authorization boundary.

If the acting user is not authorized for a post, edit, or delete, respond to the [`entity.acknowledgeCommentAction`](https://docs.slack.dev/reference/methods/entity.acknowledgecommentaction.md) API method with an `error` message so the user sees why the action did not complete:

```
{  "trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689",  "error": "You do not have permission to comment on this item"}
```

The `can_edit`, `can_delete`, `can_reply`, and `can_post_comment` fields in your [`entity.presentComments`](https://docs.slack.dev/reference/methods/entity.presentcomments.md) API method response control which affordances Slack renders. They improve the experience by hiding actions a user cannot take, but they are not a substitute for the server-side authorization check described above.

## Managing comments in Slack {#manage-comments}

Component

Name

Description

Interactivity payload type

`view_submission` events for comment post/edits

A `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`](https://docs.slack.dev/reference/methods/entity.acknowledgecommentaction.md) API method.

Interactivity payload type

`block_actions` events for comment deletion

Deletions will dispatch a [`block_actions` event](https://docs.slack.dev/messaging/work-objects-implementation.md#actions-handling-block-actions) to your app containing the `delete_action_id` and value supplied by the app indicating which comment to delete.

Slack Web API

[`entity.acknowledgeCommentAction`](https://docs.slack.dev/reference/methods/entity.acknowledgecommentaction.md) API method

The 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 {#post-edit-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`](https://docs.slack.dev/reference/methods/entity.presentcomments.md) API method.

Be sure to acknowledge the [interactivity request](https://docs.slack.dev/interactivity/handling-user-interaction.md#acknowledgment_response) or users will see an error in the flexpane. After processing the comment, call the [`entity.acknowledgeCommentAction`](https://docs.slack.dev/reference/methods/entity.acknowledgecommentaction.md) 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 {#delete-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`](https://docs.slack.dev/reference/methods/entity.presentcomments.md) 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 {#entity-acknowledge-comment-action-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"}
```

Property

Type

Description

Required?

`trigger_id`

string

The `trigger_id` from the `view_submission` or `block_actions` interactivity payload.

Required

`comment`

Comment

The persisted comment data. Required for successful post and edit actions. Same comment schema as the [`entity.presentComments`](https://docs.slack.dev/reference/methods/entity.presentcomments.md) API method.

Optional

`error`

string

An 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 {#threaded-replies}

The comments system supports threaded replies. When a user opens a thread in Slack, your app receives an [`entity_comments_requested`](https://docs.slack.dev/reference/events/entity_comments_requested.md) event with a `thread_root_id` field indicating which comment's replies are being requested. Respond with the [`entity.presentComments`](https://docs.slack.dev/reference/methods/entity.presentcomments.md) 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`](https://docs.slack.dev/reference/methods/entity.presentcomments.md) 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`](https://docs.slack.dev/reference/methods/entity.presentcomments.md) API method calls. When responding to a request for top-level comments (an [`entity_comments_requested`](https://docs.slack.dev/reference/events/entity_comments_requested.md) 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"}
```
