Skip to content

Connect Mattermost to OpenClaw: 5-Minute Setup Guide

Mattermost works as a plugin and isn’t included in the core installation. You can install it via the CLI from the npm registry:

Terminal window
openclaw plugins install @openclaw/mattermost

If you are running from a git repo and have a local checkout, use this command:

Terminal window
openclaw plugins install ./path/to/local/mattermost-plugin

OpenClaw will automatically offer the local install path if it detects a git checkout when you run the setup.

Check out the Plugins page for more details.

Getting started is straightforward. You just need to follow these steps:

  1. Install the Mattermost plugin.
  2. Create a Mattermost bot account and copy the bot token.
  3. Copy your Mattermost base URL (for example, https://chat.example.com).
  4. Configure OpenClaw and start the gateway.

Here is a minimal config to get you moving:

{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing",
},
},
}

Native slash commands are an opt-in feature. When you enable them, OpenClaw registers oc_* slash commands through the Mattermost API and handles callback POSTs on the gateway HTTP server.

{
channels: {
mattermost: {
commands: {
native: true,
nativeSkills: true,
callbackPath: "/api/channels/mattermost/command",
// Use when Mattermost cannot reach the gateway directly (reverse proxy/public URL).
callbackUrl: "https://gateway.example.com/api/channels/mattermost/command",
},
},
},
}

Keep these details in mind:

  • The native: "auto" setting defaults to disabled for Mattermost. You must set native: true to use it.
  • If you omit callbackUrl, OpenClaw creates one using the gateway host, port, and callbackPath.
  • For setups with multiple accounts, you can set commands at the top level or under channels.mattermost.accounts.<id>.commands. Values set for specific accounts will override top-level settings.
  • Command callbacks use per-command tokens for validation. If a token check fails, the request fails closed for security.
  • Your Mattermost server must be able to reach the callback endpoint.
  • Do not set callbackUrl to localhost unless Mattermost is running on the same host or network namespace as OpenClaw.
  • Do not set callbackUrl to your Mattermost base URL unless that URL is configured to reverse-proxy /api/channels/mattermost/command to OpenClaw.
  • You can verify reachability by running curl https://<gateway-host>/api/channels/mattermost/command. A GET request should return 405 Method Not Allowed from OpenClaw rather than a 404.
  • Check the Mattermost egress allowlist. If your callback targets private, tailnet, or internal addresses, you need to update Mattermost ServiceSettings.AllowedUntrustedInternalConnections to include the callback host or domain.
  • Use host or domain entries instead of full URLs in the allowlist.
    • Good: gateway.tailnet-name.ts.net
    • Bad: https://gateway.tailnet-name.ts.net

If you prefer using environment variables, you can set these on the gateway host:

  • MATTERMOST_BOT_TOKEN=...
  • MATTERMOST_URL=https://chat.example.com

These environment variables only apply to the default account. If you are using other accounts, you must use the configuration values instead.

Mattermost handles your DMs automatically, but you can control how it behaves in channels using the chatmode setting. You have three main options to choose from:

  • oncall (default): The bot responds only when you @mention it in channels.
  • onmessage: The bot responds to every single message in the channel.
  • onchar: The bot responds when a message starts with a specific trigger prefix.

Here is a config example:

{
channels: {
mattermost: {
chatmode: "onchar",
oncharPrefixes: [">", "!"],
},
},
}

A few things to keep in mind: onchar still responds to explicit @mentions. Also, while channels.mattermost.requireMention is still supported for legacy configs, you should use chatmode instead as it is the preferred method.

You can use channels.mattermost.replyToMode to decide if channel and group replies stay in the main channel or start a thread under the post that triggered them.

  • off (default): It only replies in a thread if the inbound post is already in one.
  • first: For top-level channel or group posts, it starts a thread under that post and routes the conversation to a thread-scoped session.
  • all: This currently behaves the same as first for Mattermost.
  • Direct messages ignore this setting and always stay non-threaded.

Config example:

{
channels: {
mattermost: {
replyToMode: "all",
},
},
}

Note that thread-scoped sessions use the triggering post id as the thread root. Currently, first and all are equivalent because once Mattermost has a thread root, follow-up chunks and media continue in that same thread.

By default, the system uses channels.mattermost.dmPolicy = "pairing". This means unknown senders will receive a pairing code. You can manage these approvals using the following commands:

  • openclaw pairing list mattermost
  • openclaw pairing approve mattermost <CODE>

If you want to enable public DMs, you need to set channels.mattermost.dmPolicy="open" and add channels.mattermost.allowFrom=["*"].

