Context
Running OpenClaw v2026.4.9 (pin 0512059d) with channels.slack in Socket Mode and a set of custom plugins. The Slack monitor currently routes three Slack interaction event families from Bolt into the OpenClaw agent runtime via enqueueSystemEvent:
There is no equivalent handler for Slack shortcuts — neither the global shortcut type ("type": "shortcut") nor the message-action shortcut type ("type": "message_action"). A grep of extensions/slack/src/ in v2026.4.9 for message_action, app.shortcut, or ctx.app.shortcut returns zero matches:
$ grep -rn 'message_action\|app\.shortcut\|ctx\.app\.shortcut' extensions/slack/src/
# (no output)
Bolt silently drops unhandled shortcut events. The net effect is that a custom Slack app manifest that registers either shortcut type simply does nothing when triggered — no log line, no system event, no agent turn. The Slack plugin reports this as a known limitation; our project tracks it as workaround item #3 in our OpenClaw upgrade notes.
Proposal
Add extensions/slack/src/monitor/events/interactions.shortcuts.ts, mirroring the interactions.block-actions.ts pattern, that:
- Registers a regex matcher with bolt:
ctx.app.shortcut(/.+/, handler) — matching all shortcut callback_ids, same way ctx.app.action(/.+/, ...) matches all action IDs. If per-scope filtering is desirable, this can be split into two calls: ctx.app.shortcut({ type: 'shortcut', callback_id: /.+/ }, handler) and ctx.app.shortcut({ type: 'message_action', callback_id: /.+/ }, handler).
- Parses the shortcut payload into a typed summary (discriminated union between
"global_shortcut" and "message_action"), extracting the fields needed for session routing and context:
- Common:
callbackId, userId, teamId, triggerId, actionTs.
- Message shortcut only:
channelId, channelName, messageTs, messageText, messageUserId, responseUrl, and (optionally) sanitized messageBlocks.
- Global shortcut only: none beyond the common set.
- Resolves a session key via the existing
ctx.resolveSlackSystemEventSessionKey(...) helper. For message shortcuts, use { channelId, channelType, senderId: userId }. For global shortcuts (which have no channel), fall back to a DM-scoped key: { channelId: null, channelType: "im", senderId: userId } — or whatever the team prefers as the canonical "no channel context" path.
- Calls
enqueueSystemEvent with a new event text prefix (e.g., "Slack shortcut: ") and the same { sessionKey, contextKey } shape as the block_actions handler. contextKey would be: ["slack:shortcut", callbackId, channelId?, messageTs?].filter(Boolean).join(":").
- Exports a
registerSlackShortcutHandler({ ctx, formatSystemEvent }) that registerSlackInteractionEvents calls alongside registerSlackBlockActionHandler in interactions.ts:180. Guard with if (typeof ctx.app.shortcut !== "function") return; same way the existing code guards ctx.app.view.
- Optionally scope by callback_id prefix (e.g.,
openclaw:) the same way modal matchers use modalMatcher = new RegExp(^${OPENCLAW_ACTION_PREFIX}). This lets OpenClaw avoid fighting with bolt apps that register their own shortcut handlers.
The InteractionSummary type in interactions.block-actions.ts:56 already has interactionType: "block_action" | "view_submission" | "view_closed" — add "global_shortcut" | "message_shortcut" to the union (or introduce a sibling ShortcutSummary type if the field set diverges too much). Reusing formatSlackInteractionSystemEvent from interactions.ts:126 would give us consistent truncation/sanitization for free — the sanitizer already walks arbitrary payloads.
No new SystemEventType variant is strictly required: the existing enqueueSystemEvent(text, { sessionKey, contextKey }) signature is string-typed and plugins just grep the event text. We already use the Slack interaction: prefix for block_actions; the new Slack shortcut: prefix is a natural parallel and subscribers can match on it.
Reference: existing enqueueSlackBlockActionEvent shape
From interactions.block-actions.ts:600–637, for parity:
function enqueueSlackBlockActionEvent(params: {
ctx: SlackMonitorContext;
parsed: ParsedSlackBlockAction;
auth: { channelType?: "im" | "mpim" | "channel" | "group" };
formatSystemEvent: (payload: Record<string, unknown>) => string;
}): void {
const eventPayload: InteractionSummary = {
interactionType: "block_action",
actionId: params.parsed.actionId,
// ... fields ...
};
const sessionKey = params.ctx.resolveSlackSystemEventSessionKey({
channelId: params.parsed.channelId,
channelType: params.auth.channelType,
senderId: params.parsed.userId,
});
const contextParts = [
"slack:interaction",
params.parsed.channelId,
params.parsed.messageTs,
params.parsed.actionId,
].filter(Boolean);
enqueueSystemEvent(params.formatSystemEvent(eventPayload), {
sessionKey,
contextKey: contextParts.join(":"),
});
}
The shortcut handler's enqueue call would be structurally identical, differing only in the payload fields and the contextKey prefix.
Use cases (why this matters)
- "Summarize this thread" message-action shortcut. Today, Slack users have to @-mention the bot inline or switch to a DM to get thread-level summarization. A message-action shortcut (right-click a message → "Summarize thread with OpenClaw") is the canonical Slack UX for this — and it's the pattern most Slack-native tools (Linear, Notion, GitHub) use for context-menu actions. Our Finley deployment has a
slack_summarize_thread tool ready but has no clean ergonomic entry point.
- "Create Linear issue from this message" message-action shortcut. Convert any Slack message directly into a Linear ticket. Our lead-follow-up plugin already has the machinery to parse unstructured text into a structured record — this shortcut would let any employee file a ticket without leaving Slack. Without shortcut routing, the only alternatives are: (a) require the bot mention, which pollutes the thread, or (b) post Block Kit action buttons on every message, which is spammy.
- Global "Ask OpenClaw" shortcut (⚡ menu). Opens an ephemeral/DM session from anywhere in Slack, without needing to know the bot's user ID or navigate to the bot's DM. This is how Slack's first-party assistants (including Slack AI) surface themselves — parity matters for adoption.
All three of these are dead in the water today: the Slack manifest allows registering the shortcuts, Slack delivers the events over the Socket Mode WebSocket, bolt's middleware layer receives them, but OpenClaw's Slack monitor never subscribes so they get silently discarded by bolt's unhandled-event path.
Estimated effort
Based on the line counts of the reference implementation:
interactions.block-actions.ts is 798 lines, but much of that is legacy Block Kit confirmation-message rewriting (buildSlackConfirmationBlocks, updateSlackLegacyBlockAction) and plugin-binding approval routing (handleSlackPluginBindingApproval, dispatchSlackPluginInteraction) that have no analog for shortcuts — shortcuts don't have an inline message that needs in-place updating, and we wouldn't wire the shortcut handler into the plugin-conversation binding system in a first pass.
interactions.modal.ts is 262 lines and is the closer structural analog: parse → summarize → session-key → enqueue.
A shortcut handler mirroring the modal lifecycle pattern (without the block-actions legacy surface) lands around 200–300 lines including types, payload parsing, session-key resolution, the enqueue call, and the registerSlackShortcutHandler export. Plus ~100 lines of table-driven tests against a mock bolt app, consistent with the existing interactions.test.ts.
Total PR: ~300–400 lines of new code + tests, 4–6 hours of work including writing tests that match the project's existing style.
Offer to PR
I'm willing to submit the PR following the interactions.block-actions.ts + interactions.modal.ts pattern. I'd propose scoping the first PR to:
- New file
extensions/slack/src/monitor/events/interactions.shortcuts.ts with registerSlackShortcutHandler.
- Extension to the
InteractionSummary union (or a new sibling type).
- Registration call added to
registerSlackInteractionEvents in interactions.ts.
- New tests in
interactions.test.ts covering: global shortcut delivers a system event, message shortcut delivers a system event with channel/message context, callback_id prefix filtering (if that pattern is chosen), session-key resolution parity with block_actions.
- Documentation update in the Slack channel docs listing
shortcut / message_action alongside block_actions as routed interaction types.
Out of scope for the first PR (can be follow-ups):
- Plugin-level shortcut registration API (analogous to
dispatchSlackPluginInteractiveHandler).
- Callback_id → session-key mapping config (for routing shortcuts to specific agents).
- Automatic Block Kit acknowledgement messages (the way block_actions rewrites the source message).
Happy to start on this as soon as the proposal has rough buy-in from a maintainer, so I don't re-invent naming or scope that someone's already thought through.
Environment
- OpenClaw: v2026.4.9 (commit
0512059dd4e6d2e582393828b837c376ddc4719c)
- Slack mode: Socket Mode (
SLACK_APP_TOKEN + SLACK_BOT_TOKEN)
- Bolt JS version: whatever v2026.4.9's
extensions/slack/package.json resolves
- Verified gap on: 2026-04-09 via source grep against the pinned commit
Context
Running OpenClaw v2026.4.9 (pin
0512059d) withchannels.slackin Socket Mode and a set of custom plugins. The Slack monitor currently routes three Slack interaction event families from Bolt into the OpenClaw agent runtime viaenqueueSystemEvent:block_actions— wired inextensions/slack/src/monitor/events/interactions.block-actions.ts:791viactx.app.action(/.+/, ...)and enqueued at line 633.view_submission— wired inextensions/slack/src/monitor/events/interactions.ts:192viactx.app.view(modalMatcher, ...), enqueued throughregisterModalLifecycleHandlerininteractions.modal.ts.view_closed— same handler, different lifecycle path, lines 211–219 ofinteractions.ts.There is no equivalent handler for Slack shortcuts — neither the global shortcut type (
"type": "shortcut") nor the message-action shortcut type ("type": "message_action"). A grep ofextensions/slack/src/in v2026.4.9 formessage_action,app.shortcut, orctx.app.shortcutreturns zero matches:Bolt silently drops unhandled shortcut events. The net effect is that a custom Slack app manifest that registers either shortcut type simply does nothing when triggered — no log line, no system event, no agent turn. The Slack plugin reports this as a known limitation; our project tracks it as workaround item #3 in our OpenClaw upgrade notes.
Proposal
Add
extensions/slack/src/monitor/events/interactions.shortcuts.ts, mirroring theinteractions.block-actions.tspattern, that:ctx.app.shortcut(/.+/, handler)— matching all shortcutcallback_ids, same wayctx.app.action(/.+/, ...)matches all action IDs. If per-scope filtering is desirable, this can be split into two calls:ctx.app.shortcut({ type: 'shortcut', callback_id: /.+/ }, handler)andctx.app.shortcut({ type: 'message_action', callback_id: /.+/ }, handler)."global_shortcut"and"message_action"), extracting the fields needed for session routing and context:callbackId,userId,teamId,triggerId,actionTs.channelId,channelName,messageTs,messageText,messageUserId,responseUrl, and (optionally) sanitizedmessageBlocks.ctx.resolveSlackSystemEventSessionKey(...)helper. For message shortcuts, use{ channelId, channelType, senderId: userId }. For global shortcuts (which have no channel), fall back to a DM-scoped key:{ channelId: null, channelType: "im", senderId: userId }— or whatever the team prefers as the canonical "no channel context" path.enqueueSystemEventwith a new event text prefix (e.g.,"Slack shortcut: ") and the same{ sessionKey, contextKey }shape as the block_actions handler.contextKeywould be:["slack:shortcut", callbackId, channelId?, messageTs?].filter(Boolean).join(":").registerSlackShortcutHandler({ ctx, formatSystemEvent })thatregisterSlackInteractionEventscalls alongsideregisterSlackBlockActionHandlerininteractions.ts:180. Guard withif (typeof ctx.app.shortcut !== "function") return;same way the existing code guardsctx.app.view.openclaw:) the same way modal matchers usemodalMatcher = new RegExp(^${OPENCLAW_ACTION_PREFIX}). This lets OpenClaw avoid fighting with bolt apps that register their own shortcut handlers.The
InteractionSummarytype ininteractions.block-actions.ts:56already hasinteractionType: "block_action" | "view_submission" | "view_closed"— add"global_shortcut" | "message_shortcut"to the union (or introduce a siblingShortcutSummarytype if the field set diverges too much). ReusingformatSlackInteractionSystemEventfrominteractions.ts:126would give us consistent truncation/sanitization for free — the sanitizer already walks arbitrary payloads.No new SystemEventType variant is strictly required: the existing
enqueueSystemEvent(text, { sessionKey, contextKey })signature is string-typed and plugins just grep the event text. We already use theSlack interaction:prefix for block_actions; the newSlack shortcut:prefix is a natural parallel and subscribers can match on it.Reference: existing
enqueueSlackBlockActionEventshapeFrom
interactions.block-actions.ts:600–637, for parity:The shortcut handler's enqueue call would be structurally identical, differing only in the payload fields and the
contextKeyprefix.Use cases (why this matters)
slack_summarize_threadtool ready but has no clean ergonomic entry point.All three of these are dead in the water today: the Slack manifest allows registering the shortcuts, Slack delivers the events over the Socket Mode WebSocket, bolt's middleware layer receives them, but OpenClaw's Slack monitor never subscribes so they get silently discarded by bolt's unhandled-event path.
Estimated effort
Based on the line counts of the reference implementation:
interactions.block-actions.tsis 798 lines, but much of that is legacy Block Kit confirmation-message rewriting (buildSlackConfirmationBlocks,updateSlackLegacyBlockAction) and plugin-binding approval routing (handleSlackPluginBindingApproval,dispatchSlackPluginInteraction) that have no analog for shortcuts — shortcuts don't have an inline message that needs in-place updating, and we wouldn't wire the shortcut handler into the plugin-conversation binding system in a first pass.interactions.modal.tsis 262 lines and is the closer structural analog: parse → summarize → session-key → enqueue.A shortcut handler mirroring the modal lifecycle pattern (without the block-actions legacy surface) lands around 200–300 lines including types, payload parsing, session-key resolution, the enqueue call, and the
registerSlackShortcutHandlerexport. Plus ~100 lines of table-driven tests against a mock bolt app, consistent with the existinginteractions.test.ts.Total PR: ~300–400 lines of new code + tests, 4–6 hours of work including writing tests that match the project's existing style.
Offer to PR
I'm willing to submit the PR following the
interactions.block-actions.ts+interactions.modal.tspattern. I'd propose scoping the first PR to:extensions/slack/src/monitor/events/interactions.shortcuts.tswithregisterSlackShortcutHandler.InteractionSummaryunion (or a new sibling type).registerSlackInteractionEventsininteractions.ts.interactions.test.tscovering: global shortcut delivers a system event, message shortcut delivers a system event with channel/message context, callback_id prefix filtering (if that pattern is chosen), session-key resolution parity with block_actions.shortcut/message_actionalongsideblock_actionsas routed interaction types.Out of scope for the first PR (can be follow-ups):
dispatchSlackPluginInteractiveHandler).Happy to start on this as soon as the proposal has rough buy-in from a maintainer, so I don't re-invent naming or scope that someone's already thought through.
Environment
0512059dd4e6d2e582393828b837c376ddc4719c)SLACK_APP_TOKEN+SLACK_BOT_TOKEN)extensions/slack/package.jsonresolves