Skip to content

Expose Slack message_action and global shortcut events via channels.slack #63920

Description

@chrisbaker2000

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:

  1. 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).
  2. 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.
  3. 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.
  4. 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(":").
  5. 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.
  6. 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)

  1. "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.
  2. "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.
  3. 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

Metadata

Metadata

Assignees

Labels

P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.no-staleExclude from stale automation

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions