MM Blocks are Mattermost’s structured post format for integration messages. An integration sends a block tree in props.mm_blocks to define layout, text, images, buttons, and menus, and registers action handlers in props.mm_blocks_actions so the server can dispatch clicks and menu selections back to the integration.
An interactive MM Blocks post has two parts:
props.mm_blocks — an array of block objects that define layout, text, images, buttons, and menus. A post may contain up to 100 blocks in total, counting nested blocks throughout the tree.props.mm_blocks_actions — a map keyed by action ID. Each entry tells the server what to do when a user clicks a button, selects a menu option, or activates a markdown action button.When the post is stored, the server validates that every referenced action ID has a matching registry entry (and no unused entries remain). It then encrypts the action registry into an opaque cookie string that clients send back when dispatching actions.
The following payload posts a message with text, a primary button, and a static select menu:
{
"channel_id": "qmd5oqtwoibz8cuzxzg5ekshgr",
"message": "Deployment #42 finished.",
"props": {
"mm_blocks": [
{
"type": "text",
"text": "Deployed `main` to **staging**. Choose a follow-up action:"
},
{
"type": "container",
"flow": "horizontal",
"gap": "small",
"content": [
{
"type": "button",
"text": "View logs",
"style": "primary",
"action_id": "view_logs"
},
{
"type": "button",
"text": "Rollback",
"style": "danger",
"action_id": "rollback"
},
{
"type": "static_select",
"action_id": "next_step",
"placeholder": "Select next step…",
"options": [
{"text": "Promote to production", "value": "promote"},
{"text": "Run smoke tests", "value": "smoke"}
]
}
]
}
],
"mm_blocks_actions": {
"view_logs": {
"type": "external",
"url": "https://integration.example.com/actions/view-logs",
"context": {"deployment_id": "42"}
},
"rollback": {
"type": "external",
"url": "https://integration.example.com/actions/rollback",
"context": {"deployment_id": "42"}
},
"next_step": {
"type": "external",
"url": "https://integration.example.com/actions/next-step",
"context": {"deployment_id": "42"}
}
}
}
}
You can send this payload using the create post REST API, an incoming webhook, a custom slash command, or from a plugin.
curl -X POST $MM_URL/hooks/$WEBHOOK_ID \
-H "Content-Type: application/json" \
-d '{
"text": "Deployment #42 finished.",
"props": {
"mm_blocks": [
{"type": "text", "text": "Deployed `main` to **staging**."},
{
"type": "button",
"text": "View logs",
"style": "primary",
"action_id": "view_logs"
}
],
"mm_blocks_actions": {
"view_logs": {
"type": "external",
"url": "https://integration.example.com/actions/view-logs",
"context": {"deployment_id": "42"}
}
}
}
}'
For webhook payloads, place mm_blocks and mm_blocks_actions inside props. The top-level attachments array remains available for legacy integrations.
post := &model.Post{
ChannelId: channelID,
UserId: p.botID,
Message: "Deployment #42 finished.",
Props: model.StringInterface{
"mm_blocks": []any{
map[string]any{
"type": "text",
"text": "Deployed `main` to **staging**.",
},
map[string]any{
"type": "button",
"text": "View logs",
"style": "primary",
"action_id": "view_logs",
},
},
"mm_blocks_actions": map[string]any{
"view_logs": map[string]any{
"type": "external",
"url": fmt.Sprintf("/plugins/%s/actions/view-logs", manifest.Id),
"context": map[string]any{"deployment_id": "42"},
},
},
},
}
_, err := p.API.CreatePost(post)
Each element of props.mm_blocks is an object with a required type field. The supported block types are:
| Type | Purpose |
|---|---|
text |
Markdown-formatted text |
image |
Remote image with optional sizing and alignment |
divider |
Horizontal rule between blocks |
button |
Interactive button |
static_select |
Dropdown menu with static options or dynamic data sources |
container |
Groups blocks with optional border, accent bar, background, and layout flow |
collapsible |
Expandable section with separate header and content block arrays |
column_set |
Horizontal row of columns |
column |
Column inside a column_set (not valid as a top-level block) |
Nested layout blocks (container, collapsible, column_set, and column) may nest up to 32 levels deep.
Malformed blocks are omitted at render time; valid sibling blocks still display.
{
"type": "text",
"text": "Hello **from** MM Blocks.",
"is_subtle": false,
"size": "default"
}
| Field | Required | Description |
|---|---|---|
text |
yes | Markdown-formatted content. Supports @mentions. All text and button text fields in a post share a combined limit of 16,000 characters. |
is_subtle |
no | When true, renders in a muted color. Does not change font size. |
size |
no | Typography scale: small or default. Omitted is equivalent to default. |
{
"type": "image",
"url": "https://example.com/logo.png",
"alt_text": "Company logo",
"title": "Logo",
"size": "medium",
"max_width": 400,
"max_height": 300,
"image_style": "default",
"horizontal_alignment": "center"
}
| Field | Required | Description |
|---|---|---|
url |
yes | Image URL (GIF, JPEG, PNG, BMP, or SVG). |
alt_text |
no | Accessible description. |
title |
no | Plain-text tooltip shown on hover. |
size |
no | Preset sizing: auto, xsmall, small, medium, large, or stretch. Defaults to stretch. |
max_width |
no | Maximum width in pixels. |
max_height |
no | Maximum height in pixels. |
image_style |
no | default or person (avatar-style crop). |
horizontal_alignment |
no | left, center, or right. |
{"type": "divider"}
{
"type": "button",
"text": "Approve",
"action_id": "approve",
"style": "primary",
"tooltip": "Approve this change",
"disabled": false,
"query": {"ticket": "ISS-101"}
}
| Field | Required | Description |
|---|---|---|
text |
yes | Button label. Supports Markdown. Counts toward the combined 16,000-character limit for text and button blocks. |
action_id |
yes | Must match a key in mm_blocks_actions. |
style |
no | Semantic color: default, primary, danger, good, success, or warning. Hex colors such as #2d81ff are also accepted. |
tooltip |
no | Help text shown on hover. |
disabled |
no | When true, the button renders but cannot be clicked. |
query |
no | Static query parameters merged into the action URL when clicked. Up to 50 entries; each key may be up to 128 characters and each value up to 2048 characters. |
{
"type": "static_select",
"action_id": "pick_region",
"placeholder": "Pick a region",
"options": [
{"text": "North", "value": "north"},
{"text": "South", "value": "south"}
],
"initial_option": "north",
"disabled": false,
"data_source": "channels"
}
| Field | Required | Description |
|---|---|---|
action_id |
yes | Must match a key in mm_blocks_actions. |
placeholder |
yes | Placeholder text for the menu. |
options |
depends on | Array of {text, value} pairs. Required unless data_source is set. |
initial_option |
no | Pre-selected option value. |
disabled |
no | When true, the menu renders but cannot be used. |
data_source |
no | Dynamic option source: channels or users. When set, options is optional. Users can only select public channels in their teams. |
When a user selects an option, the integration callback receives selected_option in the request context with the chosen value (or user/channel ID for dynamic data sources).
{
"type": "container",
"content": [
{"type": "text", "text": "Container title"},
{"type": "divider"},
{"type": "text", "text": "Body copy", "is_subtle": true, "size": "small"}
],
"border": true,
"accent_color": "primary",
"background": "gray",
"flow": "vertical",
"gap": "small",
"max_height": "medium"
}
| Field | Required | Description |
|---|---|---|
content |
yes | Array of nested blocks. |
border |
no | When true, draws a border around the container. |
accent_color |
no | Left accent bar color. Semantic values: default, primary, good, warning, or danger. CSS colors such as #439FE0 are also accepted. |
background |
no | none (default) or gray. |
flow |
no | Child layout direction: horizontal or vertical. Defaults to vertical. |
gap |
no | Spacing between children: none, small, medium, large, or xlarge. Defaults to none. |
max_height |
no | Maximum height preset: none, small, medium, or large. Overflowing content scrolls inside the container. On mobile, users can open scrollable content in a dedicated full-screen view. |
{
"type": "collapsible",
"collapsed": false,
"header": [
{"type": "text", "text": "**Details**"}
],
"content": [
{"type": "text", "text": "Expanded content goes here."}
]
}
| Field | Required | Description |
|---|---|---|
header |
yes | Blocks shown in the always-visible header row. |
content |
yes | Blocks shown when expanded. |
collapsed |
no | Initial collapsed state. Defaults to false. |
{
"type": "column_set",
"gap": "medium",
"columns": [
{
"type": "column",
"width": "stretch",
"gap": "small",
"items": [
{"type": "text", "text": "Left column"}
]
},
{
"type": "column",
"width": "auto",
"items": [
{"type": "text", "text": "Right column"}
]
}
]
}
column blocks are only valid inside a column_set. Each column has an items array of nested blocks.
mm_blocks_actions registry The mm_blocks_actions post prop is a map keyed by action ID. Each entry describes how the server handles clicks on that action. The registry supports up to 50 action entries. Each action ID must match [A-Za-z0-9_-]+, may be up to 64 characters long, and is matched case-sensitively.
{
"mm_blocks_actions": {
"<action_id>": {
"type": "<action_type>",
"url": "...",
"context": { ... },
"query": { ... }
}
}
}
| Field | Required | Description |
|---|---|---|
type |
yes | Action type. See Action types below. |
url |
depends on type | Target URL. Required for external and openURL. |
context |
no | Server-side context forwarded to the integration in the post-action request body. Not visible to clients. Up to 50 entries; each key may be up to 128 characters. |
query |
no | Static string → string map merged into the target URL’s query string. Combined with any per-control query on the block — block values win on key conflict. Up to 50 entries; each key may be up to 128 characters and each value up to 2048 characters. |
Every action ID referenced by interactive content — MM Blocks controls, markdown mmaction:// links, Block Kit actions, or Adaptive Card actions — must have a matching registry entry. Unused registry entries are rejected at post-create time.
After the post is stored, clients receive an encrypted cookie string in place of the plaintext registry map.
| Type | Behavior |
|---|---|
external |
The server sends an HTTP POST request to url with the standard post-action request body. The integration responds with a post-action response (update, ephemeral message, or navigation). Relative plugin paths such as /plugins/myplugin/action are supported. |
openURL |
Navigates the user without calling an integration. Relative paths (for example /myteam/channels/off-topic) navigate inside Mattermost. http:// and https:// URLs open in a new browser tab. Plugin paths are not allowed. |
Additional action types may be introduced in future releases. Entries with an unknown type value are rejected at post-create time.
mm_blocks controls and matching mm_blocks_actions entries.POST /api/v4/posts/{post_id}/actions/{action_id} with the encrypted cookie, optional query, selected_option (for menus), and integration_format: "mm_block".openURL) or POSTs to the integration endpoint (external).Action IDs in the URL path must match [A-Za-z0-9_-]+.
When a user activates an external action, the Mattermost server sends an HTTP POST request to the configured url. The request body uses the same PostActionIntegrationRequest shape as legacy message attachment buttons:
{
"user_id": "rd49ehbqyjytddasoownkuqrxe",
"user_name": "alice",
"channel_id": "j6j53p28k6urx15fpcgsr20psq",
"channel_name": "town-square",
"team_id": "5xxzt146eax4tul69409opqjlf",
"team_domain": "myteam",
"post_id": "gqrnh3675jfxzftnjyjfe4udeh",
"trigger_id": "...",
"type": "button",
"context": {
"deployment_id": "42",
"selected_option": "promote"
}
}
For static select menus, the selected value is added to context.selected_option.
Integrations respond with the same post-action response format used by attachment actions:
{
"update": {
"message": "Updated!",
"props": {
"mm_blocks": [
{"type": "text", "text": "Deployment promoted to production."}
]
}
},
"ephemeral_text": "Promotion started.",
"goto_location": "/myteam/channels/releases"
}
| Response field | Description |
|---|---|
update |
Replaces the original post message and props. Use update.props.mm_blocks to refresh the block layout. |
ephemeral_text |
Sends a private message visible only to the user who clicked. |
goto_location |
Navigates the user to a URL after the action completes. Supports in-app paths and external URLs. |
error |
Returns a custom error message displayed below the interactive content. |
skip_slack_parsing |
Set to true to bypass Slack-compatibility parsing of ephemeral_text. |
See interactive messages for error handling details and update.props semantics.
Mattermost continues to accept these older payload formats:
| Prop | Format | Notes |
|---|---|---|
attachments |
Legacy message attachments | Attachment actions arrays are translated into MM Blocks buttons and selects at render time. |
blocks |
Slack Block Kit | Translated into MM Blocks. Interactive Block Kit elements require matching mm_blocks_actions entries keyed by action_id. |
cards |
Microsoft Adaptive Cards | Translated into MM Blocks. Interactive card actions require matching mm_blocks_actions entries keyed by action id. |
New integrations should prefer native mm_blocks for full control over layout and action registration.
MM Blocks follow the same security model as legacy interactive messages:
url values are invoked server-to-server, never directly from the client.context values are server-only and are not exposed to rendering clients.mm_blocks_actions map is replaced with an encrypted cookie.openURL actions reject plugin paths and path-traversal segments.mmaction:// links using the same action registry.