Skip to Content
SimulatorsSlackInteractivity

Simulators

Interactivity

A person can press a button on your bot’s message, or submit your bot’s modal, and your app receives Slack’s block_actions or view_submission payload. The press is a call the Slack client makes, so it carries X-Rystic-Surface: client and the person’s identity (a bound token or X-Rystic-Actor); without the header both methods answer unknown_method, as on the Web API. Post a message with a button, press it as a person and see what the simulator answers when no app is listening:

curl -s -X POST "$RYSTIC_URL/_rystic/scenario" -d '{"name":"design_studio"}' -o /dev/null
curl -s -X POST "$RYSTIC_URL/api/chat.postMessage" -H 'Authorization: Bearer xoxb-local' --data-urlencode 'blocks=[{"type":"actions","block_id":"review","elements":[{"type":"button","action_id":"approve","text":{"type":"plain_text","text":"Approve"}}]}]' -d channel=CWDC0000004 -d 'text=Review the draft' | jq -c '{ok, ts}'
# -> {"ok":true,"ts":"1772323200.000001"}
curl -s -X POST "$RYSTIC_URL/api/blocks.actions" -H 'Authorization: Bearer xoxp-anything' -d channel=CWDC0000004 -d ts=1772323200.000001 -d action_id=approve | jq -c .
# -> {"error":"unknown_method","ok":false,"req_method":"blocks.actions"}
curl -s -X POST "$RYSTIC_URL/api/blocks.actions" -H 'Authorization: Bearer xoxp-anything' -H 'X-Rystic-Actor: UWDC0000020' -H 'X-Rystic-Surface: client' -d channel=CWDC0000004 -d ts=1772323200.000001 -d action_id=approve | jq -c .
# -> {"error":"app_unreachable","ok":false}
curl -s -X POST "$RYSTIC_URL/api/blocks.actions" -H 'Authorization: Bearer xoxp-anything' -H 'X-Rystic-Actor: UWDC0000020' -H 'X-Rystic-Surface: client' -d channel=CWDC0000004 -d ts=1772323200.000001 -d action_id=reject | jq -c .
# -> {"error":"action_not_found","ok":false}

The two methods

blocks.actions takes channel and ts (the message), action_id and optionally block_id, and presses that button. views.submit takes view_id and state ({"values":{"<block_id>":{"<action_id>":{…}}}}), the modal’s inputs.

Where the payload goes

The app whose app_id the message or view carries receives it. Over Socket Mode it is an interactive envelope on that app’s connections alone. Otherwise it is a signed form post of payload=<JSON> to SLACK_TWIN_INTERACTIVITY_URL, or to SLACK_TWIN_EVENTS_URL when that is unset, since Bolt serves both on one path. Either needs SLACK_TWIN_SIGNING_SECRET; without it the simulator refuses to start. Only the default app has a request URL. Events API covers the two delivery modes.

Acks and errors

Your app acks within 3 seconds, by envelope_id or with a 2xx, and the call answers {"ok":true}, with "ack" holding what your app sent back, such as a submit’s {"response_action":"errors",…}. An interaction is never retried.

Otherwise the call answers app_unreachable, app_timeout or app_error (a non-2xx, or an answer cut short or over 1 MiB). A message, button or view that is not there answers message_not_found, action_not_found or view_not_found. These codes are the simulator’s own: Slack shows the person a generic error. A response_url answers used_url once its five calls are spent and expired_url after 30 minutes.

Trigger ids and response URLs

Each interaction carries a trigger_id that lives 3 seconds on the simulator’s clock and is exchanged once. views.open does not take it yet; it still needs a seeded state_trigger.

response_url is on the simulator’s own origin and answers 5 calls within 30 minutes of simulator time, made as your app. replace_original: true updates the message, delete_original: true deletes it, response_type: "in_channel" posts in the channel, and otherwise the reply is ephemeral to the person who pressed, Slack’s default. Your app can call chat.update or chat.postEphemeral with its bot token instead.

Not modeled

Select menus, shortcuts, slash commands and views.push are not modeled, and neither is view_closed.

Last updated on