For group channels, the default setting is channels.mattermost.groupPolicy = "allowlist", which is mention-gated. You can allow specific senders using channels.mattermost.groupAllowFrom; I recommend using user IDs for this.

Matching by @username can be unreliable because names change, so it is only enabled if you set channels.mattermost.dangerouslyAllowNameMatching: true. For open channels, use channels.mattermost.groupPolicy="open".

One important runtime note: if the channels.mattermost configuration is completely missing, the system falls back to groupPolicy="allowlist" for group checks. This happens even if you have a different channels.defaults.groupPolicy defined.

When you use openclaw message send or set up cron jobs and webhooks, you have a few ways to specify where your message should go:

  • channel:<id> for a specific channel
  • user:<id> for a direct message
  • @username for a direct message (this gets resolved through the Mattermost API)

You should be careful with bare opaque IDs like 64ifufp.... In Mattermost, these are ambiguous because they could represent either a user ID or a channel ID.

OpenClaw handles these using a user-first approach:

  • It first checks if the ID belongs to a user via GET /api/v4/users/<id>. If that works, OpenClaw sends a DM by finding the direct channel through /api/v4/channels/direct.
  • If no user is found, it treats the ID as a channel ID.

If you want to be sure about what happens, you should always use the explicit user:<id> or channel:<id> prefixes.

When you send a message to a Mattermost DM target, OpenClaw often has to resolve the direct channel first. If that initial creation fails due to a temporary issue, the system retries the attempt by default.

You can adjust this behavior globally for the Mattermost plugin using channels.mattermost.dmChannelRetry, or set it for a specific account with channels.mattermost.accounts.<id>.dmChannelRetry.

{
channels: {
mattermost: {
dmChannelRetry: {
maxRetries: 3,
initialDelayMs: 1000,
maxDelayMs: 10000,
timeoutMs: 30000,
},
},
},
}

A few things to keep in mind:

  • This retry logic only applies to DM channel creation (/api/v4/channels/direct). It doesn’t affect every single Mattermost API call.
  • Retries happen for transient failures. This includes rate limits, 5xx server responses, network errors, and timeout errors.
  • If you get a 4xx client error (other than a 429 rate limit), OpenClaw treats it as a permanent failure and won’t retry.

If you want your agent to interact with posts using emojis, you can use the message action=react tool. You just need to specify channel=mattermost and provide the messageId, which is the unique ID of the Mattermost post.

For the emoji parameter, you can use standard names like thumbsup or include colons like :+1:. If you need to retract a reaction, just set remove=true. When users add or remove reactions, these actions are sent back to the agent session as system events so your bot stays in the loop.

Examples:

message action=react channel=mattermost target=channel:<channelId> messageId=<postId> emoji=thumbsup
message action=react channel=mattermost target=channel:<channelId> messageId=<postId> emoji=thumbsup remove=true

You can control this feature in your configuration. By default, channels.mattermost.actions.reactions is set to true. If you are running multiple accounts, you can also set an override for a specific account using channels.mattermost.accounts.<id>.actions.reactions.

You can make your messages more interactive by adding clickable buttons. When a user clicks one, your agent receives the selection and can decide how to respond.

To get started, you need to enable buttons by adding inlineButtons to your channel capabilities in the config:

{
channels: {
mattermost: {
capabilities: ["inlineButtons"],
},
},
}

When you want to send buttons, use message action=send with a buttons parameter. Buttons are organized in a 2D array, which allows you to group them into rows:

message action=send channel=mattermost target=channel:<channelId> buttons=[[{"text":"Yes","callback_data":"yes"},{"text":"No","callback_data":"no"}]]

Each button requires a text label and callback_data, which acts as the action ID. You can also use the style field to set the look to "default", "primary", or "danger".

Once a user clicks a button, Mattermost replaces all buttons in that message with a confirmation line, such as ”✓ Yes selected by @user”. Your agent then receives that selection as an inbound message.

A few things to keep in mind:

  • Button callbacks use automatic HMAC-SHA256 verification.
  • Mattermost removes all buttons on click because it strips callback data from API responses for security.
  • If your action IDs have hyphens or underscores, the system handles the sanitization automatically to avoid routing issues.

For configuration, use channels.mattermost.capabilities to enable the tool description in the system prompt. If your Mattermost server cannot reach the gateway directly, set channels.mattermost.interactions.callbackBaseUrl to an external URL. If you leave this out, the system tries to build a URL from your bind host and port.

