Edit on GitHub

Use MM Blocks

MM Blocks are the recommended way to build interactive messages. Instead of placing buttons and menus inside legacy message attachments, integrations send a block tree in props.mm_blocks and register action handlers in props.mm_blocks_actions.

Legacy attachment arrays (top-level attachments on webhooks and slash-command responses, or props.attachments on REST API posts), Slack Block Kit (props.blocks), and Microsoft Adaptive Cards (props.cards) are still accepted. Mattermost translates them into MM Blocks at render time, but new integrations should use native MM Blocks directly.

How it works 

An interactive MM Blocks post has two parts:

  1. props.mm_blocks — an array of block objects that define layout, text, images, buttons, and menus.
  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:

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)

Malformed blocks are omitted at render time; valid sibling blocks still display.

Text 

{
  "type": "text",
  "text": "Hello **from** MM Blocks.",
  "is_subtle": false,
  "size": "default"
}
Field Required Description
text yes Markdown-formatted content. Supports @mentions.
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.

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

Divider 

{"type": "divider"}

Button 

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

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"
}
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).

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

Collapsible 

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

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.

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

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.

Action types 

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.

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

Legacy format compatibility 

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.

Validation limits 

Posts that exceed any of the following limits are rejected at create or update time.

Limit Value
Maximum entries in mm_blocks_actions 50
Maximum length of an action ID (map key) 64 characters
Action ID character set [A-Za-z0-9_-]+
Maximum entries in query (link, registry, or block) 50
Maximum length of a query key 128 characters
Maximum length of a query value 2048 characters
Maximum entries in context per action 50
Maximum length of a context key 128 characters

Security considerations 

MM 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 

Did you find what you were looking for?

Thank you! We appreciate your feedback.
×

Tell us more

Your feedback helps us improve the Mattermost developer documentation.

Have a feature request? Share it here.

Having issues? Join our Community server.