Message Shortcut Design Proposal — "Check against brand guide"

Date: 2026-04-22 Scope: Add a Slack message shortcut that lets any user right-click a message and get a brand-guide voice/terminology check in a modal.

Why

The Slack bot has four possible surfaces (slash commands · @mention · Home tab · message shortcuts). The first three are complete. This adds the fourth — and it's the one where the bot moves from "reference" to "active helper." Someone drafts a post in #marketing, right-clicks it, gets instant brand feedback without leaving Slack.

Rules-based v1 (no LLM needed, zero latency, zero cost). LLM-enhanced v2 when the dedicated Anthropic org lands.

Trigger flow

  1. User right-clicks any message in Slack → More actionsCheck against brand guide.
  2. Slack POSTs a message_action payload to /api/slack/interactivity with the message text + a trigger_id.
  3. Handler runs the rule engine against the text.
  4. Handler calls views.open with the trigger_id to display the analysis modal (<3s window).

No response posted in-channel. The modal is private to the triggering user.

v1 rule set (pure deterministic)

Rules are sourced from the tenant's brand guide content — nothing is hardcoded to CF.

Rule Source Check
Banned names messaging.namingConventions entries where usage contains "never" Substring match each banned example against the input text.
Contractions voice.writingRules rule named "Contractions" Detect non-contracted forms: \b(we|it|do|does|is|are|will|would|should|have|has)\s+(not|are|is|will|would)\b → suggest contracted form.
Numbers voice.writingRules rule named "Numbers" Detect standalone digits \b[1-9]\b (not 10+) → flag "spell out one through nine."
Corporate jargon voice.examples[].bad For each bad example, extract 3-5-word fingerprint phrases → fuzzy match (case-insensitive, whitespace-normalized) against input.

Rules absent from the content (e.g. a tenant with no namingConventions) are silently skipped — no false "pass" messages.

[header] 🔎 Brand Check
[context] Against California Forever brand guide

[divider]

[section] Original message (quote style)
> [truncated to ~280 chars, ellipsis if longer]

[divider]

(for each rule in the fixed order above)
  [section (pass)]  ✓ {Rule name} — clean
        OR
  [section (fail)]  ✗ {Rule name} — {count} issue{s}
  [context]         {first issue's detail + suggestion}
  (if >1 issue: [context] {second detail}, etc. capped at 3)

[divider]

[section]  Summary: {N of M rules passed} · {tight one-line takeaway}
[context] Full brand guide · <url|Voice & Tone →>

No action buttons on this view v1 — it's a one-shot analysis. If a user wants to iterate they edit the original message and re-trigger the shortcut.

Copy (voice of the bot)

Pass row: ✓ Naming — clean Fail row: ✗ Naming — 1 issue (not "Error" / "Warning" — those read cold) Fail detail: "Cal Forever" isn't an approved name. Use "California Forever" or "CF" in internal contexts only.

Summary examples: - All pass: All 4 rules passed. On-brand. - 3/4 pass: 3 of 4 rules passed. Review the naming flag. - 1/4 pass: 1 of 4 rules passed. Strong revision needed.

Written in the brand's own voice — direct, no corporate hedging, no AI-sounding "I noticed..." preamble.

Technical plan

Manifest update: - Add features.shortcuts entry: json { "name": "Check against brand guide", "type": "message", "callback_id": "check_brand", "description": "Get a brand-guide voice + terminology check on this message." } - Interactivity is already enabled (Phase 1). - No new OAuth scopes needed — message shortcuts piggyback on the app's existing bot install.

Code structure: - New lib/brand-check/rules.ts — pure functions: checkNaming(text, rules) → Issue[], same for contractions / numbers / jargon. No Slack knowledge, unit-testable. - lib/brand-check/engine.ts — composes the rules, accepts the content subset it needs, returns { ruleName, status: "pass" | "fail", issues: Issue[] }[]. - lib/slack/format.tsformatBrandCheckModal(result, opts) returns the view blocks. - app/api/slack/interactivity/route.ts — currently a stub; extend to parse message_action payloads with callback_id === "check_brand", fetch the needed content categories (messaging + voice) in parallel, run the engine, call views.open via the bot client.

Fetching & caching: Every trigger fetches messaging + voice from the brand guide API. Both are already cached 60s by the existing fetchBrandCategory revalidate. Acceptable for v1.

Content loading: Only pull the categories we actually check. Don't fetch all 9 categories just to run 4 rules.

What this doesn't cover (v2 scope)

Open questions

What happens next

  1. Approval on this doc.
  2. Manifest update: add message shortcut entry, push via chrome-devtools.
  3. Implement: lib/brand-check/*, modal formatter, interactivity handler extension.
  4. Smoke test: trigger on a test message in G&M Slack → verify modal renders + checks run.
  5. Iterate.
  6. Commit atomically, push, verify.
  7. Update docs/SLACK-UX.md with the Message Shortcut surface + rule engine reference.