From fe44223600cb880350643dbdb9092eec84cc1b60 Mon Sep 17 00:00:00 2001 From: waleed Date: Mon, 29 Jun 2026 22:10:08 -0700 Subject: [PATCH] improvement(slack-trigger): expose view, message, and state on interactivity payloads The native Slack trigger flattened every interaction into scalar event.* fields and dropped the structured objects, so view_submission and block-rewrite workflows could not read view.state.values, view.private_metadata, or message.blocks. Pass the full Slack view and message objects through, plus the top-level block_actions state (state.values), and declare them in the trigger outputs so they surface in the editor. Additive and backwards compatible: existing flattened fields are unchanged and new fields default to null. --- apps/sim/lib/webhooks/providers/slack.test.ts | 51 ++++++++++++++++++- apps/sim/lib/webhooks/providers/slack.ts | 25 +++++++++ apps/sim/triggers/slack/webhook.ts | 15 ++++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/webhooks/providers/slack.test.ts b/apps/sim/lib/webhooks/providers/slack.test.ts index 641f17ad25b..1c261ba3ec3 100644 --- a/apps/sim/lib/webhooks/providers/slack.test.ts +++ b/apps/sim/lib/webhooks/providers/slack.test.ts @@ -56,7 +56,13 @@ describe('slackHandler formatInput - interactivity (block_actions)', () => { trigger_id: 'trigger-1', response_url: 'https://hooks.slack.com/actions/abc', container: { message_ts: '999.000' }, - message: { ts: '999.000', text: 'Approve this?', thread_ts: '999.aaa' }, + message: { + ts: '999.000', + text: 'Approve this?', + thread_ts: '999.aaa', + blocks: [{ type: 'section', block_id: 'b1', text: { type: 'mrkdwn', text: 'Approve?' } }], + }, + state: { values: { reason_block: { reason_input: { value: 'looks good' } } } }, actions: [ { action_id: 'approve_btn', @@ -85,6 +91,49 @@ describe('slackHandler formatInput - interactivity (block_actions)', () => { expect(event.api_app_id).toBe('A123') expect(Array.isArray(event.actions)).toBe(true) expect((event.actions as unknown[]).length).toBe(1) + const message = event.message as Record + expect(message).not.toBeNull() + expect(Array.isArray(message.blocks)).toBe(true) + expect((message.blocks as unknown[]).length).toBe(1) + expect(event.view).toBeNull() + const state = event.state as { values: Record> } + expect(state).not.toBeNull() + expect(state.values.reason_block.reason_input.value).toBe('looks good') + }) + + it('carries the full view (state.values + private_metadata) through for a view_submission', async () => { + const { input } = await slackHandler.formatInput!( + ctx({ + type: 'view_submission', + user: { id: 'U1', username: 'alice' }, + team: { id: 'T1' }, + trigger_id: 'trigger-2', + view: { + id: 'V123', + callback_id: 'create_ticket', + private_metadata: '{"thread_ts":"999.aaa"}', + hash: 'abc.def', + state: { + values: { + summary_block: { summary_input: { type: 'plain_text_input', value: 'Printer down' } }, + }, + }, + }, + }) + ) + const event = eventOf(input) + expect(event.event_type).toBe('view_submission') + expect(event.callback_id).toBe('create_ticket') + const view = event.view as Record + expect(view).not.toBeNull() + expect(view.private_metadata).toBe('{"thread_ts":"999.aaa"}') + const values = (view.state as Record).values as Record< + string, + Record> + > + expect(values.summary_block.summary_input.value).toBe('Printer down') + expect(event.message).toBeNull() + expect(event.state).toBeNull() }) it('normalizes a static_select value and falls back to action value for text', async () => { diff --git a/apps/sim/lib/webhooks/providers/slack.ts b/apps/sim/lib/webhooks/providers/slack.ts index 1682b277394..2d83b196dd1 100644 --- a/apps/sim/lib/webhooks/providers/slack.ts +++ b/apps/sim/lib/webhooks/providers/slack.ts @@ -74,6 +74,25 @@ interface SlackTriggerEvent { callback_id: string api_app_id: string message_ts: string + /** + * Full Slack view object for modal interactions (view_submission/view_closed): + * `state.values` (submitted input values), `private_metadata`, `id`, + * `callback_id`, `hash`, etc. Null for non-modal interactions and Events API. + */ + view: Record | null + /** + * Full Slack message object the interaction originated from (block_actions): + * `blocks`, `text`, `ts`, etc. — needed to rewrite the source message's blocks. + * Null when the interaction has no source message and for slash/Events API. + */ + message: Record | null + /** + * Top-level interactivity `state` for block_actions: the current values of all + * stateful elements in the surface (`state.values`), e.g. inputs read on a + * button click without a modal submit. Distinct from `view.state` (modal + * submissions). Null for non-block_actions payloads. + */ + state: Record | null hasFiles: boolean files: SlackDownloadedFile[] } @@ -104,6 +123,9 @@ function createSlackEvent(): SlackTriggerEvent { callback_id: '', api_app_id: '', message_ts: '', + view: null, + message: null, + state: null, hasFiles: false, files: [], } @@ -206,11 +228,14 @@ function formatSlackInteractive(b: Record): SlackTriggerEvent { // Prefer the source message text; fall back to the triggering action's value // so a blocks-only message still surfaces something useful in `text`. event.text = asString(message?.text) || event.action_value + event.message = message ?? null event.response_url = asString(b.response_url) event.trigger_id = asString(b.trigger_id) const view = b.view as Record | undefined event.callback_id = asString(b.callback_id) || asString(view?.callback_id) + event.view = view ?? null + event.state = (b.state as Record) ?? null event.api_app_id = asString(b.api_app_id) return event diff --git a/apps/sim/triggers/slack/webhook.ts b/apps/sim/triggers/slack/webhook.ts index e87db1f7127..dad856e3222 100644 --- a/apps/sim/triggers/slack/webhook.ts +++ b/apps/sim/triggers/slack/webhook.ts @@ -177,6 +177,21 @@ export const slackWebhookTrigger: TriggerConfig = { description: 'Timestamp of the message the interaction originated from. Present for block_actions', }, + view: { + type: 'json', + description: + 'Full Slack view object for modal interactions: state.values (submitted input values), private_metadata, id, callback_id, and hash. Present for view_submission/view_closed; null otherwise', + }, + message: { + type: 'json', + description: + 'Full source message object the interaction came from, including its blocks and text. Present for block_actions on a message; null otherwise', + }, + state: { + type: 'json', + description: + 'Current values of all stateful elements in the surface (state.values) at the time of a block action — e.g. inputs read on a button click. Present for block_actions; null otherwise', + }, hasFiles: { type: 'boolean', description: 'Whether the message has file attachments',