Skip to main content

Mattermost Blocks

Mattermost Blocks are the 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.

Feature flag

Mattermost Blocks are controlled by the MmBlocksEnabled feature flag (enabled by default). When disabled, Mattermost Blocks payloads are not rendered and Mattermost Blocks action cookies are rejected.

How it works

An interactive Mattermost Blocks post has two parts:

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

Example post payload

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.

Submit using an incoming webhook

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.

Submit from a plugin

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)

Block types

Each element of props.mm_blocks is an object with a required type field. The supported block types are:

TypePurpose
textMarkdown-formatted text
imageRemote image with optional sizing and alignment
dividerHorizontal rule between blocks
buttonInteractive button
static_selectDropdown menu with static options or dynamic data sources
containerGroups blocks with optional border, accent bar, background, and layout flow
collapsibleExpandable section with separate header and content block arrays
column_setHorizontal row of columns
columnColumn 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.

Text

{
"type": "text",
"text": "Hello **from** Mattermost Blocks.",
"is_subtle": false,
"size": "default"
}
FieldRequiredDescription
textyesMarkdown-formatted content. Supports @mentions. All text and button text fields in a post share a combined limit of 16,000 characters.
is_subtlenoWhen true, renders in a muted color. Does not change font size.
sizenoTypography scale: small or default. Omitted is equivalent to default.

Image

{
"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"
}
FieldRequiredDescription
urlyesImage URL (GIF, JPEG, PNG, BMP, or SVG).
alt_textnoAccessible description.
titlenoPlain-text tooltip shown on hover.
sizenoPreset sizing: auto, xsmall, small, medium, large, or stretch. Defaults to stretch.
max_widthnoMaximum width in pixels.
max_heightnoMaximum height in pixels.
image_stylenodefault or person (avatar-style crop).
horizontal_alignmentnoleft, center, or right.

Divider

{"type": "divider"}

Button

{
"type": "button",
"text": "Approve",
"action_id": "approve",
"style": "primary",
"tooltip": "Approve this change",
"disabled": false,
"query": {"ticket": "ISS-101"}
}
FieldRequiredDescription
textyesButton label. Supports Markdown. Counts toward the combined 16,000-character limit for text and button blocks.
action_idyesMust match a key in mm_blocks_actions.
stylenoSemantic color: default, primary, danger, good, success, or warning. Hex colors such as #2d81ff are also accepted.
tooltipnoHelp text shown on hover.
disablednoWhen true, the button renders but cannot be clicked.
querynoStatic 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.

Static select

{
"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"
}
FieldRequiredDescription
action_idyesMust match a key in mm_blocks_actions.
placeholderyesPlaceholder text for the menu.
optionsdepends onArray of {text, value} pairs. Required unless data_source is set.
initial_optionnoPre-selected option value.
disablednoWhen true, the menu renders but cannot be used.
data_sourcenoDynamic 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).

Container

{
"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"
}
FieldRequiredDescription
contentyesArray of nested blocks.
bordernoWhen true, draws a border around the container.
accent_colornoLeft accent bar color. Semantic values: default, primary, good, warning, or danger. CSS colors such as #439FE0 are also accepted.
backgroundnonone (default) or gray.
flownoChild layout direction: horizontal or vertical. Defaults to vertical.
gapnoSpacing between children: none, small, medium, large, or xlarge. Defaults to none.
max_heightnoMaximum 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.

Collapsible

{
"type": "collapsible",
"collapsed": false,
"header": [
{"type": "text", "text": "**Details**"}
],
"content": [
{"type": "text", "text": "Expanded content goes here."}
]
}
FieldRequiredDescription
headeryesBlocks shown in the always-visible header row.
contentyesBlocks shown when expanded.
collapsednoInitial collapsed state. Defaults to false.

Column set and column

{
"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.

The 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": { ... }
}
}
}
FieldRequiredDescription
typeyesAction type. See Action types below.
urldepends on typeTarget URL. Required for external and openURL.
contextnoServer-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.
querynoStatic 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 — Mattermost 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.

Action types

TypeBehavior
externalThe 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.
openURLNavigates 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.

Action dispatch flow

  1. Integration creates a post with mm_blocks controls and matching mm_blocks_actions entries.
  2. Mattermost server validates the pairing, encrypts the action registry, and stores the post.
  3. Client renders the blocks. When the user clicks a button or selects a menu option, the client sends POST /api/v4/posts/{post_id}/actions/{action_id} with the encrypted cookie, optional query, selected_option (for menus), and integration_format: "mm_block".
  4. Mattermost server decrypts the cookie, resolves the action, merges query parameters, and either navigates (openURL) or POSTs to the integration endpoint (external).
  5. Integration responds with a standard post-action response.

Action IDs in the URL path must match [A-Za-z0-9_-]+.

Receiving action callbacks

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 fieldDescription
updateReplaces the original post message and props. Use update.props.mm_blocks to refresh the block layout.
ephemeral_textSends a private message visible only to the user who clicked.
goto_locationNavigates the user to a URL after the action completes. Supports in-app paths and external URLs.
errorReturns a custom error message displayed below the interactive content.
skip_slack_parsingSet to true to bypass Slack-compatibility parsing of ephemeral_text.

See interactive messages for error handling details and update.props semantics.

Legacy format compatibility

Mattermost continues to accept these older payload formats:

PropFormatNotes
attachmentsLegacy message attachmentsAttachment actions arrays are translated into Mattermost Blocks buttons and selects at render time.
blocksSlack Block KitTranslated into Mattermost Blocks. Interactive Block Kit elements require matching mm_blocks_actions entries keyed by action_id.
cardsMicrosoft Adaptive CardsTranslated into Mattermost 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.

Security considerations

Mattermost Blocks follow the same security model as legacy interactive messages:

  • Integration url values are invoked server-to-server, never directly from the client.
  • context values are server-only and are not exposed to rendering clients.
  • After create, the plaintext mm_blocks_actions map is replaced with an encrypted cookie.
  • Action IDs are validated and must match referenced interactive content exactly.
  • openURL actions reject plugin paths and path-traversal segments.

See also