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

  • native and nativeSkills default to "auto", which resolves to disabled for Mattermost. Set them to true explicitly.
  • callbackPath defaults to /api/channels/mattermost/command.
  • If callbackUrl is omitted, OpenClaw derives http://<gateway.customBindHost or localhost>:<gateway.port, default 18789><callbackPath>. Wildcard bind hosts (0.0.0.0, ::) fall back to localhost.
  • For multi-account setups, commands can be set at the top level or under channels.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 callbackUrl to localhost unless Mattermost runs on the same host/network namespace as OpenClaw.
  • Do not set callbackUrl to your Mattermost base URL unless that URL reverse-proxies /api/channels/mattermost/command to OpenClaw.
  • A quick check is curl https://<gateway-host>/api/channels/mattermost/command; a GET should return 405 Method Not Allowed from OpenClaw, not 404.

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_URL from a workspace .env is 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.requireMention remains recognized, chatmode is the recommended option. Any per-channel groups.<channelId>.requireMention configuration 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 onchar prefix, 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 on channels.mattermost.accounts.<id>.implicitMentions. Since Mattermost does not currently generate replyToBot or quotedBot facts, 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.
  • all and batched: behave like first for 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 off even when replyToMode is 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, or batched: 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.
  • first and all are 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 a direct override, 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 mattermost
    • openclaw pairing approve mattermost <CODE>
  • For public DMs: channels.mattermost.dmPolicy="open" combined with channels.mattermost.allowFrom=["*"] (the wildcard is enforced by the config schema).
  • channels.mattermost.allowFrom accepts user ids (preferred) as well as accessGroup:<name> entries. See Access groups for more.

Channels (groups)

  • Default: channels.mattermost.groupPolicy = "allowlist" (mention-gated).
  • Use channels.mattermost.groupAllowFrom to allowlist senders (user IDs are recommended).
  • channels.mattermost.groupAllowFrom handles accessGroup:<name> entries. Check Access groups.
  • Per-channel mention overrides are found under channels.mattermost.groups.<channelId>.requireMention or, for a default, channels.mattermost.groups["*"].requireMention.
  • @username matching can change and is active only when channels.mattermost.dangerouslyAllowNameMatching: true.
  • Open channels: channels.mattermost.groupPolicy="open" (mention-gated).
  • Order of resolution: channels.mattermost.groupPolicy, then channels.defaults.groupPolicy, then "allowlist".
  • Runtime note: if the channels.mattermost section is absent entirely, runtime fails closed to groupPolicy="allowlist" for group checks (even when channels.defaults.groupPolicy is 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:

TargetDelivers to
channel:<id>Channel by id
channel:<name> or #channel-nameChannel by name, searched across the teams the bot belongs to
user:<id> or mattermost:<id>DM with that user
@usernameDM (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 429 are 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.
  • block rotates 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.
  • progress shows a status preview while generating and only posts the final answer at completion.
  • off disables preview streaming. With streaming.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 > Thinking blockquote. Set /reasoning on to 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 timestampMs and timestampUtc fields.
  • limit is preset to 60, with a ceiling at Mattermost's 200-post maximum. Pagination can be handled through either before=<postId> or after=<postId>, but combining both cursors is not allowed.
  • When operators call directly, Mattermost's channel membership and the read_channel permission 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, or groupPolicy: "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, use channels.mattermost.accounts.<id>.actions.messages.

Reactions (message tool)

  • Combine message action=react with channel=mattermost.
  • The Mattermost post id is what messageId holds.
  • Names like thumbsup or :+1: are accepted by emoji (colons are optional).
  • To remove a reaction, set remove=true as 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 as text).

  • value (string), The value returned on click, which serves as the action ID (also callback_data or callbackData). A clickable button needs this unless url is provided.

  • url (string), A link button; it appears as label: url text 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_id does not correspond to an action on the original post are refused with 403 ("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 as https://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.callbackBaseUrl is not provided, OpenClaw constructs the callback URL from gateway.customBindHost combined with gateway.port (defaulting to 18789), and then falls back to http://localhost:<port>. The callback path is /mattermost/interactions/<accountId>.
  • Reachability requirement: the Mattermost server must be able to reach the button callback URL. localhost is 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 with 403. When a reverse proxy is involved, set gateway.trustedProxies as 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

  1. Place attachments in props.attachments, not at the top level under attachments (those are silently discarded).
  2. Each action requires type: "button"; without it, clicks are ignored without any notice.
  3. Every action must include an id field, since Mattermost disregards actions lacking IDs.
  4. Action id values 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.
  5. context.action_id needs to correspond to the button's id; the gateway rejects clicks whose action_id is absent from the post.
  6. context.action_id is mandatory, as the interaction handler returns 400 when it is missing.
  7. The callback's source IP must be permitted (refer to interactions.allowedSourceIps above).

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.dumps inserts spaces by default ({"key": "val"}). To match JavaScript's compact output ({"key":"val"}), use separators=(",", ":").
  • Always sign all context fields (minus _token). The gateway removes _token and 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 default account.
  • 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 commands or mattermost: native slash commands enabled but no commands could be registered.
  • If callbackUrl is missing and the logs warn that the callback resolved to a loopback URL like http://localhost:18789/..., that address is only reachable when Mattermost shares the same host or network namespace as OpenClaw. Provide an explicit externally reachable commands.callbackUrl in 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 label and a value; 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 that ServiceSettings.AllowedUntrustedInternalConnections contains the callback host for private targets.
  • Buttons return 404 when clicked: the button id probably 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 in interactions.allowedSourceIps. Add the Mattermost server or your ingress to the allowlist, and configure gateway.trustedProxies when 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 _token field 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_id does not correspond to any action id on the post. Align both to the same sanitized value.
  • Agent never offers buttons: add capabilities: ["inlineButtons"] to the Mattermost channel configuration.
3,887 words · updated Sep 1, 2026