Mattermost Bot Setup and OpenClaw Configuration
Learn how to install and configure the Mattermost plugin for OpenClaw, including bot creation, base URL setup, and private network options. Ideal for teams integrating Mattermost channels.
Read this when
- Setting up Mattermost
- Debugging Mattermost routing
Status: downloadable plugin (bot token + WebSocket events). Channels, private channels, group DMs, and DMs are supported. Mattermost is a self-hostable team messaging platform (mattermost.com).
Install
npm registry
openclaw plugins install @openclaw/mattermost
Local checkout
openclaw plugins install ./path/to/local/mattermost-plugin
Details: Plugins
Quick setup
Ensure plugin is available
Install @openclaw/mattermost using the command above, then reboot the Gateway if it is currently active.
Create a Mattermost bot
Set up a Mattermost bot account, grab the bot token, and include the bot in the teams and channels it needs to monitor.
Copy the base URL
Provide the Mattermost base URL (for instance, https://chat.example.com). Any trailing /api/v4 gets removed automatically.
Configure OpenClaw and start the gateway
Minimal config:
{
channels: {
mattermost: {
enabled: true,
botToken: "mm-token",
baseUrl: "https://chat.example.com",
dmPolicy: "pairing",
},
},
}
Non-interactive alternative:
openclaw channels add --channel mattermost --bot-token <token> --http-url https://chat.example.com
Note
Self-hosted Mattermost on a private/LAN/tailnet address: outbound Mattermost API requests pass through an SSRF guard that blocks private and internal IPs by default. Opt in with
channels.mattermost.network.dangerouslyAllowPrivateNetwork: true(per account:channels.mattermost.accounts.<id>.network.dangerouslyAllowPrivateNetwork).
Native slash commands
Native slash commands are opt-in. When enabled, OpenClaw registers oc_* slash commands on every team the bot is a member of 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",
},
},
},
}
Registered commands: /oc_status, /oc_model, /oc_models, /oc_new, /oc_help, /oc_think, /oc_reasoning, /oc_verbose, /oc_queue. With nativeSkills: true, skill commands are also registered as /oc_<skill>.
Behavior notes
nativeandnativeSkillsdefault to"auto", which resolves to disabled for Mattermost. Set them totrueexplicitly.callbackPathdefaults to/api/channels/mattermost/command.- If
callbackUrlis omitted, OpenClaw deriveshttp://<gateway.customBindHost or localhost>:<gateway.port, default 18789><callbackPath>. Wildcard bind hosts (0.0.0.0,::) fall back tolocalhost. - For multi-account setups,
commandscan be set at the top level or underchannels.mattermost.accounts.<id>.commands(account values override top-level fields). - Existing slash commands with the same trigger created by other integrations are left untouched (registration skips them); commands the bot created are updated or recreated when the callback URL drifts.
- Command callbacks are validated with the per-command tokens returned by Mattermost when OpenClaw registers
oc_*commands. - OpenClaw refreshes current Mattermost command registration before accepting each callback, so stale tokens from deleted or regenerated slash commands stop being accepted without a gateway restart.
- Callback validation fails closed if the Mattermost API cannot confirm the command is still current; failed validations are cached briefly, concurrent lookups are coalesced, and fresh lookup starts are rate-limited per command to bound replay pressure.
- Slash callbacks fail closed when registration failed, startup was partial, or the callback token does not match the resolved command's registered token (a token valid for one command cannot reach upstream validation for a different command).
- Accepted callbacks are acknowledged with an ephemeral "Processing..." reply; the real answer arrives as a normal message.
Reachability requirement
The callback endpoint must be reachable from the Mattermost server.
- Do not set
callbackUrltolocalhostunless Mattermost runs on the same host/network namespace as OpenClaw. - Do not set
callbackUrlto your Mattermost base URL unless that URL reverse-proxies/api/channels/mattermost/commandto OpenClaw. - A quick check is
curl https://<gateway-host>/api/channels/mattermost/command; a GET should return405 Method Not Allowedfrom OpenClaw, not404.
Mattermost egress allowlist
If your callback targets private/tailnet/internal addresses, set Mattermost ServiceSettings.AllowedUntrustedInternalConnections to include the callback host/domain.
Use host/domain entries, not full URLs.
- Good:
gateway.tailnet-name.ts.net - Bad:
https://gateway.tailnet-name.ts.net
Environment variables (default account)
Set these on the gateway host if you prefer env vars:
MATTERMOST_BOT_TOKEN=...MATTERMOST_URL=https://chat.example.com
Note
The default account (
default) is the only one affected by environment variables. Other accounts need to rely on config values.Setting
MATTERMOST_URLfrom a workspace.envis not possible; refer to Workspace .env files for details.
Chat modes
DMs are handled automatically by Mattermost. The chatmode setting dictates how channels behave:
oncall (default)
Only respond when @mentioned within channels.
onmessage
Reply to every message posted in channels.
onchar
Reply whenever a message begins with a trigger prefix.
Example configuration:
{
channels: {
mattermost: {
chatmode: "onchar",
oncharPrefixes: [">", "!"], // default
},
},
}
Additional notes:
- Explicit @mentions still trigger responses from
onchar. - While
channels.mattermost.requireMentionremains recognized,chatmodeis the recommended option. Any per-channelgroups.<channelId>.requireMentionconfiguration takes precedence over both. - Once the bot posts a visible reply in a channel thread, subsequent messages in that same thread get answered without needing another @mention or
oncharprefix, which keeps multi-turn thread conversations going. The bot remembers participation for 7 days after its last reply in that thread, and this memory survives gateway restarts. Threads the bot merely observed are not affected; to require an explicit mention again, start a fresh top-level message. - To prevent participated-thread follow-ups from skipping mention gating, set
channels.mattermost.implicitMentions.threadParticipation: false. Account-level overrides rely onchannels.mattermost.accounts.<id>.implicitMentions. Since Mattermost does not currently generatereplyToBotorquotedBotfacts, those flags have no effect here.
Threading and sessions
Whether channel and group replies stay in the main channel or spawn a thread under the triggering post is controlled by channels.mattermost.replyToMode.
off(default): only reply within a thread if the incoming post is already part of one.first: for top-level channel/group posts, create a thread under that post and direct the conversation to a thread-scoped session.allandbatched: behave likefirstfor Mattermost at present, since once a thread root exists in Mattermost, follow-up chunks and media continue in that same thread.- Direct messages default to
offeven whenreplyToModeis configured.
To override the mode for direct, group, or channel chats, use channels.mattermost.replyToModeByChatType. Set direct to enable threading for direct messages:
off(default): direct messages remain non-threaded within a single rolling session.first,all, orbatched: every top-level direct message initiates a Mattermost thread backed by a new, independent session.
{
channels: {
mattermost: {
replyToMode: "all",
replyToModeByChatType: {
direct: "first",
},
},
},
}
Notes:
- The triggering post id serves as the thread root for thread-scoped sessions.
firstandallare interchangeable right now, because once Mattermost has a thread root, follow-up chunks and media continue in that same thread.- Per-chat-type overrides outrank
replyToMode. Without adirectoverride, existing deployments keep flat, non-threaded DMs.
Access control (DMs)
- Default is
channels.mattermost.dmPolicy = "pairing"(unknown senders receive a pairing code). Alternatives:allowlist,open,disabled. - Approval methods:
openclaw pairing list mattermostopenclaw pairing approve mattermost <CODE>
- For public DMs:
channels.mattermost.dmPolicy="open"combined withchannels.mattermost.allowFrom=["*"](the wildcard is enforced by the config schema). channels.mattermost.allowFromaccepts user ids (preferred) as well asaccessGroup:<name>entries. See Access groups for more.
Channels (groups)
- Default:
channels.mattermost.groupPolicy = "allowlist"(mention-gated). - Use
channels.mattermost.groupAllowFromto allowlist senders (user IDs are recommended). channels.mattermost.groupAllowFromhandlesaccessGroup:<name>entries. Check Access groups.- Per-channel mention overrides are found under
channels.mattermost.groups.<channelId>.requireMentionor, for a default,channels.mattermost.groups["*"].requireMention. @usernamematching can change and is active only whenchannels.mattermost.dangerouslyAllowNameMatching: true.- Open channels:
channels.mattermost.groupPolicy="open"(mention-gated). - Order of resolution:
channels.mattermost.groupPolicy, thenchannels.defaults.groupPolicy, then"allowlist". - Runtime note: if the
channels.mattermostsection is absent entirely, runtime fails closed togroupPolicy="allowlist"for group checks (even whenchannels.defaults.groupPolicyis set) and logs a one-time warning.
Example:
{
channels: {
mattermost: {
groupPolicy: "open",
groups: {
"*": { requireMention: true },
"team-channel-id": { requireMention: false },
},
},
},
}
Targets for outbound delivery
Apply these target formats with openclaw message send or cron/webhooks:
| Target | Delivers to |
|---|---|
channel:<id> | Channel by id |
channel:<name> or #channel-name | Channel by name, searched across the teams the bot belongs to |
user:<id> or mattermost:<id> | DM with that user |
@username | DM (username resolved via the Mattermost API) |
Outbound sends allow only one attachment per message; split multiple files into separate sends.
Set channels.mattermost.mediaMaxMb to cap each inbound download and outbound attachment in MiB. accounts.<id>.mediaMaxMb overrides the channel root, then agents.defaults.mediaMaxMb provides the fallback. With no cap configured, inbound downloads keep their 8 MiB default and outbound media keeps the shared loader defaults. Outbound images may be optimized. With a cap set, download or upload failures cause the send to fail instead of posting the unchecked original URL. Without a cap set, the existing URL fallback remains available.
Warning
Bare opaque IDs (like
64ifufp...) are ambiguous in Mattermost (user ID vs channel ID).OpenClaw resolves them user-first:
- If the ID exists as a user (
GET /api/v4/users/<id>succeeds), OpenClaw sends a DM by resolving the direct channel via/api/v4/channels/direct.- Otherwise the ID is treated as a channel ID.
For deterministic behavior, always use the explicit prefixes (
user:<id>/channel:<id>).
DM channel retry
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 adjust that behavior globally for the Mattermost plugin, or channels.mattermost.accounts.<id>.dmChannelRetry for one account. Defaults:
{
channels: {
mattermost: {
dmChannelRetry: {
maxRetries: 3,
initialDelayMs: 1000,
maxDelayMs: 10000,
timeoutMs: 30000,
},
},
},
}
Notes:
- This applies only to DM channel creation (
/api/v4/channels/direct), not every Mattermost API call. - Retries use exponential backoff with jitter and apply to transient failures such as rate limits, 5xx responses, and network or timeout errors.
- 4xx client errors other than
429are treated as permanent and are not retried.
Preview streaming
Mattermost streams thinking, tool activity, and partial reply text into a draft preview post that finalizes in place when the final answer is safe to send. In partial mode the preview updates on the same post id instead of spamming the channel with per-chunk messages. In block mode the preview rotates between completed text and tool-activity blocks, so earlier blocks stay visible as their own posts instead of being overwritten by the next one. Media/error finals cancel pending preview edits and use normal delivery instead of flushing a throwaway preview post.
Preview streaming is on by default in partial mode. Configure via channels.mattermost.streaming.mode (legacy scalar/boolean streaming values are migrated by openclaw doctor --fix):
{
channels: {
mattermost: {
streaming: { mode: "partial" }, // off | partial | block | progress
},
},
}
Streaming modes
partial(default): one preview post that is edited as the reply grows, then finalized with the complete answer.blockrotates the preview between completed text and tool-activity blocks, so each block stays visible as its own post instead of being overwritten in place. Parallel and consecutive tool updates share the current tool-activity post.progressshows a status preview while generating and only posts the final answer at completion.offdisables preview streaming. Withstreaming.block.enabled: true, completed assistant blocks are still delivered as normal block replies (separate posts) rather than a single coalesced final post.
Streaming behavior notes
- If the stream cannot be finalized in place (for example the post was deleted mid-stream), OpenClaw falls back to sending a fresh final post so the reply is never lost.
- Thinking-only payloads are suppressed from channel posts, including text that arrives as a
> Thinkingblockquote. Set/reasoning onto see thinking in other surfaces; the Mattermost final post keeps the answer only. - See Streaming for the channel-mapping matrix.
Read channel history (message tool)
Use message action=read or the CLI to read posts from a channel that the configured Mattermost bot can access:
openclaw message read --channel mattermost --target channel:<channelId> --limit 5 --json
- The returned results mirror Mattermost's ordered post sequence and include the normalized
timestampMsandtimestampUtcfields. limitis preset to 60, with a ceiling at Mattermost's 200-post maximum. Pagination can be handled through eitherbefore=<postId>orafter=<postId>, but combining both cursors is not allowed.- When operators call directly, Mattermost's channel membership and the
read_channelpermission are what govern access. A provider 403 surfaces as an ordinary, visible tool error. - For delegated reads, the current account can access the active Mattermost conversation. To read across channels, you must supply the destination channel ID under
channels.mattermost.groups, a"*"groups entry, orgroupPolicy: "open". Reads that span accounts or cross channels in DMs will fail closed. - History reads start out disabled. Turn them on by setting
channels.mattermost.actions.messages: true. To change this per account, usechannels.mattermost.accounts.<id>.actions.messages.
Reactions (message tool)
- Combine
message action=reactwithchannel=mattermost. - The Mattermost post id is what
messageIdholds. - Names like
thumbsupor:+1:are accepted byemoji(colons are optional). - To remove a reaction, set
remove=trueas a boolean. - Reaction additions and removals are passed along as system events to the routed agent session, and they go through the same DM/group policy checks that messages do.
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
Config:
channels.mattermost.actions.reactions: toggles reaction actions on or off (default is true).- Override on a per-account basis:
channels.mattermost.accounts.<id>.actions.reactions.
Interactive buttons (message tool)
Send messages that include clickable buttons. When someone clicks one, the agent gets the selection and can reply.
Buttons are derived from the semantic presentation payload (both in standard agent replies and within message action=send). OpenClaw turns value buttons into Mattermost interactive buttons, leaves URL buttons as visible text in the message, and converts select menus into readable text.
message action=send channel=mattermost target=channel:<channelId> presentation={"blocks":[{"type":"buttons","buttons":[{"label":"Yes","value":"yes"},{"label":"No","value":"no"}]}]}
Fields for presentation buttons:
-
label(string, required), The label shown to users (also known astext). -
value(string), The value returned on click, which serves as the action ID (alsocallback_dataorcallbackData). A clickable button needs this unlessurlis provided. -
url(string), A link button; it appears aslabel: urltext within the message body rather than as an interactive button. -
style(primary" | "secondary" | "success" | "danger), Determines the button's style. Unsupported values get default styling from Mattermost.
To advertise button support in the agent system prompt, append inlineButtons to the channel capabilities:
{
channels: {
mattermost: {
capabilities: ["inlineButtons"],
},
},
}
When a button is clicked:
Access check
The person clicking must satisfy the same DM/group policy checks as someone sending a message; unauthorized clicks receive an ephemeral notice and are disregarded.
Buttons replaced with confirmation
Every button gets swapped out for a confirmation line (for instance, "✓ Yes selected by @user").
Agent receives the selection
The agent receives the selection as an inbound message (along with a system event) and then responds.
Implementation notes
- Callbacks for buttons are verified with HMAC-SHA256 (automatic, no setup required).
- Clicking replaces the entire attachment block, so all buttons vanish together; removing just some of them is not possible.
- Hyphens and underscores in action IDs are cleaned up automatically (a Mattermost routing constraint).
- Clicks where
action_iddoes not correspond to an action on the original post are refused with403("Unknown action").
Config and reachability
channels.mattermost.capabilities: a list of capability strings. To include the buttons tool description in the agent system prompt, add"inlineButtons".channels.mattermost.interactions.callbackBaseUrl: an optional external base URL for button callbacks, such ashttps://gateway.example.com. This is useful when Mattermost cannot access the gateway directly via its bind host.- In multi-account configurations, the same field can be set under
channels.mattermost.accounts.<id>.interactions.callbackBaseUrl. - When
interactions.callbackBaseUrlis not provided, OpenClaw constructs the callback URL fromgateway.customBindHostcombined withgateway.port(defaulting to 18789), and then falls back tohttp://localhost:<port>. The callback path is/mattermost/interactions/<accountId>. - Reachability requirement: the Mattermost server must be able to reach the button callback URL.
localhostis only effective when both Mattermost and OpenClaw operate on the same host or network namespace. channels.mattermost.interactions.allowedSourceIps: an allowlist of source IPs for button callbacks. In its absence, only loopback addresses (127.0.0.1,::1) are permitted, so a remote Mattermost server must be added here or its clicks will be denied with403. When a reverse proxy is involved, setgateway.trustedProxiesas well so the actual client IP is extracted from forwarded headers.- If the callback destination is private, on a tailnet, or internal, add its hostname or domain to Mattermost's
ServiceSettings.AllowedUntrustedInternalConnections.
Direct API integration (external scripts)
External scripts and webhooks can send buttons directly through the Mattermost REST API rather than relying on the agent's message tool. OpenClaw's message tool is the preferred approach. For direct integrations, import buildButtonAttachments from @openclaw/mattermost/api.js; when posting raw JSON, adhere to these guidelines:
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
action: "approve",
// ... any custom fields ...
_token: "<hmac>", // see HMAC section below
},
},
},
],
},
],
},
}
Warning
Critical rules
- Place attachments in
props.attachments, not at the top level underattachments(those are silently discarded).- Each action requires
type: "button"; without it, clicks are ignored without any notice.- Every action must include an
idfield, since Mattermost disregards actions lacking IDs.- Action
idvalues must be alphanumeric only ([a-zA-Z0-9]). Hyphens and underscores cause Mattermost's server-side action routing to fail, returning 404. Remove them beforehand.context.action_idneeds to correspond to the button'sid; the gateway rejects clicks whoseaction_idis absent from the post.context.action_idis mandatory, as the interaction handler returns 400 when it is missing.- The callback's source IP must be permitted (refer to
interactions.allowedSourceIpsabove).
HMAC token generation
The gateway authenticates button clicks using HMAC-SHA256. External scripts must produce tokens that align with the gateway's verification process:
Derive the secret from the bot token
HMAC-SHA256(key="openclaw-mattermost-interactions", data=botToken), encoded as hex.
Build the context object
Construct the context object with every field excluding _token.
Serialize with sorted keys
Serialize using recursively sorted keys and no whitespace (the gateway also canonicalizes nested objects and emits compact JSON).
Sign the payload
HMAC-SHA256(key=secret, data=serializedContext)
Add the token
Include the resulting hex digest as _token within 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
- Python's
json.dumpsinserts spaces by default ({"key": "val"}). To match JavaScript's compact output ({"key":"val"}), useseparators=(",", ":"). - Always sign all context fields (minus
_token). The gateway removes_tokenand then signs everything that remains. Signing only a portion results in silent verification failures. - Use
sort_keys=True, because the gateway sorts keys before signing and Mattermost may reorder context fields when storing the payload. - Derive the secret deterministically from the bot token, not from random bytes. The same secret must be used by both the process creating buttons and the gateway performing verification.
Directory adapter
A directory adapter is included in the Mattermost plugin, resolving channel and user names through the Mattermost API. This allows #channel-name and @username targets in openclaw message send as well as cron and webhook deliveries.
No setup is required, since the adapter relies on the bot token from the account configuration.
Multi-account
Multiple accounts are supported 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" },
},
},
},
}
Account-level values take precedence over top-level ones; channels.mattermost.defaultAccount determines which account is used when none is explicitly specified.
Troubleshooting
No replies in channels
The bot needs to be present in the channel before you can interact with it. You can either mention it directly with (oncall), rely on a trigger prefix like (onchar), or configure chatmode: "onmessage".
Auth or multi-account errors
- Confirm the bot token is valid, the base URL is correct, and the account itself is active.
- When running multiple accounts, remember that environment variables only affect the
defaultaccount. - If your Mattermost instance lives on a private or LAN network, you must set
network.dangerouslyAllowPrivateNetwork: true, since the SSRF guard blocks private IP addresses by default.
Native slash commands fail
Unauthorized: invalid command token.: OpenClaw rejected the callback token. This usually happens for one of these reasons:- the slash command registration failed or only partially completed during startup
- the callback is directed at the wrong gateway or account
- Mattermost still holds old command definitions pointing to a previous callback destination
- the gateway restarted without re-registering slash commands
- When native slash commands stop responding, inspect the logs for
mattermost: failed to register slash commandsormattermost: native slash commands enabled but no commands could be registered. - If
callbackUrlis missing and the logs warn that the callback resolved to a loopback URL likehttp://localhost:18789/..., that address is only reachable when Mattermost shares the same host or network namespace as OpenClaw. Provide an explicit externally reachablecommands.callbackUrlin that case.
Buttons issues
- Buttons show up as white boxes or are missing entirely: the button payload is malformed. Every presentation button must carry a
labeland avalue; buttons lacking either one are discarded. - Buttons render but clicks have no effect: make sure the gateway is reachable from the Mattermost server, that the Mattermost server IP appears in
channels.mattermost.interactions.allowedSourceIps(only loopback is allowed without it), and thatServiceSettings.AllowedUntrustedInternalConnectionscontains the callback host for private targets. - Buttons return 404 when clicked: the button
idprobably includes hyphens or underscores. Mattermost's action router cannot handle non-alphanumeric IDs. Stick to[a-zA-Z0-9]only. - Gateway logs
rejected callback source: the click originated from an IP not listed ininteractions.allowedSourceIps. Add the Mattermost server or your ingress to the allowlist, and configuregateway.trustedProxieswhen a reverse proxy is in front. - Gateway logs
invalid _token: the HMAC check failed. Verify that every context field is signed (not just a subset), that keys are sorted, and that the JSON is compact with no spaces. Refer to the HMAC section above. - Gateway logs
missing _token in context: the_tokenfield is absent from the button's context. Make sure it is included when constructing the integration payload. - Gateway rejects the click with
Unknown action:context.action_iddoes not correspond to any actionidon the post. Align both to the same sanitized value. - Agent never offers buttons: add
capabilities: ["inlineButtons"]to the Mattermost channel configuration.
Related
- Channel Routing - how messages are routed per session
- Channels Overview - every channel type that is supported
- Groups - group chat behavior and mention gating
- Pairing - DM authentication and the pairing flow
- Security - the access model and hardening measures