Skip to main content

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.

Unfurl component

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.

Flexpane component

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:

  1. Visit https://api.slack.com/apps and select your app.
  2. Navigate to Work Object Previews under the left sidebar menu.
  3. Enable the toggle.
  4. Select the entity type(s) that you would like to add to your app. Supported entity types can be found here.
  5. Click Save.

App settings

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
}
]
}
ParameterDescription
app_unfurl_urlThe URL link posted by the user in a conversation. This is the same URL obtained from the link_shared event.
entitiesThe 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_typeEach 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_refContains 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.
urlThe 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:

  1. Visit https://api.slack.com/apps and select your app.
  2. Navigate to the Events & Subscriptions section in the sidebar.
  3. Click the Subscribe to bot events section.
  4. 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 the entity 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.

Flexpane editing

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:

PropertyTypeDescription
enabledbooleanToggle to enable edit mode on this field.
placeholderobjectPlaceholder text shown on empty text and number inputs.
hintobjectHint text is displayed below the input on supported input blocks.
optionalbooleanWhether this field can be left blank.
selectobjectOptions relevant to select and multi-select inputs.
numberobjectAdditional properties relevant to number type fields.
textobjectAdditional properties relevant to text type fields.

The placeholder and hint properties have the following properties:

PropertyTypeDescription
typestringSet type: "plain_text".
textstringText to be displayed.
emojibooleanWhether 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:

PropertyTypeDescription
min_valuenumberMinimum allowed value.
max_valuenumberMaximum allowed value.

The text property has the following properties:

PropertyTypeDescription
min_lengthnumberMinimum string length. Cannot be lower than 0 or greater than 3000.
max_lengthnumberMaximum string length. Cannot be lower than 0 or greater than 3000.

The select property has the following properties:

PropertyTypeDescription
current_valuestringThe current value of the select input.
current_valuesarray of stringsIf 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_optionsarray of objectsIf present on a valid field, then these values will appear on the fields as static select options.
fetch_options_dynamicallybooleanWhen 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:

PropertyRequiredTypeDescription
valueYesstringThe value that will be sent to your application when the user submits the form. Max length: 150 characters.
textYesobjectHuman-readable text that will be displayed in the select menu. The object schema can be found here. Max length: 75 characters.
descriptionNoObjectA 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 typeSupportedRestrictions
CheckboxesSoonWill be limited to a single checkbox for boolean types
Date pickerYesDefault input for Work Object date fields
Datetime pickerYesDefault input for Work Object datetime fields
EmailSoonDefault input for Work Object email fields
FileNo
Multi-selectYesThis 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.
NumberYesMin/max and whether decimals are allowed can be configured via field edit metadata
Plain-textYesMaximum of 3000 characters allowed. Edit will be disabled for any field with a value larger than this
Radio button groupNoUse selects instead.
Rich textNoRich text editing is not currently supported. As an alternative, raw markdown can be used in a standard text input.
SelectYesFor 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 pickerYes
URLSoonDefault 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![Image](https://github.com/user-attachments/assets/f94f0a80-993d-46c5-be4c-1d3f3cd8312d)"
}
}
}
},
"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:

  1. Client-side field level validation within Slack via edit properties.
  2. Server-side field level validation via response to view_submission.
  3. 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,
}
}
},
]
}
}

Client-side errors

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 field errors

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",
}
}

Server-side form errors

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.

Actions

Additionally, up to five actions can be defined in the overflow menu. This can be accessed by clicking More actions in the overflow menu.

Actions unfurl overflow

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:

PropertyRequiredTypeDescription
textYesstringHuman readable text that will be displayed in the button.
action_idYesstringAn 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.
valueNostringThe value to send along with the interaction payload. Maximum length is 2000 characters.
styleNostringDecorates the button with either a green or red background. Use style: primary for green and style: danger for red.
urlNostringClicking the button will open the URL in the user's browser. Max length: 3000 characters.
accessibility_labelNostringA 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 new metadata.
  • If the action is successful in the flexpane, then refresh the flexpane by making a request to the entity.presentDetails API method with the new metadata. This way, the user will not have to manually click the refresh icon.

Supported entity types

Typeentity_typeDescription
Fileslack#/entities/fileThis can represent a document, a spreadsheet, an image, etc.
Taskslack#/entities/taskThis can represent a ticket, a to-do, etc.
Incidentslack#/entities/incidentThis can represent an incident, a service interruption, etc.
Content Itemslack#/entities/content_itemThis can represent a content page, an article page, etc.
Itemslack#/entities/itemA general-purpose entity that can represent anything.

File

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

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

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

Incident

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:

PropertyRequired/OptionalDescription
valuerequiredThe value of the property. Not required for image properties such as preview.
typevariesRequired 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.
iconoptionalCan 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.
linkoptionalCan only be set when the type is string, date, or timestamp. The field's content will be hyperlinked with the URL specified here.
tag_coloroptionalCan 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".
formatoptionalCan 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_typeoptionalRequired 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_urloptionalUsed when the field's type is slack#/types/image.
longoptionalCan only be set when the type is string. Expands the field across a wider area in the unfurl card. Set long: true.
slack_fileoptionalUsed when the field's type is slack#/types/image. The object schema can be found here.
alt_textoptionalUsed when the field's type is slack#/types/image.
useroptionalUsed 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.

PropertyRequired/OptionalDescription
keyrequiredKey that Slack will be use to reference this property.
labelrequiredLabel that will be displayed in the Work Object body.

Data types

Here are the acceptable values for type:

TypeDescription
stringA string value.
integerAn integer value.
arrayAn array of items of a single type.
slack#/types/userA 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_idA Slack conversation ID (e.g., C1234567).
slack#/types/timestampA UNIX timestamp (e.g., 1741164235).
slack#/types/dateA date in the format YYYY-MM-DD.
slack#/types/imageAn 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:

PropertyRequired/OptionalTypeDescription
user_idoptionalstringA slack user_id. Don't provide text when user_id is provided.
textoptionalstringThe full name of the user. Don't provide user_id when text is provided.
urloptionalstringA link to an external profile for the user.
emailoptionalstringThe email of the user. When the email provided matches an valid Slack user, we will display the Slack user.
iconoptionaliconAn 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.

Flexpane refresh button

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 and Conversations 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.

Flexpane refresh button 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 and Conversations 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.

ParameterTypeRequired/OptionalDescription
trigger_idstringrequiredThe trigger_id obtained from the entity_details_requested event.
metadatastringrequiredThe URL-encoded JSON of the Work Object entity to present in the flexpane.
user_auth_requiredbooleanoptionalSet to true (or 1) to indicate that the user must authenticate to view full flexpane data.
user_auth_urlstringoptionalA URL to which users are directed for authentication if user_auth_required is set to true.
errorobjectoptionalObject 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.
}
}
]
}
}

Custom partial access request

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"
}
}

Custom partial access request updated

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.