Use markdown action buttons to add inline, in-text affordances to a post — without using a message attachment. They’re useful when:
For attachment-style buttons and menus, see interactive messages.
A markdown action has two parts:
A markdown link in the post body using the mmaction:// scheme, where the link host is the action ID:
[Approve](mmaction://approve?ticket=ISS-101)
A matching entry in the post’s props.mm_blocks_actions registry that tells the server what to do when the link is clicked:
{
"mm_blocks_actions": {
"approve": {
"type": "external",
"url": "https://integration.example.com/hook/approve",
"context": {"project": "Demo Project"}
}
}
}
The client renders the link as a button. Clicking it dispatches a request to the Mattermost server, which forwards the call to the integration’s url along with merged query parameters and any server-side context.
The following payload posts a message with two markdown action buttons. The body markdown references action IDs defined in mm_blocks_actions.
{
"channel_id": "qmd5oqtwoibz8cuzxzg5ekshgr",
"message": "Ticket ISS-101 needs review: [Approve](mmaction://approve?ticket=ISS-101) [Reject](mmaction://reject?ticket=ISS-101)",
"props": {
"mm_blocks_actions": {
"approve": {
"type": "external",
"url": "https://integration.example.com/hook/approve",
"context": {"project": "Demo Project"}
},
"reject": {
"type": "external",
"url": "https://integration.example.com/hook/reject",
"context": {"project": "Demo Project"}
}
}
}
}
You can send this payload using the create post REST API, an incoming webhook, or from a plugin.
curl -X POST $MM_URL/api/v4/posts \
-H "Authorization: Bearer $BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"channel_id": "'$CHANNEL_ID'",
"message": "Ticket ISS-101 needs review: [Approve](mmaction://approve?ticket=ISS-101) [Reject](mmaction://reject?ticket=ISS-101)",
"props": {
"mm_blocks_actions": {
"approve": {
"type": "external",
"url": "https://integration.example.com/hook/approve",
"context": {"project": "Demo Project"}
},
"reject": {
"type": "external",
"url": "https://integration.example.com/hook/reject",
"context": {"project": "Demo Project"}
}
}
}
}'
A server-side plugin creates posts with markdown actions through the existing API.CreatePost / API.UpdatePost interfaces. The action url typically points at the plugin’s own HTTP handler.
post := &model.Post{
ChannelId: channelID,
UserId: p.botID,
Message: "Ticket ISS-101 needs review: " +
"[Approve](mmaction://approve?ticket=ISS-101) " +
"[Reject](mmaction://reject?ticket=ISS-101)",
Props: model.StringInterface{
"mm_blocks_actions": map[string]any{
"approve": map[string]any{
"type": "external",
"url": fmt.Sprintf("/plugins/%s/inline_action/approve", manifest.Id),
"context": map[string]any{"project": "Demo Project"},
},
"reject": map[string]any{
"type": "external",
"url": fmt.Sprintf("/plugins/%s/inline_action/reject", manifest.Id),
"context": map[string]any{"project": "Demo Project"},
},
},
},
}
_, err := p.API.CreatePost(post)
Plugin updates to mm_blocks_actions via UpdatePost are accepted only when the updated value passes validation. Removal of the mm_blocks_actions prop by non-integration sessions is restricted to prevent dropping or corrupting actions on posts owned by another integration.
[<label>](mmaction://<action_id>?<query_string>)
<label>
The link text rendered as the button label.
<action_id>
The host portion of the URL. Must match a key in props.mm_blocks_actions. Must be alphanumeric ([A-Za-z0-9]+), matched case-sensitively.
<query_string> (optional)
key=value pairs that are forwarded with the dispatched action and merged into the target URL’s query string by the server. Link-supplied values override registry-supplied values on key conflict.
mm_blocks_actions registry The mm_blocks_actions post prop is a map keyed by action ID. Each entry describes how the server should handle 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 for the integration’s callback endpoint. Required for external. |
context |
no | Object of server-side context values forwarded to the integration in the post-action request body. Not visible to the client. |
query |
no | Static string -> string map merged into the target URL’s query string by the server. Combined with any query parameters supplied in the mmaction:// link — link values win on key conflict. |
| Type | Behaviour |
|---|---|
external |
The server sends a POST request to url with the dispatched action, including context, query (after merging), and any link-supplied query parameters. The integration responds with a standard post-action response. |
Additional action types may be introduced as the broader Interactive Messages framework lands. Entries with an unknown type value are rejected at post-create time.
The diagram below describes the lifecycle of a single click on a markdown action button.
mmaction://<id> link and a matching mm_blocks_actions[<id>] entry.mm_blocks_actions schema and limits) and stores it.POST /api/v4/posts/{post_id}/actions/{action_id} with the link’s query string in the request body.mm_blocks_actions[<id>], merges the registry’s static query with the request body’s query (request body wins on conflict), merges the result into the action URL’s query string, and POSTs the integration endpoint.When a user clicks a markdown action button, the Mattermost server sends an HTTP POST request to the url configured in the matching mm_blocks_actions entry. The request body follows the same PostActionIntegrationRequest shape used by message attachment buttons — the integration responds with the same post-action response format.
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 or registry) |
50 |
| Maximum length of a query key | 128 characters |
| Maximum length of a query value | 2048 characters |
The following error IDs may be returned by the post-action and post-create APIs when processing markdown action requests.
| Error ID | Cause |
|---|---|
api.post.do_action.query.app_error |
The query parameters supplied with the action click exceeded one of the limits above. |
api.post.do_action.merge_query.app_error |
The server could not merge the supplied query parameters into the action’s target URL — typically because the URL is malformed. |
Markdown action buttons follow the same security model as message attachment actions:
url is invoked server-to-server, never directly from the client.context values are server-only and are not exposed to the rendering client.type values are rejected at post-create time and never reach the click-dispatch path.