Skip to content

When someone clicks a button, picks an option, changes a date or submits a form in something your app posted, Workstation sends your app a request — the same payloads Slack sends. This page covers clicks and changes (block_actions), answering them, and live option lists (block_suggestion). Forms are covered in Modals.

Open Settings → Integrations & Developers → Developer Apps → Bots, expand your bot and fill in Interactivity. Workstation sends:

Only the app that posted a message hears about its controls. Every request is signed — verify it.

{
"type": "block_actions",
"team": { "id": "1bf87154-…" },
"user": { "id": "42bc76d7-…", "username": "[email protected]", "name": "Dana Reader", "team_id": "1bf87154-…" },
"api_app_id": "75385d0c-…",
"container": { "type": "message", "message_ts": "3a77c808-…", "channel_id": "e0fd46e9-…", "is_ephemeral": false },
"trigger_id": "c0b6f3d1-…",
"channel": { "id": "e0fd46e9-…", "name": "refunds" },
"message": { "type": "message", "ts": "3a77c808-…", "bot_id": "75385d0c-…", "blocks": [ … ] },
"state": {
"values": {
"pick": { "reason": { "type": "static_select", "selected_option": { "text": { "type": "plain_text", "text": "Damaged" }, "value": "damaged" } } }
}
},
"response_url": "https://workstation.repzo.com/api/bot/responses/eyJ…",
"actions": [
{
"type": "button",
"action_id": "approve",
"block_id": "decision",
"text": { "type": "plain_text", "text": "Approve" },
"value": "refund-42",
"style": "primary",
"action_ts": "1791799555.321000"
}
]
}
  • actions[0] is what was used. Its value field depends on the element — value for buttons, selected_option for menus and radio buttons, selected_date, selected_time, selected_date_time, selected_user(s), selected_conversation(s)… Each element page lists its own.
  • state.values holds the current value of every other control in the message, keyed by block_id then action_id, so a Submit button can read the menus next to it.
  • People can only pick what your blocks offer: option text in the payload comes from your message, never from the person’s browser.
  • In a modal or Home tab, container is { "type": "view", "view_id": "…" }, the payload carries view instead of message, and there is no response_url.

Answer with any 2xx. Clicks are delivered from a queue, so unlike Slack you don’t have to answer within three seconds — but a failed delivery isn’t retried, so keep the endpoint up. Each delivery is logged, and a subscription that fails five times in a row is turned off: fix the URL and turn it back on in the bot’s settings.

Each payload from a message has a response_url. POST JSON to it to change what the person sees — no token needed. It works five times within 30 minutes.

Body Effect
{ "replace_original": true, "text": "…", "blocks": [ … ] } Replace the message the control was in
{ "delete_original": true } Delete that message
{ "response_type": "in_channel", "text": "…", "blocks": [ … ] } Post a new message in the same conversation (and thread)
{ "text": "…", "blocks": [ … ] } Post an ephemeral message only the person who clicked sees (Slack’s default)
Terminal window
curl -X POST "$RESPONSE_URL" -H "Content-Type: application/json" \
-d '{ "replace_original": true, "text": "Refund approved ✅" }'

attachments are accepted in every case. You can also edit the message any time with chat.update.

Every interaction payload has a trigger_id. Pass it to views.open within 60 seconds to open a modal for that person; each trigger works once. See Modals.

Add a confirm object to a button, menu or picker to ask “Are you sure?” before the action is sent.

An external_select asks your app for options as the person types. Once they type at least min_query_length characters (default 3), Workstation POSTs, signed:

{
"type": "block_suggestion",
"user": { "id": "42bc76d7-…", "name": "Dana Reader" },
"api_app_id": "75385d0c-…",
"container": { "type": "message", "message_ts": "3a77c808-…", "channel_id": "e0fd46e9-…" },
"block_id": "lookup",
"action_id": "customer",
"value": "lin"
}

Answer within 3 seconds with up to 100 options or option groups:

{
"options": [
{ "text": { "type": "plain_text", "text": "Lina Haddad" }, "value": "cust-1" },
{ "text": { "type": "plain_text", "text": "Linda Saleh" }, "value": "cust-2" }
]
}

The person can only pick an option you returned (or the element’s initial_option); anything else is refused before it reaches you.

To hear about Home tab opens, subscribe to app_home_opened — it arrives as a regular event ({ "type": "event_callback", "event": { … } }). See App Home.