Work Objects
One of the primary ways to share external content within Slack is by posting URL links in conversations. However, links in their primitive form don't provide a lot of information. That's why we originally introduced link unfurling, so that Slack apps could provide rich previews and actions inside of conversations without requiring users to click the link. With Work Objects, apps can take the unfurling experience even further.
Work Objects can represent any type of entity or data other than conversations within Slack. Examples include files, tasks, and incidents. Work Objects aim to standardize the presentation of these entities inside Slack and to provide users with richer previews and greater feature extensibility.
Work Objects have two primary components: the unfurl component, and the flexpane component.
The unfurl component
Work Objects appear when a link is unfurled in a conversation. Each Work Object is represented by an entity, such as the Task
entity shown below. Entities can be customized to preview a variety of information. The full list of supported entities can be found in the supported entity types section.
Similar to link unfurls, the content of a Work Object is visible to everyone in the conversation. Therefore, it's important to avoid sending sensitive information and to ensure the content is relevant to all users.
The flexpane component
When a user clicks on a Work Object unfurl, a flexpane opens on the right side of Slack to reveal more content. This new surface provides an additional layer of rich contextual information and customization options for apps.
It can also optionally require user authentication into a third-party service, which is useful in scenarios where the flexpane may display sensitive information. As general guidance, any information that you would not like to show in the unfurl can be shown in the flexpane following authentication. The flexpane also aggregates any related conversations where this Work Object resource has been referenced in Slack.
Implementation details
Implementing Work Objects requires a Slack app. For more information on how to create an app, you can view this quickstart guide.
First, you must enable the Work Objects feature on your app. To do so, perform the following steps:
- Visit https://api.slack.com/apps and select your app.
- Navigate to Work Object Previews under the left sidebar menu.
- Enable the toggle.
- Select the entity type(s) that you would like to add to your app. Supported entity types can be found here.
- Click Save.
Unfurl implementation
Work Object unfurls are an extension of the existing link unfurl feature for Slack apps. If your app has not been configured to link unfurls yet, please follow the setup instructions to do so.
The chat.unfurl
API method, which provides unfurl content to Slack, has been updated with a new optional parameter: metadata
. Slack uses this parameter to generate a Work Object representing the resource your app is unfurling. Please ensure that the metadata
parameter is URL-encoded. If you're using one of Slack's developer tools, this is handled automatically, and no further action is needed.
The JSON schema is found below:
"metadata": {
"entities": [
{
"app_unfurl_url": "https://example.com/document/123?eid=123456&edit=abcxyz", // URL posted by the user in a conversation
"url": "https://example.com/document/123", // URL representing the resource in the third party system
"external_ref": {
"id": "123", // a string ID that uniquely identifies the resource being unfurled
"type": "document" // An optional internal type for entity in the source system. Only needed if the ID is not globally unique or needed when retrieving the item
},
"entity_type": "slack#/entities/file", // entity type
"entity_payload": {}, // entity schema
}
]
}
Parameter | Description |
---|---|
app_unfurl_url | The URL link posted by the user in a conversation. This is the same URL obtained from the link_shared event. |
entities | The array contains the Work Object entities that you want to unfurl in Slack. You can provide multiple entities if the user has provided multiple URLs that need to be unfurled. |
entity_type | Each entity has a type defined by entity_type . The available types are listed in the supported entity types section. Each entity also includes a set of properties that are displayed in the Work Object, contained within the entity_payload object. Refer to the entity payload schema for details on customizing the displayed information. |
external_ref | Contains a string ID that uniquely identifies the resource being unfurled. It's strongly recommended to use the same ID your third-party system uses to identify and retrieve the resource via its APIs. |
url | The URL that directs the user to the desired resource on a third party system. When a user clicks the Work Object link trigger, they'll be directed to this URL. |
Notifications implementation
The chat.postMessage
API method can also be used to post Work Object entities by re-using the existing metadata
parameter. In this case, no link has been unfurled, so the app_unfurl_url
property is not required. The JSON schema for passing entity metadata is the same as that for the chat.unfurl
API method.
Flexpane implementation
The flexpane component requires the Work Object unfurl to be implemented.
The flexpane introduces a new event: entity_details_requested
and a new API method: entity.presentDetails
.
To subscribe to the entity_details_requested
event:
- Visit https://api.slack.com/apps and select your app.
- Navigate to the Events & Subscriptions section in the sidebar.
- Click the Subscribe to bot events section.
- Add the
entity_details_requested
event.
When a user clicks an unfurl, an entity_details_requested
event is sent to your app. This event contains information about the user requesting access to the flexpane. Your app can then use the entity.presentDetails
API method to populate the flexpane with content.
Additionally, your app can require user authentication with a third-party service before granting access to the flexpane. If the user is not authenticated, your app can use the user_auth_required
parameter to prompt authentication. You can specify a user_auth_url
to redirect users to an authentication page.
The metadata
schema for the entity.presentDetails
API method is nearly identical to that of the chat.unfurl
API method, except:
- the
entities
array is removed. - the
app_unfurl_url
parameter is not included in theentity
object.
"metadata": {
"entity_type": "slack#/entities/file", // entity type
"entity_payload": {}, // entity schema
}
If your app does not implement the flexpane, the content displayed in the unfurl will be shown in the flexpane as a placeholder.
Entity payload schema
The entity payload contains information that will populate the unfurl or the flexpane. The schema is as follows:
{
"entity_payload": {
"attributes": {},
"fields": {},
"custom_fields": [], // optional
"display_order": [] // optional
}
}
The attributes
property
The attributes
property contains fields that help Slack identify the resource and generate its Work Object header.
{
"attributes": {
// Required fields
"title": {
"text": "Document 123" // title of the Work Object entity
},
// Optional fields
"display_id": "123", // user-friendly string ID to display on the entity
"display_type": "Document", // string that represents the resource being unfurled. Default is "File" for a file entity or "Task" for a task entity.
"product_name": "Slack", // name of the product that will be displayed in the Work Objects header. Default is the app name.
"product_icon": {
// A product icon to display in the Work Object header. Default is the app icon. If you choose to provide a custom icon, use a different image than your app icon to avoid
// displaying duplicate icons in the UI. Must be a publicly-accessible URL
// or a `slack_file` object (https://docs.slack.dev/reference/block-kit/composition-objects/slack-file-object/).
"alt_text": "Image of Document app icon",
// this
"url": "https://example.com/icon", // publicly-accessible URL to an image
// OR
"slack_file": { // provide either file ID or the url; only one is required.
"id": "F0123456",
"url": "https://files.slack.com/files-pri/T0123456-F0123456/xyz.png"
}
},
"full_size_preview": {
"is_supported": true, // required
"preview_url": "https://example.com/document-123-image.png?res=2000x2000", // publicly-accessible URL to an image
"mime_type": "image/png" // Mime type for the preview (only PDF and images types are currently supported)
},
// This field indicates when the metadata about this item was last
// modified. We use this to determine whether we should request new unfurl
// metadata via the `link_shared` event. This will often be the same value as
// `fields.date_updated`, but can be controlled independently to offer less/more
// frequent requests to update unfurl metadata.
"metadata_last_modified": 1741164235 // value in UNIX timestamp
}
}
The full_size_preview
property
If your entity supports a visual preview within Slack, you can provide a thumbnail image in the unfurl card and a full size image or PDF preview to be displayed in the full size preview modal. Below are examples of how to define the schema for each scenario.
Unfurl card
{
"metadata": {
"entities": [
{
"app_unfurl_url": "https://example.com/document/123",
"entity_type": "slack#/entities/file",
"entity_payload": {
"attributes": {
"full_size_preview": {
// `is_supported` lets Slack know that a full preview is supported
"is_supported": true, // required
"preview_url": "https://example.com/document/123/preview_123.png", // required if the full preview is supported
"mime_type": "application/pdf", // required if the full preview is supported
"error": {
"code": "file_not_supported", // "file_not_supported", "file_size_exceeded", or "custom"
"message": "Detailed error message"
} // error message to display if the full-size preview is not available
}
},
"fields":{
"preview": {
// this is the thumbnail that is displayed in the unfurl card
"image_url": "https://example.com/document/123/preview_123.png",
"alt_text": "Preview"
}
}
}
}
]
}
}
The File
and Content Item
entity types define a preview
field that can be used to display the thumbnail in the unfurl. If desired for other entity types, a custom field can be used to display an image in the unfurl.
The full_size_preview
object provided in the unfurl metadata must include the is_supported
attribute to enable the full size preview. Currently, only images and PDFs are supported for the preview.
Flexpane window
{
"metadata": {
{
"entity_type": "slack#/entities/file",
"entity_payload": {
"attributes": {
"full_size_preview": {
// `is_supported` lets Slack know that a full preview is going to be rendered
"is_supported": true, // required
"preview_url": "https://example.com/document/123.pdf", // required if the full preview is supported
"mime_type": "application/pdf", // required if the full preview is supported
"error": {
"code": "file_not_supported", // "file_not_supported", "file_size_exceeded", or "custom"
"message": "Detailed error message"
} // error message to display if the full-size preview is not available
}
},
"fields": {...}
}
}
}
}
The full_size_preview
object includes three required properties to display a preview: is_supported
, preview_url
, and mime_type
. The preview_url
contains publicly accessible URLs for PDFs, images, or PDF versions of other supported documents. For security reasons, these public URLs must include the CORS response header set to access-control-allow-origin:https://app.slack.com
. The mime_type
property is needed to render the preview modal for the correct media type.
You may provide the minimal {'is_supported': true}
value for the full_size_preview
field of an unfurl to enable generation of URLs that are only accessible for a limited
duration.
It is not recommended to include the preview
field when providing entity details for the full size preview modal.
The fields
property
The fields
properties are optional (but recommended) properties displayed in the Work Object unfurl below the header. The properties within the fields
object vary by entity type. For a list of supported entity types and their recommended fields
properties, refer to the supported entity types section.
The custom_fields
property
The custom_fields
property is similar to the fields
property but allows your app to define fully custom properties. This parameter is optional. See the supported properties for a field section for a list of supported properties.
{
"custom_fields": [
{
"key": "ticket_type", // key that will be used to reference this property
"label": "Ticket Type", // property label that will be displayed in the Work Object body
"value": "Epic", // property value that will be shown in the Work Object body
"type": "string" // Slack data type
}
]
}
The display_order
property
The display_order
property determines the order in which properties from fields
and custom_fields
are displayed in the Work Object body. If display_order
is not set, the properties will appear in the order defined by the entity type's fields
schema and the custom_fields
will be appended at the end.
{
"display_order": ["ticket_type", "created_by", "preview"]
}
Editing Work Object fields
Apps can designate fields as editable by a user in the Work Object flexpane. If supported by the app, users will see a pencil icon in the flexpane header that they can click to modify the editable fields. Once the edits are complete, the user can click Save
, which will send a view_submission
payload to your app with the updated information.
Designating a field as editable
Your app can designate a field as editable by adding the edit
property to a field and setting the value to true
as shown below:
{
"fields": {
"description": {
"value": "We need to implement a login page using [...]",
"format": "markdown",
"edit": {
"enabled": true
}
},
{...} // other fields
}
}
The edit
property
The edit
property contains the following attributes that can help further customize the experience:
Property | Type | Description |
---|---|---|
enabled | boolean | Toggle to enable edit mode on this field. |
placeholder | object | Placeholder text shown on empty text and number inputs. |
hint | object | Hint text is displayed below the input on supported input blocks. |
optional | boolean | Whether this field can be left blank. |
select | object | Options relevant to select and multi-select inputs. |
number | object | Additional properties relevant to number type fields. |
text | object | Additional properties relevant to text type fields. |
The placeholder
and hint
properties have the following properties:
Property | Type | Description |
---|---|---|
type | string | Set type: "plain_text" . |
text | string | Text to be displayed. |
emoji | boolean | Whether or not Slack-style emojis should be recognized within the string. This is only supported for the hint and not the placeholder. |
The number
property has the following properties:
Property | Type | Description |
---|---|---|
min_value | number | Minimum allowed value. |
max_value | number | Maximum allowed value. |
The text
property has the following properties:
Property | Type | Description |
---|---|---|
min_length | number | Minimum string length. Cannot be lower than 0 or greater than 3000. |
max_length | number | Maximum string length. Cannot be lower than 0 or greater than 3000. |
The select
property has the following properties:
Property | Type | Description |
---|---|---|
current_value | string | The current value of the select input. |
current_values | array of strings | If multi-select, then the current values of the multi-select input. Should be in the same order as the human readable form of the values. |
static_options | array of objects | If present on a valid field, then these values will appear on the fields as static select options. |
fetch_options_dynamically | boolean | When set to true, Slack will dynamically fetch the select options as the user types. Default is false. |
The static_options
property above has the following properties:
Property | Required | Type | Description |
---|---|---|---|
value | Yes | string | The value that will be sent to your application when the user submits the form. Max length: 150 characters. |
text | Yes | object | Human-readable text that will be displayed in the select menu. The object schema can be found here. Max length: 75 characters. |
description | No | Object | A descriptive text that will be shown below the text property. The object schema can be found here. Max length: 75 characters. |
Field mapping
When a field is made editable, Slack will map the field's type to the most appropriate Block Kit input. For example, a description
field will become a Block Kit Plain-Text input, and a due_date
field will become a Date picker input when the slack#/types/date
type is used. The full list of supported Block Kit inputs can be found below:
Block Kit input type | Supported | Restrictions |
---|---|---|
Checkboxes | Soon | Will be limited to a single checkbox for boolean types |
Date picker | Yes | Default input for Work Object date fields |
Datetime picker | Yes | Default input for Work Object datetime fields |
Email | Soon | Default input for Work Object email fields |
File | No | |
Multi-select | Yes | This is supported on array types using the same criteria outlined below for Select. For example, an array with slack/#types/channel_id items will become a multi-conversations select. |
Number | Yes | Min/max and whether decimals are allowed can be configured via field edit metadata |
Plain-text | Yes | Maximum of 3000 characters allowed. Edit will be disabled for any field with a value larger than this |
Radio button group | No | Use selects instead. |
Rich text | No | Rich text editing is not currently supported. As an alternative, raw markdown can be used in a standard text input. |
Select | Yes | For static or external selects, you'll need to set up appropriate configuration in the fields edit.select section. Fields of type slack#/types/user or slack/#types/channel_id will automatically become users select or conversations select (respectively). |
Time picker | Yes | |
URL | Soon | Default input for Work Object url fields |
Apart from the external_select
input, all changes to any of the input fields are packaged in a view_submission
payload and sent to your app when the user clicks Save.
Here is an example of a metadata payload for a task
entity that contains several editable fields:
{
"entity_type": "slack#/entities/task",
"entity_payload": {
"attributes": {
"title": {
"text": "Some task",
"edit": {
// This would cause the title element to transition into a
// to-be-determined edit surface. This would likely not be
// a native Block Kit element, but should have similar properties.
"enabled": true,
"text": {
"max_length": 50
}
}
},
},
"fields": {
"status": {
"value": "Blocked",
"edit": {
// Providing select options here turns this field into a select during
// edit mode. Without this, the field would become a text input.
"enabled": true,
"select": {
"static_options": [
{
"value": "123",
"text": {
"text": "In progress"
}
},
{
"value": "456",
"text": {
"text": "Done"
}
}
]
}
}
},
"due_date": {
"value": "Blocked",
"edit": {
// This would become a datetime or date picker element based on the
// fields type in the schema.
"enabled": true
}
},
"assignee": {
"type": "slack#/types/user",
// You can identify a user in one of two ways:
// - Use "user_id" with a Slack user ID (e.g., "U123ABC456") if known.
// - Use "text" with the user's display name if the Slack user ID is not available.
"user": {
"text": "Joan Smith",
"email": "joan.smith@example.com",
},
"edit": {
// External select supported by setting the
// `fetch_options_dynamically` field to true.
"enabled": true,
"select": {
"fetch_options_dynamically": true
}
}
}
}
}
}
Handling view_submission
requests
When the user clicks Save, a view_submission
payload will be sent to your app. Here is an example:
{
"type": "view_submission",
"team": {
"id": "T123ABC456",
"domain": "slack-domain-12343",
"enterprise_id": "E1234",
"enterprise_name": "Acme Corp"
},
"user": {
"id": "U1234",
"username": "jennifer_hynes",
"name": "jennifer_hynes",
"team_id": "T123ABC456"
},
"api_app_id": "A123ABC456",
"token": "vrv7tNLHMRT8hdqLZjuM10St",
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689",
"view": {
"id": "V08UHA2RFFF",
"team_id": "E123ABC456",
"type": "entity_detail",
"blocks": [],
"private_metadata": "",
"callback_id": "",
"state": {
"values": {
"description": {
"description.input": {
"type": "plain_text_input",
"value": "We need to implement a login page using **Capybara** for our testing framework. This page will be used for testing user authentication and login functionality within the application. hello\\n\\n### Requirements:\\n- Create a simple login page with fields for:\\n - **Username**\\n - **Password**\\n - **Login button**\\n- Implement basic form validation to ensure both fields are populated before submitting.\\n- Ensure the page is responsive and works well on both desktop and mobile views.\\n- Use **Capybara** to automate the interaction with the login form for testing purposes.\\n - Automate filling out the username and password fields.\\n - Simulate clicking the login button and validating the redirect or success message.\\n\\n"
}
}
}
},
"hash": "1748539925.7JuMmoCH",
"external_ref": {
"id": "139"
},
"entity_url": "https://github.com/repos/{user}/{repo}/issues/139",
"message_ts": "1758902242.754879",
"thread_ts": "1758901978.959789",
"channel": "C123ABC456",
"app_unfurl_url": "https://github.com/repos/{user}/{repo}/issues/139?myquery=param",
"app_id": "A123ABC456",
"external_id": "",
"app_installed_team_id": "E123ABC456",
"bot_id": "B0123456"
},
"response_urls": [],
"is_enterprise_install": false,
"enterprise": {
"id": "E1234",
"name": "Acme Corp"
}
}
The payload contains new properties relevant to Work Objects, such as the entity_url
and external_ref
properties. You can use both to identify the Work Object that was edited. Additionally, the view.type
property of the interactivity event will be set to entity_detail
for these types of requests specifically. The values that have been updated in the flexpane will be provided in the view.state.values
property.
If the user has submitted an edit that has incorrect or invalid information, there are multiple ways to provide feedback on this to the user using error handling.
Once your app has finished processing the changes submitted by the user, you should make a request to the entity.presentDetails
API method with the updated state of the Work Object using the trigger_id
that was provided in the view_submission
payload.
Dynamic External Select
Dynamic External Select allows the app to generate options for single or multi-select inputs dynamically as the user types in the input box.
When edit.select.fetch_options_dynamically
is set under a select field, Slack will dispatch a block_suggestion
request when the user begins to modify the field. In the payload, the block_id
and action_id
properties are mapped from the name of the field provided in the schema.
{
"type": "block_suggestions",
"block_id": "assignee", // comes from the name of the field in the schema
"action_id": "assignee.input",
"value": "jo",
...
}
Your app should respond with the options of the select, such as in the example below:
{
"options": [
{
"text": {
"type": "plain_text",
"text": "joe.bob@example.com"
},
"value": "U123"
},
{
"text": {
"type": "plain_text",
"text": "joan.rivers@example.com"
},
"value": "U456"
},
{
"text": {
"type": "plain_text",
"text": "jonny.appleseed@example.com"
},
"value": "U789"
}
]
}
Editing validations
There are three levels of validations to consider when it comes to updating a Work Object in Slack:
- Client-side field level validation within Slack via
edit
properties. - Server-side field level validation via response to
view_submission
. - Server-side form level validation via the
entity.presentDetails
API method.
Client-side field level validation
This type of validation prevents the user from submitting the form until the issue is
resolved. These types of validations are configured by the contents of the edit
block in
a given field. For example, an entity with the following metadata will result in the validation errors shown in the subsequent image:
{
"entity_type": "slack#/entities/task",
"entity_payload": {
"attributes": {
"title": {
"text": "Update links in login page",
"edit": {
"enabled": true,
// The title is always required (i.e., optional: false)
}
},
},
"fields": {
"description": {
"value": "We need to update the links in the login page to use our new branding",
"edit": {
"enabled": true,
"text": {
"min_length": 15,
}
}
}
},
"custom_fields": [
{
"key": "story_points",
"label": "Story points",
"type": "integer",
"edit": {
"enabled": true,
"number": {
"max_value": 10,
}
}
},
]
}
}
Server-side field level validation
These types of errors are documented in more detail here, but you'll want to respond to the view_submission
event with an object instead of an empty ack()
. Note that because this is using the ack
function, you'll need to respond within 3 seconds to prevent a timeout. Here is an example:
{
"response_action": "errors",
"errors": {
"due_date": "You cannot select a date in the past"
}
}
When this happens, we'll display the error in-line and disable the Submit button until that field is changed.
Server-side form level validation
The last type of error handling is to be used when there isn't anything wrong with a specific field, but instead, something went wrong at a higher level. Examples of these kinds of errors could include rate limit errors, consistency mismatches, or an uncaught exception when processing the save.
These errors should be sent by calling the entity.presentDetails
API method with an edit_error
error
payload instead of any entity metadata. For example:
{
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689",
"error": {
"status": "edit_error",
"custom_message": "Something went wrong but we're not sure what. Try again later",
}
}
Adding actions to Work Objects
Block Kit actions, such as buttons, can be added to either (or both) the unfurl card and the flexpane view footer. Up to two primary actions can be defined. These will always appear in the footer of the unfurl card or the flexpane window.
Additionally, up to five actions can be defined in the overflow menu. This can be accessed by clicking More actions in the overflow menu.
The actions defined in the unfurl card can be different from the ones defined in the flexpane window. Finally, some default actions may appear in the overflow menu, such as:
- Share [Object] (e.g., Share Issue)
- Copy link to [Object] (e.g., Copy link to Issue)
- Add to To-do
- View in App
Defining actions
in the metadata payload
A new actions
field is used to define what actions will appear in the unfurl or flexpane. Both primary_actions
and overflow_actions
are optional properties.
{
"actions": {
"primary_actions": [], // up to two actions here
"overflow_actions": [] // up to five actions here
}
}
Each action
is defined by the following properties:
Property | Required | Type | Description |
---|---|---|---|
text | Yes | string | Human readable text that will be displayed in the button. |
action_id | Yes | string | An identifier for this action. You can use this when you receive an interaction payload to identify the source of the action. Max length: 255 characters. |
value | No | string | The value to send along with the interaction payload. Maximum length is 2000 characters. |
style | No | string | Decorates the button with either a green or red background. Use style: primary for green and style: danger for red. |
url | No | string | Clicking the button will open the URL in the user's browser. Max length: 3000 characters. |
accessibility_label | No | string | A label for longer descriptive text about a button element. This label will be read out by screen readers instead of the button's text parameter. Maximum length: 75 characters. |
Example:
{
"actions": {
"primary_actions": [ // up to two actions here
{
"text": "Summarize issue with AI",
"action_id": "github_wo_button_summarize_issue",
"style": "primary",
"value": "user"
},
{
"text": "Close issue",
"action_id": "github_wo_button_close_issue",
"style": "danger",
"value": "user"
}
],
"overflow_actions": [ // up to five actions here
{
"text": "Pin issue",
"action_id": "github_wo_button_pin_issue",
"style": "primary",
"value": "user"
},
{
"text": "Assign to me",
"action_id": "github_wo_button_assign_issue",
"style": "primary",
"value": "user"
}
]
}
}
The actions
field is part of the entity_payload
of each Work Object:
{
"entities": [
{
"app_unfurl_url": "https://github.com/issues",
"entity_type": "slack#/entities/task",
"entity_payload": {
"attributes": {},
"fields": {},
"actions": {
"primary_actions": [ // up to two actions here
{
"text": "Summarize issue with AI",
"action_id": "github_wo_button_summarize_issue",
"style": "primary",
"value": "user"
},
{
"text": "Close issue",
"action_id": "github_wo_button_close_issue",
"style": "danger",
"value": "user"
}
],
"overflow_actions": [ // up to five actions here
{
"text": "Pin issue",
"action_id": "github_wo_button_pin_issue",
"style": "primary",
"value": "user"
},
{
"text": "Assign to me",
"action_id": "github_wo_button_assign_issue",
"style": "primary",
"value": "user"
}
]
},
"custom_fields": [],
"display_order": []
}
}
]
}
When a user clicks a button, a block_actions
interactivity request is sent to your app.
Handling block_actions
events
Your app can handle block_actions
events exactly like you'd respond to button action payloads from anywhere else in Slack.
The event payload looks slightly different when it's coming from an action on the unfurl vs. an action on the flexpane, but the properties in the container related to Work Objects are the same. This includes entity_url
, external_ref
, app_unfurl_url
, message_ts
, thread_ts
and channel_id
.
Below is an example payload when an action button on the unfurl is clicked:
{
"type": "block_actions",
"user": {
"id": "U123ABC456",
"username": "jennifer_hynes",
"name": "jennifer_hynes",
"team_id": "T123ABC456"
},
"api_app_id": "A123ABC456",
"token": "abc123",
"container": {
"type": "message_attachment",
"message_ts": "1753813500.959789",
"thread_ts": "1753813200.519449",
"channel_id": "C123ABC456",
"is_ephemeral": false,
"attachment_id": 1,
"is_app_unfurl": true,
"app_unfurl_url": "https://github.com/issues/139#comment123",
"entity_url": "https://github.com/issues/139",
"external_ref": {
"id": "139"
}
},
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689",
"team": {
"id": "T123ABC456",
"domain": "team-domain",
"enterprise_id": "E123ABC456",
"enterprise_name": "Acme Corp"
},
"enterprise": {
"id": "E123ABC456",
"name": "Acme Corp"
},
"is_enterprise_install": false,
"channel": {
"id": "C123ABC456",
"name": "jennifer-test"
},
"app_unfurl": {
"id": 1,
"bot_id": "B123ABC456",
"bot_team_id": "T123ABC456",
"app_unfurl_url": "https://github.com/KrishnaPatel1/github-function-test/issues/139",
"is_app_unfurl": true,
"fallback": "Create Capybara login page\n Issue in GitHub",
"text": "Issue in GitHub",
"title": "Create Capybara login page",
"title_link": "https://github.com/issues/139",
"service_name": "Work Objects Demo App",
"fields": [
{
"value": "open",
"title": "Status",
"short": true
},
{
"value": "KrishnaPatel1",
"title": "Assignee",
"short": true
},
{
"value": "FY26 Q2",
"title": "Milestone",
"short": true
}
]
},
"response_url": "https://hooks.slack.com/actions/T123/123/abc123",
"actions": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Summarize issue with AI",
"emoji": true
},
"action_id": "github_wo_button_summarize_issue",
"block_id": "TPdc",
"style": "primary",
"value": "user",
"action_ts": "1748809126.803329"
}
]
}
Note that container.type
will be message_attachment
when the event is coming from an action on the unfurl, and will be entity_detail
when the event is coming from an action on the flexpane.
Handling authentication
You can enforce authentication when a user clicks a button. If the user is not authenticated to perform the action, then your app can make a request to the entity.presentDetails
API method with user_auth_required=true
and a valid user_auth_url
. This will open the flexpane with an appropriate treatment that encourages the user to complete the authentication flow by following user_auth_url
. Other error types will not auto-open the flexpane. For non-authentication related errors, it’s best to send the user a DM for now.
Other ways to handle the request
- Open a modal to collect more information from the user.
- Post a message to the thread there the unfurl message is posted.
- Send a message to the user's DM if the action has failed.
- If the action is successful in the unfurl card, then refresh the unfurl by making a request to the
chat.unfurl
API method and by passing the newmetadata
. - If the action is successful in the flexpane, then refresh the flexpane by making a request to the
entity.presentDetails
API method with the newmetadata
. This way, the user will not have to manually click the refresh icon.
Supported entity types
Type | entity_type | Description |
---|---|---|
File | slack#/entities/file | This can represent a document, a spreadsheet, an image, etc. |
Task | slack#/entities/task | This can represent a ticket, a to-do, etc. |
Incident | slack#/entities/incident | This can represent an incident, a service interruption, etc. |
Content Item | slack#/entities/content_item | This can represent a content page, an article page, etc. |
Item | slack#/entities/item | A general-purpose entity that can represent anything. |
File
If your app is providing remote files, you'll want to use the same value for the the remote_file.external_id
property and the Work Object's external_ref.id
. This will allow us to provide the same external_ref
back to your app in the entity_details_requested
event triggered by a user opening the flexpane or preview from the files browser.
Supported fields
{
"fields": {
"preview": {
"type": "slack#/types/image",
"alt_text": "Document 123 image",
"image_url": "https://example.com/document-123-image.png" // must be a publicly-accessible URL or a `slack_file` URL for an image (https://docs.slack.dev/reference/block-kit/composition-objects/#slack_file)
},
"created_by": {
// You can identify a user in one of two ways:
// - Use "user_id" with a Slack user ID (e.g., "U123ABC456") if known
// - Use "text" with the user's display name if the Slack user ID is not available
"user": {
"user_id": "U123ABC456",
},
"type": "slack#/types/user"
},
"date_created": {
"value": 1741164235 // value in UNIX timestamp
},
"date_updated": {
"value": 1741164235 // value in UNIX timestamp
},
"last_modified_by": {
// You can identify a user in one of two ways:
// - Use "user_id" with a Slack user ID (e.g., "U123ABC456") if known
// - Use "text" with the user's display name if the Slack user ID is not available
"user": {
"user_id": "U123ABC456",
},
"type": "slack#/types/user"
},
"file_size": {
"value": "256MB"
},
"mime_type": {
"value": "image/gif"
}
},
"slack_file": {
// App's providing remote files as well as Work Objects should provide the
// file ID to ensure compatibility with Slack's unified files browser
"id": "F123ABC456",
// The file's extension can optionally be specified here to create an icon for the file using
// built-in file icons like https://a.slack-edge.com/470bf9a/img/wo-file-icons/icon_image.png
"type": "gif"
}
}
Task
Supported fields
{
"fields": {
"description": {
"value": "task description here",
"format": "markdown" // optional
},
"created_by": {
// You can identify a user in one of two ways:
// - Use "user_id" with a Slack user ID (e.g., "U123ABC456") if known
// - Use "text" with the user's display name if the Slack user ID is not available
"user": {
"user_id": "U0123456",
},
"type": "slack#/types/user"
},
"date_created": {
"value": 1741164235 // value in UNIX timestamp
},
"date_updated": {
"value": 1741164235 // value in UNIX timestamp
},
"assignee": {
// You can identify a user in one of two ways:
// - Use "user_id" with a Slack user ID (e.g., "U123ABC456") if known
// - Use "text" with the user's display name if the Slack user ID is not available
"user": {
"text": "John Smith",
"email" "johnsmith@example.com"
},
"type": "slack#/types/user"
},
"status": {
"value": "open", // the status of the task
"tag_color": "blue", // used to provide a colored "chip" around the text
"link": "https://example.com/tasks?status=open"
},
"due_date": {
"value": "2025-06-10", // date in the format 'YYYY-MM-DD', or an integer unix timestamp if the type is "slack#/types/timestamp"
"type": "slack#/types/date" // this can also be type "slack#/types/timestamp"
},
"priority": {
"value": "high", // the priority level of the task
"icon": {
// the icon will be rendered to the left of the text
"alt_text": "Icon to indicate a high priority item",
"url": "https://example.com/icon/high-priority.png"
},
"link": "https://example.com/tasks?priority=high"
}
}
}
Incident
Supported fields
{
"fields": {
"status": {
"value": "open" // the status of the incident
},
"severity": {
"value": "high" // severity level of the incident
},
"created_by": {
// You can identify a user in one of two ways:
// - Use "user_id" with a Slack user ID (e.g., "U123ABC456") if known
// - Use "text" with the user's display name if the Slack user ID is not available
"user": {
"user_id": "U0123456",
},
"type": "slack#/types/user"
},
"assigned_to": {
// You can identify a user in one of two ways:
// - Use "user_id" with a Slack user ID (e.g., "U123ABC456") if known
// - Use "text" with the user's display name if the Slack user ID is not available
"user": {
"text": "John Smith",
"email" "johnsmith@example.com"
},
"type": "slack#/types/user"
},
"date_created": {
"value": 1741164235 // value in UNIX timestamp
},
"date_updated": {
"value": 1741164235 // value in UNIX timestamp
},
"description": {
"value": "incident description here",
},
"service": {
"value": "backend" // the service that is affected by the incident
}
}
}
Content Item
Supported Fields
{
"fields": {
"preview": {
"type": "slack#/types/image",
"alt_text": "Page 123 review",
"image_url": "https://example.com/page-123-image.png" // must be a publicly-accessible URL or a `slack_file` URL for an image (https://docs.slack.dev/reference/block-kit/composition-objects/#slack_file)
},
"description": {
"value": "content description here",
},
"created_by": {
// You can identify a user in one of two ways:
// - Use "user_id" with a Slack user ID (e.g., "U123ABC456") if known
// - Use "text" with the user's display name if the Slack user ID is not available
"user": {
"user_id": "U0123456",
},
"type": "slack#/types/user"
},
"date_created": {
"value": 1741164235 // value in UNIX timestamp
},
"date_updated": {
"value": 1741164235 // value in UNIX timestamp
},
"last_modified_by": {
// You can identify a user in one of two ways:
// - Use "user_id" with a Slack user ID (e.g., "U123ABC456") if known
// - Use "text" with the user's display name if the Slack user ID is not available
"user": {
"user_id": "U0123456",
},
"type": "slack#/types/user"
}
}
}
Item
The item
entity does not contain a fields
object. It is a general-purpose entity in which all of the properties must be specified in the custom_fields
object.
Here is an example for a social media post item:
{
"custom_fields": [
{
"key": "saves",
"label": "Saves",
"value": 12,
"type": "integer"
},
{
"key": "likes",
"label": "Likes",
"value": 25,
"type": "integer"
},
{
"key": "username",
"label": "Username",
"value": "Slack Platform",
"type": "string"
},
{
"key": "caption",
"label": "Caption",
"value": "Check out my brand new Work Object app!",
"type": "string"
}
]
}
Supported properties for a field
For fields
and custom_fields
, here are the properties that may be supported depending on field type
:
Property | Required/Optional | Description |
---|---|---|
value | required | The value of the property. Not required for image properties such as preview . |
type | varies | Required in custom_fields and in certain some entity fields. See the entity's fields schema for guidance on where this is optional. See the section below for acceptable values. |
icon | optional | Can only be set when the type is string . This icon will be displayed next to field's text value. Not compatible with tag_color . More details available here. |
link | optional | Can only be set when the type is string , date , or timestamp . The field's content will be hyperlinked with the URL specified here. |
tag_color | optional | Can only be set when the type is string . Allows the string to be highlighted in of one of the following colors: red , yellow , green , gray , blue . e.g., tag_color: "red" . |
format | optional | Can only be set when the type is string . Allows the string to be formatted in markdown . Incompatible with the icon or link properties. Set format: "markdown" . |
item_type | optional | Required when the field's type is array . Value should be set to one of the valid array types. See valid array types in the next section. |
image_url | optional | Used when the field's type is slack#/types/image . |
long | optional | Can only be set when the type is string . Expands the field across a wider area in the unfurl card. Set long: true . |
slack_file | optional | Used when the field's type is slack#/types/image . The object schema can be found here. |
alt_text | optional | Used when the field's type is slack#/types/image . |
user | optional | Used when the field's type is slack#/types/user . See supported properties for user type. |
For custom_fields
, the key
and label
properties are required in addition to the value
and type
.
Property | Required/Optional | Description |
---|---|---|
key | required | Key that Slack will be use to reference this property. |
label | required | Label that will be displayed in the Work Object body. |
Data types
Here are the acceptable values for type
:
Type | Description |
---|---|
string | A string value. |
integer | An integer value. |
array | An array of items of a single type. |
slack#/types/user | A user value. You can represent a user by providing either user_id (when the Slack user ID is known e.g., U1234567) or text (when only the user's name is available e.g., John Smith). |
slack#/types/channel_id | A Slack conversation ID (e.g., C1234567). |
slack#/types/timestamp | A UNIX timestamp (e.g., 1741164235). |
slack#/types/date | A date in the format YYYY-MM-DD . |
slack#/types/image | An image. Provide either the URL of a publicly hosted image (image_url ) or a slack_file with a preview image. |
For arrays, the supported data types are:
string
integer
slack#/types/channel_id
slack#/types/user
Here is an example of a custom field with the array
type:
{
"type": "array",
"key": "array-of-strings",
"label": "Array of Strings",
"item_type": "string",
"value": [
{
"value": "A"
},
{
"value": "B"
}
]
}
User types
Here are acceptable properties for a user type:
Property | Required/Optional | Type | Description |
---|---|---|---|
user_id | optional | string | A slack user_id . Don't provide text when user_id is provided. |
text | optional | string | The full name of the user. Don't provide user_id when text is provided. |
url | optional | string | A link to an external profile for the user. |
email | optional | string | The email of the user. When the email provided matches an valid Slack user, we will display the Slack user. |
icon | optional | icon | An avatar for the user. Schema is described here. |
Icon properties
Properties such as the attributes.product_icon
property and the icon
property in string
fields take a common structure that looks like this:
{
"alt_text": "Summary of image's content",
"url": "https://example.com/icon", // publicly-accessible URL to an image
// OR
"slack_file": { // provide either file ID or the url. Only one is required
"id": "F0123456",
"url": "https://files.slack.com/files-pri/T0123456-F0123456/xyz.png"
}
}
The entity_details_requested
event
This event is sent to your app when a user clicks a Work Objects unfurl or refreshes the flexpane.
{
"type": "entity_details_requested",
"user": "U0123456", // user who opened the flexpane
"external_ref": {
// `external_ref` that was set in the `metadata` of `chat.unfurl`.
// This is not guaranteed to be set in all cases. For example,
// when a work object is opened from an Enterprise Search result
// provided by a Slack-developed search provider, we cannot provide
// an `external_ref`.
"id": "123",
"type": "my-type" // optional
},
// This is the URL that identifies the entity in the developer's system. In the entity metadata, this property is called `url`.
"entity_url": "https://example.com/document/123",
"link": {
"url": "https://example.com/document/123",
"domain": "example.com" // domain of the URL
},
// This is the exact URL that was unfurled in the Slack message. It could be the same as `entity_url`, or different.
"app_unfurl_url": "https://example.com/document/123?myquery=param",
"event_ts": "123456789.1234566", // event timestamp
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689", // event trigger ID
"user_locale": "en-US",
// These fields will not be provided when the entity details are opened
// from outside of a message context (i.e., Enterprise Search)
"channel": "C123ABC456", // ID of the channel where the source message was posted
"message_ts": "1755035323.759739", // timestamp of the source message
"thread_ts": "1755035323.759739", // timestamp of the root message, if the source message is part of a thread
}
The user_locale
property
When building the entity details you'll be passing to the entity.presentDetails
API method, you'll want to make sure any strings for labels, statuses, select options, and so on are mapped to the user's locale in Slack. To help facilitate this, we provide an IETF language tag (i.e., en-US
, en-GB
, fr-FR
, etc.) in both the link_shared
and entity_details_requested
events. The full list of languages supported by Slack can be found here.
Flexpane content refresh
The entity_details_requested
event is always sent to your app in the following scenarios:
- When a user opens a Work Object Unfurl for the first time.
- When a user clicks on the on the refresh button in the flexpane.
Assuming a Work Object flexpane was previously opened by a user, the content has a 10 minute refresh timer (TTL) before another event is sent to your app. After the 10 minutes has elapsed, the entity_details_requested
event will be sent to your app in the following scenarios:
- A user opens the Work Object flexpane for the 2nd, 3rd, or Xth time.
- A user has the flexpane open and switches between the
Details
andConversations
tabs of the flexpane. - A user clicks on the flexpane refresh button.
Additionally, if a user has the flexpane open in their client, the refresh button will have a small red dot indicating that the data may be stale.
If the 10 minute refresh timer has not elapsed, the entity_details_requested
event will not be sent unless the user explicitly clicks the refresh button. The following scenarios do not trigger the event:
- A user closing and re-opening the flexpane.
- A user switching between the
Details
andConversations
tabs of the flexpane. - A user interacting with any other elements within the flexpane or Work Object.
The entity.presentDetails
API method
This API method allows an app to publish additional information in the flexpane view.
Parameter | Type | Required/Optional | Description |
---|---|---|---|
trigger_id | string | required | The trigger_id obtained from the entity_details_requested event. |
metadata | string | required | The URL-encoded JSON of the Work Object entity to present in the flexpane. |
user_auth_required | boolean | optional | Set to true (or 1 ) to indicate that the user must authenticate to view full flexpane data. |
user_auth_url | string | optional | A URL to which users are directed for authentication if user_auth_required is set to true . |
error | object | optional | Object to specify why an entity could not be presented (e.g., user authentication error). |
Sample error response
{
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689",
"error": {
"status": "string", // can be one of: ["restricted", "internal_error", "not_found", "custom", "custom_partial_view", "timeout", "edit_error"]
"custom_title": "string", // if the status is set to `custom`, specify the title here
"custom_message": "string", // if the status is set to `custom`, specify the message here
"message_format": "string", // Set this to `markdown` to enable additional formatting in the message body
"actions": [] // action button(s) that will be added to the flexpane
}
}
For actions in the error object, see the action button schema.
Errors with partial views
Some errors should be expected as part of standard authentication and access request flows. Slack will do this by default when you respond with user_auth_required=true
or error_status=restricted
. If you would like to do the same for a custom error, you can use the custom_partial_view
error status.
{
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689",
"error": {
"status": "custom_partial_view",
"custom_title": "Ruh roh",
"custom_message": ":hand: This item is *restricted* per our [company policy](https://example.com). Don't worry though, you can request access using the button below.",
"message_format": "markdown",
"actions": [
{
"text": "Request access",
"action_id": "request_access",
"value": "some_val",
"processing_state": {
"enabled": true // This can be enabled to disable the button and show a loading state for up to 30 seconds or until your app responds with another call to the `entity.presentDetails` API method.
}
}
]
}
}
If you implement this with an action button, you can present a new error screen in response to the button using a payload as follows:
{
"trigger_id": "1234567890123.1234567890123.abcdef01234567890abcdef012345689",
"error": {
"status": "custom_partial_view",
"custom_title": "Access requested",
"custom_message": ":hourglass_flowing_sand: The team is reviewing your request. For urgent needs, see [this runbook](https://example.com).",
"message_format": "markdown"
}
}
Slack SDKs
Work Object support is coming soon to the BoltJS, Bolt for Java, and Bolt for Python SDKs.
Slack Marketplace submission and launch considerations
If your app already supports the link unfurls feature and has all the required scopes, workspace re-authentication is not needed for Work Object unfurls to appear in customer workspaces Additionally, since the Work Objects feature requires a new event subscription, a new Slack Marketplace submission is required. More details are available here.