slack-rich-output
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: & → &, < → <, > → >. 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 elementsrich_text_list—style: "bullet"or"ordered", optionalindent(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
textfield as the accessible fallback (notifications, threads, search, screen readers) - Keep the
textandblockssemantically aligned - Use mrkdwn syntax in text objects, not standard Markdown (except in
markdownblocks) - 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_urlfor 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.postMessagealso acceptsmetadatafor 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
- Block Kit Reference — Slack
- Block Kit Blocks — Slack
- Block Kit Elements — Slack
- Block Kit Composition Objects — Slack
- Formatting Message Text — Slack
- Messaging Overview — Slack
- Work Objects — Slack
- Surfaces — Slack
- Modals — Slack
- App Home — Slack
- Canvases — Slack
- Lists — Slack
- Split View — Slack
- App Design — Slack