If your setup is internal or uses a private network, you must add the host to the Mattermost ServiceSettings.AllowedUntrustedInternalConnections list.

If you are using external scripts or webhooks, you can post buttons directly through the Mattermost REST API. While using buildButtonAttachments() is recommended, you can also post raw JSON by following these specific rules.

Payload structure:

{
channel_id: "<channelId>",
message: "Choose an option:",
props: {
attachments: [
{
actions: [
{
id: "mybutton01", // alphanumeric only — see below
type: "button", // required, or clicks are silently ignored
name: "Approve", // display label
style: "primary", // optional: "default", "primary", "danger"
integration: {
url: "https://gateway.example.com/mattermost/interactions/default",
context: {
action_id: "mybutton01", // must match button id (for name lookup)
action: "approve",
// ... any custom fields ...
_token: "<hmac>", // see HMAC section below
},
},
},
],
},
],
},
}

Critical rules:

  1. Put attachments in props.attachments. If you put them at the top level, Mattermost ignores them.
  2. You must include type: "button" for every action.
  3. Every action needs an id that is strictly alphanumeric. Hyphens or underscores will cause 404 errors in Mattermost routing.
  4. Ensure context.action_id matches the button id so the confirmation message displays the correct button name.

HMAC token generation:

The gateway uses HMAC-SHA256 to verify that button clicks are legitimate. Your external scripts need to generate tokens that match this logic:

  1. Create a secret by hashing your bot token with the key "openclaw-mattermost-interactions".
  2. Prepare your context object with all fields except _token.
  3. Serialize the object with sorted keys and no extra spaces.
  4. Sign that string using your secret and add the hex digest as _token.

Python example:

import hmac, hashlib, json
secret = hmac.new(
b"openclaw-mattermost-interactions",
bot_token.encode(), hashlib.sha256
).hexdigest()
ctx = {"action_id": "mybutton01", "action": "approve"}
payload = json.dumps(ctx, sort_keys=True, separators=(",", ":"))
token = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()
context = {**ctx, "_token": token}

To avoid verification failures, make sure you use separators=(",", ":") in Python to match the compact JSON format used by JavaScript. Always sign every field in the context and use sort_keys=True to keep the order consistent.

You don’t want to hunt for internal IDs every time you send a message. The Mattermost plugin includes a directory adapter that handles name resolution through the Mattermost API. This lets you use #channel-name and @username targets directly in openclaw message send or your cron and webhook deliveries. You don’t need to worry about extra configuration because the adapter uses the bot token from your existing account setup.

Sometimes one account isn’t enough, especially when you need to separate primary chat from automated alerts. Mattermost supports multiple accounts under the channels.mattermost.accounts key. You can define them like this:

{
channels: {
mattermost: {
accounts: {
default: { name: "Primary", botToken: "mm-token", baseUrl: "https://chat.example.com" },
alerts: { name: "Alerts", botToken: "mm-token-2", baseUrl: "https://alerts.example.com" },
},
},
},
}

If things aren’t working as expected, don’t worry. Here is a guide to help you fix the most common issues you might run into:

  • No replies in channels: Make sure the bot is actually in the channel. You might need to mention it (oncall), use a trigger prefix (onchar), or set chatmode: "onmessage".
  • Auth errors: Check your bot token and base URL. You should also verify that the account is enabled.
  • Multi-account issues: Keep in mind that environment variables only apply to the default account.
  • Buttons appear as white boxes: This usually happens when the agent sends malformed button data. You need to check that each button has both text and callback_data fields.
  • Buttons render but clicks do nothing: Check your Mattermost server config. Verify that AllowedUntrustedInternalConnections includes 127.0.0.1 localhost and that EnablePostActionIntegration is set to true in ServiceSettings.
  • Buttons return 404 on click: The button id likely contains hyphens or underscores. Mattermost’s action router fails on non-alphanumeric IDs. You should use [a-zA-Z0-9] only.
  • Gateway logs invalid _token: This indicates an HMAC mismatch. You must sign all context fields (not just a subset), use sorted keys, and use compact JSON without extra spaces. Refer to the HMAC section above for details.
  • Gateway logs missing _token in context: The _token field is missing from the button’s context. You need to ensure it is included when you build the integration payload.
  • Confirmation shows raw ID instead of button name: This happens when context.action_id does not match the button’s id. You should set both to the same sanitized value.
  • Agent doesn’t know about buttons: You need to add capabilities: ["inlineButtons"] to your Mattermost channel configuration.

AI Setup Assistant

OpenClaw

OpenClaw Expert

Still stuck?

If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.