
OpenClaw Integration with Mattermost

By Sarah Jenkins


By Sarah Jenkins
Status: supported via plugin (bot token + WebSocket events). Channels, groups, and DMs are supported. Mattermost is a self-hostable team messaging platform; see the official site at mattermost.com for product details and downloads.
Mattermost ships as a plugin and is not bundled with the core install.
Install via CLI (npm registry):
openclaw plugins install @openclaw/mattermostLocal checkout (when running from a git repo):
openclaw plugins install ./path/to/local/mattermost-pluginIf you choose Mattermost during setup and a git checkout is detected, OpenClaw will offer the local install path automatically.
https://chat.example.com).Minimal config:
{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing",
},
},
}Native slash commands are opt-in. When enabled, OpenClaw registers oc_* slash commands via the Mattermost API and receives 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",
},
},
},
}Notes:
native: "auto" defaults to disabled for Mattermost. Set native: true to enable.callbackUrl is omitted, OpenClaw derives one from gateway host/port + callbackPath.commands can be set at the top level or under channels.mattermost.accounts.<id>.commands (account values override top-level fields).Set these on the gateway host if you prefer env vars:
MATTERMOST_BOT_TOKEN=...MATTERMOST_URL=https://chat.example.comEnv vars apply only to the default account (default). Other accounts must use config values.
Mattermost responds to DMs automatically. Channel behavior is controlled by chatmode:
oncall (default): respond only when @mentioned in channels.onmessage: respond to every channel message.onchar: respond when a message starts with a trigger prefix.Config example:
{
channels: {
mattermost: {
chatmode: "onchar",
oncharPrefixes: [">", "!"],
},
},
}Notes:
onchar still responds to explicit @mentions.channels.mattermost.requireMention is honored for legacy configs but chatmode is preferred.Use channels.mattermost.replyToMode to control whether channel and group replies stay in the main channel or start a thread under the triggering post.
off (default): only reply in a thread when the inbound post is already in one.first: for top-level channel/group posts, start a thread under that post and route the conversation to a thread-scoped session.all: same behavior as first for Mattermost today.Config example:
{
channels: {
mattermost: {
replyToMode: "all",
},
},
}Notes:
first and all are currently equivalent because once Mattermost has a thread root, follow-up chunks and media continue in that same thread.channels.mattermost.dmPolicy = "pairing" (unknown senders get a pairing code).channels.mattermost.dmPolicy="open" plus channels.mattermost.allowFrom=["*"].channels.mattermost.groupPolicy = "allowlist" (mention-gated).channels.mattermost.groupAllowFrom (user IDs recommended).@username matching is mutable and only enabled when channels.mattermost.dangerouslyAllowNameMatching: true.channels.mattermost.groupPolicy="open" (mention-gated).channels.mattermost is completely missing, runtime falls back to groupPolicy="allowlist" for group checks (even if channels.defaults.groupPolicy is set).Use these target formats with openclaw message send or cron/webhooks:
channel:<id> for a channeluser:<id> for a DM@username for a DM (resolved via the Mattermost API)Bare opaque IDs (like 64ifufp...) are ambiguous in Mattermost (user ID vs channel ID).
OpenClaw resolves them user-first:
GET /api/v4/users/<id> succeeds), OpenClaw sends a DM by resolving the direct channel via /api/v4/channels/direct.If you need deterministic behavior, always use the explicit prefixes (user:<id> / channel:<id>).
When OpenClaw sends to a Mattermost DM target and needs to resolve the direct channel first, it retries transient direct-channel creation failures by default.
Use channels.mattermost.dmChannelRetry to tune that behavior globally for the Mattermost plugin, or channels.mattermost.accounts.<id>.dmChannelRetry for one account.
{
channels: {
mattermost: {
dmChannelRetry: {
maxRetries: 3,
initialDelayMs: 1000,
maxDelayMs: 10000,
timeoutMs: 30000,
},
},
},
}Notes:
/api/v4/channels/direct), not every Mattermost API call.429 are treated as permanent and are not retried.message action=react with channel=mattermost.messageId is the Mattermost post id.emoji accepts names like thumbsup or :+1: (colons are optional).remove=true (boolean) to remove a reaction.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=trueConfig:
channels.mattermost.actions.reactions: enable/disable reaction actions (default true).channels.mattermost.accounts.<id>.actions.reactions.Send messages with clickable buttons. When a user clicks a button, the agent receives the selection and can respond.
Enable buttons by adding inlineButtons to the channel capabilities:
{
channels: {
mattermost: {
capabilities: ["inlineButtons"],
},
},
}Use message action=send with a buttons parameter. Buttons are a 2D array (rows of buttons):
message action=send channel=mattermost target=channel:<channelId> buttons=[[{"text":"Yes","callback_data":"yes"},{"text":"No","callback_data":"no"}]]Button fields:
text (required): display label.callback_data (required): value sent back on click (used as the action ID).style (optional): "default", "primary", or "danger".When a user clicks a button:
Notes:
Config:
channels.mattermost.capabilities: array of capability strings. Add "inlineButtons" to enable the buttons tool description in the agent system prompt.channels.mattermost.interactions.callbackBaseUrl: optional external base URL for button callbacks (for example https://gateway.example.com). Use this when Mattermost cannot reach the gateway at its bind host directly.channels.mattermost.accounts.<id>.interactions.callbackBaseUrl.interactions.callbackBaseUrl is omitted, OpenClaw derives the callback URL from gateway.customBindHost + gateway.port, then falls back to http://localhost:<port>.localhost only works when Mattermost and OpenClaw run on the same host/network namespace.ServiceSettings.AllowedUntrustedInternalConnections.External scripts and webhooks can post buttons directly via the Mattermost REST API instead of going through the agent's message tool. Use buildButtonAttachments() from the extension when possible; if posting raw JSON, follow these 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:
props.attachments, not top-level attachments (silently ignored).type: "button" — without it, clicks are swallowed silently.id field — Mattermost ignores actions without IDs.id must be alphanumeric only ([a-zA-Z0-9]). Hyphens and underscores break Mattermost's server-side action routing (returns 404). Strip them before use.context.action_id must match the button's id so the confirmation message shows the button name (e.g., "Approve") instead of a raw ID.context.action_id is required — the interaction handler returns 400 without it.HMAC token generation:
The gateway verifies button clicks with HMAC-SHA256. External scripts must generate tokens that match the gateway's verification logic:
HMAC-SHA256(key="openclaw-mattermost-interactions", data=botToken)_token.JSON.stringify with sorted keys, which produces compact output).HMAC-SHA256(key=secret, data=serializedContext)_token in the context.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}Common HMAC pitfalls:
json.dumps adds spaces by default ({"key": "val"}). Use separators=(",", ":") to match JavaScript's compact output ({"key":"val"})._token). The gateway strips _token then signs everything remaining. Signing a subset causes silent verification failure.sort_keys=True — the gateway sorts keys before signing, and Mattermost may reorder context fields when storing the payload.The Mattermost plugin includes a directory adapter that resolves channel and user names via the Mattermost API. This enables #channel-name and @username targets in openclaw message send and cron/webhook deliveries.
No configuration is needed — the adapter uses the bot token from the account config.
Mattermost supports multiple accounts under channels.mattermost.accounts:
{
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" },
},
},
},
}chatmode: "onmessage".default account.text and callback_data fields.AllowedUntrustedInternalConnections in Mattermost server config includes 127.0.0.1 localhost, and that EnablePostActionIntegration is true in ServiceSettings.id likely contains hyphens or underscores. Mattermost's action router breaks on non-alphanumeric IDs. Use [a-zA-Z0-9] only.invalid _token: HMAC mismatch. Check that you sign all context fields (not a subset), use sorted keys, and use compact JSON (no spaces). See the HMAC section above.missing _token in context: the _token field is not in the button's context. Ensure it is included when building the integration payload.context.action_id does not match the button's id. Set both to the same sanitized value.capabilities: ["inlineButtons"] to the Mattermost channel config.About the author

Sarah Jenkins is a seasoned OpenClaw developer with a strong focus on optimizing high-performance computing solutions. Her work primarily involves crafting efficient parallel algorithms and enhancing GPU acceleration for complex scientific simulations. Jenkins is renowned for her meticulous attention to detail and her ability to translate intricate theoretical concepts into practical, robust OpenClaw implementations.

by Sarah Jenkins
by Sarah Jenkins