skills/ccheney/robust-skills/slack-rich-output

slack-rich-output

SKILL.md

Slack Rich Output

Slack-native formatting for messages using mrkdwn syntax and Block Kit.

CRITICAL: Two Markup Systems

Slack has two completely different markup syntaxes. Using the wrong one is the most common formatting mistake.

System Used In Bold Link Heading
Slack mrkdwn text field, text objects (type: "mrkdwn"), section fields *bold* <url|text> Not supported
Standard Markdown markdown block only **bold** [text](url) # Heading

Standard Markdown syntax (**bold**, [text](url), # Heading) renders as literal text in mrkdwn contexts. Slack mrkdwn syntax (*bold*, <url|text>) renders as literal text in markdown blocks. Never mix them.

The markdown block is designed for AI app output — it renders standard Markdown natively in Slack. However, it only works in Messages (not Modals or Home tabs), has a 12,000 character cumulative limit per payload, and does not support syntax highlighting, horizontal rules, tables, or task lists. block_id is ignored.

Quick Decision Trees

"Should I use blocks?"

Response type?
├─ Conversational reply, short answer, <3 lines   → text only (no blocks)
├─ Multi-section summary, report, dashboard        → blocks
├─ Two-column key-value data                       → blocks (section fields)
├─ Tabular data                                    → blocks (table)
├─ Code with heading or surrounding context         → blocks
├─ Visual separation needed between topics          → blocks
└─ Feedback buttons or interactive elements         → blocks

"Which block type?"

What am I rendering?
├─ Large section title                → header (plain_text, 150 chars max)
├─ Body text or key-value pairs       → section (text + fields + accessory)
├─ Small metadata or secondary info   → context (images + text, 10 max)
├─ Horizontal separator               → divider
├─ Buttons, menus, date pickers       → actions (25 elements max)
├─ Standalone image                   → image (image_url or slack_file)
├─ Formatted text with lists, quotes  → rich_text (nested sub-elements)
├─ Tabular data                       → table (100 rows, 20 cols, 1 per msg)
├─ LLM-generated markdown content     → markdown (standard MD, messages only)
├─ Embedded video player              → video (requires links.embed:write)
├─ Remote file reference              → file (read-only, source: "remote")
├─ Feedback thumbs up/down            → context_actions (messages only)
└─ Collecting user input (modals)     → input (label + element)

"mrkdwn or markdown block?"

Content source?
├─ Short formatted text, labels, fields     → mrkdwn in section/context
├─ Long-form LLM-generated content          → markdown block (standard MD)
├─ Need tables inside blocks                → mrkdwn in section (manual layout)
├─ Need headings                            → markdown block or header blocks
└─ Mixed: structured layout + prose         → section/header blocks + markdown block

Slack mrkdwn Syntax

Format Syntax Notes
Bold *bold* Not **bold**
Italic _italic_ Not *italic*
Strikethrough ~strikethrough~ Not ~~strikethrough~~
Inline code `code` Same as standard Markdown
Code block ```code``` No syntax highlighting
Blockquote > quoted text Prefix each line
Link <https://example.com|display text> Not [text](url)
User mention <@U0123ABC> Triggers notification
Channel mention <#C0123ABC> Auto-converts to name
User group <!subteam^SAZ94GDB8> Notifies all members
@here <!here> Active channel members
@channel <!channel> All channel members
@everyone <!everyone> All non-guest workspace members
Emoji :emoji_name: Standard or custom
Newline \n Literal newline in string
Ordered list 1. item Plain text, no special rendering
Bullet list • item Rendered in rich_text blocks only

Date Formatting

Displays localized dates in the reader's timezone:

<!date^{unix_timestamp}^{token_string}^{optional_link}|{fallback_text}>

Tokens: {date_num} (2014-02-18), {date} (February 18th, 2014), {date_short} (Feb 18, 2014), {date_long} (Tuesday, February 18th, 2014), {time} (6:39 AM), {time_secs} (6:39:42 AM), {ago} (3 minutes ago). Add _pretty suffix for relative terms ("yesterday", "today").

Escaping

Only three characters need escaping: &&amp;, <&lt;, >&gt;. Do not HTML-encode other characters.

Text Object Verbatim

Set verbatim: true on mrkdwn text objects to disable auto-link conversion and mention parsing. Useful when displaying raw URLs or text that looks like mentions but isn't.

Block Types Reference

header

Large bold text for section titles. plain_text only. Max 150 chars.

{ "type": "header", "text": { "type": "plain_text", "text": "Section Title", "emoji": true } }

section

Primary content block. Supports text, two-column fields, and one accessory element.

{
  "type": "section",
  "text": { "type": "mrkdwn", "text": "*Project Status*\nAll systems operational." }
}

Two-column fields layout:

{
  "type": "section",
  "fields": [
    { "type": "mrkdwn", "text": "*Status:*\nActive" },
    { "type": "mrkdwn", "text": "*Owner:*\nChris" },
    { "type": "mrkdwn", "text": "*Priority:*\nHigh" },
    { "type": "mrkdwn", "text": "*Due:*\nFriday" }
  ]
}

Either text or fields required (or both). Text max 3000 chars. Fields max 10 items, each max 2000 chars. Set expand: true to force full text display without "see more" truncation.

Compatible accessories: button, overflow, datepicker, timepicker, select menus, multi-select menus, checkboxes, radio buttons, image.

divider

Horizontal rule between blocks.

{ "type": "divider" }

context

Small, muted text for metadata or secondary info. Elements: mrkdwn text objects or image elements. Max 10 elements.

{
  "type": "context",
  "elements": [
    { "type": "mrkdwn", "text": "Last updated: Feb 9, 2026" },
    { "type": "mrkdwn", "text": "Source: deploy-bot" }
  ]
}

actions

Interactive elements: buttons, select menus, overflow menus, date pickers. Max 25 elements.

{
  "type": "actions",
  "elements": [
    {
      "type": "button",
      "text": { "type": "plain_text", "text": "Approve", "emoji": true },
      "style": "primary",
      "action_id": "approve_action",
      "value": "approved"
    }
  ]
}

Button styles: primary (green), danger (red), or omit for default. Use primary sparingly — one per set. Action IDs must be unique within the message.

image

Standalone image with alt text. Provide either image_url (public, max 3000 chars) or slack_file object. Formats: png, jpg, jpeg, gif.

{
  "type": "image",
  "image_url": "https://example.com/chart.png",
  "alt_text": "Deployment success rate chart",
  "title": { "type": "plain_text", "text": "Deploy Metrics" }
}

rich_text

Advanced formatted text with nested elements. Supports styled text, lists, code blocks, and quotes.

{
  "type": "rich_text",
  "elements": [
    {
      "type": "rich_text_section",
      "elements": [
        { "type": "text", "text": "Key findings:", "style": { "bold": true } },
        { "type": "text", "text": "\n" }
      ]
    },
    {
      "type": "rich_text_list",
      "style": "bullet",
      "elements": [
        {
          "type": "rich_text_section",
          "elements": [{ "type": "text", "text": "Latency reduced by 40%" }]
        },
        {
          "type": "rich_text_section",
          "elements": [{ "type": "text", "text": "Error rate under 0.1%" }]
        }
      ]
    }
  ]
}

Sub-element types:

  • rich_text_section — paragraph of inline elements
  • rich_text_liststyle: "bullet" or "ordered", optional indent (pixels of indentation), offset (start number for ordered), border (px)
  • rich_text_preformatted — code block (monospace, grey background)
  • rich_text_quote — indented quote block

Inline element types within sections: text (with optional style: { bold, italic, strike, code, underline }), link (url, optional text, optional style), emoji (name), user (user_id), channel (channel_id), usergroup (usergroup_id), broadcast (range: here/channel/everyone), date (timestamp, format, fallback), color (value: hex).

table

Tabular data. One table per message (appended as attachment at bottom). The first row acts as the header.

{
  "type": "table",
  "rows": [
    [
      { "type": "raw_text", "text": "Service" },
      { "type": "raw_text", "text": "Status" },
      { "type": "raw_text", "text": "Latency" }
    ],
    [
      { "type": "raw_text", "text": "API" },
      { "type": "raw_text", "text": "Healthy" },
      { "type": "raw_text", "text": "12ms" }
    ],
    [
      { "type": "raw_text", "text": "Worker" },
      { "type": "raw_text", "text": "Degraded" },
      { "type": "raw_text", "text": "340ms" }
    ]
  ],
  "column_settings": [
    { "align": "left" },
    { "align": "center" },
    { "align": "right" }
  ]
}

Each row is an array of cell objects (NOT an object with a cells property). Cell types: raw_text (plain text) or rich_text (with elements array for links, mentions, emoji, bold). There is no columns property — the first row is the header. Max 100 rows, 20 columns per row. column_settings controls alignment (left/center/right) and is_wrapped (boolean). Sending multiple tables triggers invalid_attachments error.

markdown

Standard Markdown rendering for AI app output. Messages only.

{ "type": "markdown", "text": "**Bold**, *italic*, [link](https://example.com)\n\n## Heading\n\n- List item" }

Supports: bold, italic, strikethrough, links, headers (h1+), ordered/unordered lists, inline code, code blocks, block quotes, images (as hyperlinks). Does NOT support: syntax highlighting, horizontal rules, tables, task lists. Cumulative 12,000 char limit per payload. block_id is ignored. Escape special characters with backslash (\*, \[, \#, etc.) to render them literally.

context_actions

Feedback and icon buttons for message-level actions. Messages only. Max 5 elements.

{
  "type": "context_actions",
  "elements": [
    {
      "type": "feedback_buttons",
      "action_id": "feedback_123",
      "positive_button": {
        "text": { "type": "plain_text", "text": "Good" },
        "value": "positive"
      },
      "negative_button": {
        "text": { "type": "plain_text", "text": "Bad" },
        "value": "negative"
      }
    }
  ]
}

Compatible elements: feedback_buttons (positive + negative), icon_button (only trash icon available, supports visible_to_user_ids array).

video

Embedded video player. Requires links.embed:write scope, publicly accessible URL in app's unfurl domains.

{
  "type": "video",
  "alt_text": "Product demo",
  "title": { "type": "plain_text", "text": "Q4 Demo" },
  "video_url": "https://example.com/embed/video",
  "thumbnail_url": "https://example.com/thumb.png",
  "title_url": "https://example.com/video",
  "description": { "type": "plain_text", "text": "Quarterly product walkthrough" },
  "author_name": "Product Team"
}

input

Collects user data in modals, messages, and Home tabs. Requires label (plain_text, 2000 chars) and one compatible element.

{
  "type": "input",
  "label": { "type": "plain_text", "text": "Description" },
  "element": {
    "type": "plain_text_input",
    "action_id": "description_input",
    "multiline": true
  },
  "optional": true,
  "hint": { "type": "plain_text", "text": "Brief summary of the issue" }
}

Compatible elements: plain_text_input, number_input, email_text_input, url_text_input, rich_text_input, select menus, multi-select menus, datepicker, datetimepicker, timepicker, checkboxes, radio_buttons, file_input.

file

Remote file reference. Read-only — appears when retrieving messages with remote files. Cannot be directly added to messages.

{ "type": "file", "external_id": "ABCD1", "source": "remote" }

Composition Objects

Text Object

{ "type": "mrkdwn", "text": "*bold* and _italic_", "verbatim": false }
{ "type": "plain_text", "text": "No formatting", "emoji": true }

mrkdwn supports Slack mrkdwn syntax. plain_text renders literally. emoji: true converts :emoji: to rendered emoji (plain_text only). verbatim: true disables auto-linking (mrkdwn only). Min 1 char, max 3000 chars. Header blocks require plain_text. Section text/fields accept either.

Option Object

Used in select menus, overflow, checkboxes, radio buttons:

{
  "text": { "type": "plain_text", "text": "Option 1" },
  "value": "opt_1",
  "description": { "type": "plain_text", "text": "Detailed description" }
}

Option text max 75 chars. value max 150 chars. description optional, max 75 chars. Text type: plain_text for select/overflow menus; mrkdwn also allowed for checkboxes and radio buttons.

Option Group Object

Groups options in select menus. Max 100 groups, each with label + options array:

{
  "label": { "type": "plain_text", "text": "Group Name" },
  "options": [{ "text": { "type": "plain_text", "text": "Item" }, "value": "item_1" }]
}

Confirmation Dialog

Adds confirmation step to interactive elements:

{
  "title": { "type": "plain_text", "text": "Are you sure?" },
  "text": { "type": "plain_text", "text": "This action cannot be undone." },
  "confirm": { "type": "plain_text", "text": "Yes, do it" },
  "deny": { "type": "plain_text", "text": "Cancel" },
  "style": "danger"
}

Title max 100 chars. Text max 300 chars. Button labels max 30 chars. Style: primary (green) or danger (red).

Dispatch Action Configuration

Controls when plain_text_input or rich_text_input triggers block_actions:

{ "trigger_actions_on": ["on_enter_pressed"] }

Values: on_enter_pressed, on_character_entered. Requires dispatch_action: true on the input block.

Conversation Filter

Filters conversation select menus:

{
  "include": ["public", "mpim"],
  "exclude_external_shared_channels": true,
  "exclude_bot_users": true
}

At least one field required. include values: im, mpim, private, public.

Limits

Constraint Limit
Blocks per message 50
Blocks per modal/Home tab 100
Section text 3000 chars
Section fields 10 items, 2000 chars each
Header text 150 chars
Context elements 10
Actions elements 25
Context actions elements 5
Table rows 100
Table columns 20
Tables per message 1
Markdown block text 12,000 chars cumulative per payload
Modal title 24 chars
Modal submit/close text 24 chars
Modal views in stack 3
Modal private_metadata 3000 chars
Button text 75 chars (displays ~30)
Button value 2000 chars
action_id 255 chars
block_id 255 chars
Overflow options 5
Select options 100
Option text 75 chars
Placeholder text 150 chars
Confirmation title 100 chars
Confirmation text 300 chars
Image alt_text 2000 chars
Image URL 3000 chars
Video title 200 chars
Video description 200 chars
Video author_name 50 chars

Anti-Patterns

Anti-Pattern Problem Fix
**bold** in mrkdwn Renders literally Use *bold*
[text](url) in mrkdwn Renders literally Use <url|text>
# Heading in mrkdwn Renders as plain text Use header block or markdown block
*bold* in markdown block Renders as italic Use **bold**
Blocks without text fallback Empty notifications, no accessibility fallback Always provide text in chat.postMessage
text and blocks diverge Confusing: notification says one thing, chat shows another Keep semantically aligned
Blocks for simple replies Visual noise for short responses Use text only for simple replies
2+ tables in one message invalid_attachments error One table per message
mrkdwn in header text Ignored — headers only accept plain_text Use plain_text type
Long header text Silently truncated at 150 chars Keep under 150
Missing alt_text on images Accessibility failure, API may reject Always include alt_text
link_names: 1 for mentions Fragile — names change, IDs don't Use <@USERID> directly

Best Practices

Use blocks when:

  • The response has multiple distinct sections (summaries, reports, dashboards)
  • Two-column key-value layouts improve readability (metadata, config summaries)
  • A table presents data more clearly than prose
  • Visual separation between topics helps comprehension
  • Code needs a header or surrounding context
  • Interactive elements (buttons, menus, feedback) are needed

Don't use blocks when:

  • The response is conversational ("sure, done", "hey, good morning")
  • The response is under ~3 lines of text
  • The content is a simple answer to a direct question

Always:

  • Provide a complete text field as the accessible fallback (notifications, threads, search, screen readers)
  • Keep the text and blocks semantically aligned
  • Use mrkdwn syntax in text objects, not standard Markdown (except in markdown blocks)
  • Escape &, <, > in user-generated content
  • Use <@USERID> for mentions, not @name — IDs are stable, names change

Surfaces Overview

Block Kit renders across multiple Slack surfaces:

Surface Max Blocks Key Methods Notes
Messages 50 chat.postMessage, chat.update Primary output surface
Modals 100 views.open, views.update, views.push Requires trigger_id (3s expiry), up to 3 stacked views
App Home 100 views.publish Private per-user view, Home/Messages/About tabs
Canvases N/A canvases.create, canvases.edit Markdown only — no Block Kit support
Lists N/A lists.* API methods Task tracking and project management
Split View N/A Agents & AI Apps config AI chat surface with Chat + History tabs

Modals collect input via input blocks, return view_submission payloads. They chain up to 3 views with push/update/clear response actions. private_metadata (3000 chars) persists context between views.

Work Objects

Work Objects render rich entity previews when links are shared in Slack. They extend link unfurling with structured data, flexpane details, editable fields, and actions.

Entity Types

Type Entity ID Purpose
File slack#/entities/file Documents, spreadsheets, images
Task slack#/entities/task Tickets, to-dos, work items
Incident slack#/entities/incident Service interruptions, outages
Content Item slack#/entities/content_item Articles, pages, wiki entries
Item slack#/entities/item General-purpose entity

Implementation

Work Objects use chat.unfurl with a metadata parameter (URL-encoded) containing entity type, external reference, and entity payload (attributes, fields, custom_fields, display_order). The flexpane responds to entity_details_requested events via entity.presentDetails.

Capabilities

  • Editable fields: text, number, date, datetime, email, boolean, select, multi-select with validation
  • Actions: Up to 2 primary buttons + 5 overflow menu items per entity
  • Authentication: user_auth_required + user_auth_url for third-party auth flows
  • Full-size preview: PDFs and images via preview_url (requires CORS header)
  • Related conversations: Automatic aggregation of conversations mentioning the entity
  • Direct posting: chat.postMessage also accepts metadata for posting Work Objects without link unfurling

Work Objects cannot be rendered in standard blocks output — they require chat.unfurl metadata or chat.postMessage with metadata parameter. They are not part of the message composition flow.

Data Types

string, integer, boolean, array, slack#/types/user, slack#/types/channel_id, slack#/types/timestamp, slack#/types/date, slack#/types/image, slack#/types/link, slack#/types/email, slack#/types/entity_ref.

Reference Documentation

File Purpose
references/CHEATSHEET.md Quick reference: all blocks, elements, limits, mrkdwn at a glance
references/BLOCKS.md All 13 block types with full property tables and constraints
references/ELEMENTS.md All 19 interactive elements with properties and constraints
references/COMPOSITION.md Composition objects: text, option, confirmation, filters
references/RICH-TEXT.md Rich text block deep dive: sub-elements, inline types, styles
references/FORMATTING.md mrkdwn syntax, date formatting, mentions, escaping, auto-parsing
references/SURFACES.md Modals, App Home, canvases, lists, split view
references/WORK-OBJECTS.md Entity types, chat.unfurl, flexpane, editable fields, actions

Sources

Weekly Installs
2
GitHub Stars
22
First Seen
Feb 15, 2026
Installed on
cursor2
github-copilot2
claude-code2
gemini-cli2
replit1
windsurf1