Skip to main content
Developer Guide

Bot Messages

Rich embed cards, interactive buttons and live-updating messages for bots

Overview

Bots post rich messages by sending an action_message. The message_container.type decides how the client renders it:

  • embed_message: a rich card with a colored accent, bot header, clickable title, markdown body, status badge, diff stats, loader and collapsible reasoning. Built for GitHub notifications, CI results, deploys and AI assistants.
  • system_message: a verified system-style card with interactive buttons that run trigger chains.

Where do these fit in?

You send these payloads through the same webhook and trigger integrations described in the API documentation. This guide covers the full message containers: what you can put in a card and how buttons behave.

Embed Messages

An embed renders with a colored left accent, a "Message from {bot} · {badge}" header, a clickable title, a subtitle, and a markdown description.

Message Shape
{
  "type": "action_message",
  "channel_guid": "...",
  "server_guid": "...",
  "message_container": {
    "type": "embed_message",
    "bot_name": "Github Notifications",
    "avatar_url": "https://avatars.githubusercontent.com/u/310687?v=4",
    "badge": "acme/acme-core",
    "color": "purple",
    "title": "alex opened pull request #144",
    "title_url": "https://github.com/acme/acme-core/pull/144",
    "sub_title": "Resolve outlying hamlet to dominant nearby city",
    "description": "A hamlet is the smallest OSM settlement class ...",

    "status": { "label": "Open", "color": "green", "icon": "pull_request" },
    "additions": 86,
    "deletions": 38,
    "files_changed": 2
  }
}
Rendered in the chat
Message from Github Notifications acme/acme-core
alex opened pull request #144 Open
Resolve outlying hamlet to dominant nearby city
A hamlet is the smallest OSM settlement class …
18:45 +86 -38 2 files

Container Fields

Field Type Description
type string Must be "embed_message"
bot_name string Shown in the header: "Message from {bot_name}"
badge string Secondary header chip after the bot name (e.g. acme/acme-core)
avatar_url string Bot/source avatar, overlaid on the system icon. Full URL, or a path prefixed with the static host
color string Left accent color: blue (default), green, purple, red, orange, yellow
title string Bold title line
title_url string If set, the title becomes a link opening this URL
sub_title string Secondary bold line under the title
description string Body text. Supports markdown: bold, inline code, fenced code blocks, quotes and checkboxes
fields array [{ "field": "...", "value": "..." }]: label/value rows with a copy button
image_url / image_base64 string Optional image floated to the right

Long descriptions

The server may truncate a long description. When it does, the message carries message_big_embed_guid and the client shows a Show more button that fetches the full body.

Status Badge & Diff Stats

All optional. The status badge renders inline next to the title; the diff stats render in the footer, right of the timestamp. Provide any combination; omit them all and the embed looks exactly as before. If there is no title, the status badge falls back into the footer group.

Field Type Description
status object | string Colored status pill. A bare string is treated as { "label": <string> }
status.label string Pill text, e.g. "Open", "Merged", "Passing". Required for the pill to render
status.color string green, purple, red, orange, yellow, blue, gray (default gray, or falls back to the container color)
status.icon string Optional leading icon from the curated set below. Unknown/missing → no icon
additions number Lines added → green +86
deletions number Lines removed → red -38
files_changed number Files touched → 2 files chip (1 file when 1)

status.icon values

Value Icon Typical use
pull_requestgit-pull-requestPR opened / open
pull_request_closedgit-pull-request-closedPR closed unmerged
merge / mergedgit-mergePR merged
commitgit-commitPushed commit
issuecircle-dotIssue opened / open
issue_closedcircle-checkIssue closed
checkcircle-checkCI passed / success

Suggested color mapping (GitHub)

Event status.label status.color status.icon
PR openedOpengreenpull_request
PR draftDraftgraypull_request
PR mergedMergedpurplemerged
PR closed unmergedClosedredpull_request_closed
Issue openedOpengreenissue
Issue closedClosedpurpleissue_closed
Commit pushedCommitgraycommit
CI passedPassinggreencheck
CI failedFailingrednone

Loader State

When loader is true the card shows an animated spinner with optional texts: an "AI thinking…" style placeholder while the bot is still working. title and description may be omitted entirely in this state (they still render if present).

Field Type Description
loader boolean true renders the spinner block in place of the description body
loader_text string Primary line next to the spinner, e.g. "Thinking…"
loader_sub_text string Smaller, dimmer secondary line, e.g. "Searching your documents"
Loader Card
{
  "message_container": {
    "type": "embed_message",
    "bot_name": "Assistant",
    "color": "blue",
    "loader": true,
    "loader_text": "Thinking…",
    "loader_sub_text": "Searching your documents"
  }
}
Rendered in the chat
Message from Assistant
Thinking… Searching your documents
18:45

The bot clears the loader by sending an UPDATE_MESSAGE with a replaced message_container that omits loader and carries the final content; the client re-renders the card in place.

Thinking (Collapsible Reasoning)

When thinking is a non-empty string, a collapsed Show thinking toggle row renders below the description. Click expands it client-side. Same markdown support as description. Typical use: the bot posts a loader card, then edits it into the final answer with the model's reasoning tucked behind the toggle.

Answer with Thinking
{
  "message_container": {
    "type": "embed_message",
    "bot_name": "Assistant",
    "color": "blue",
    "sub_title": "marius: how late is the standup?",
    "description": "Standup is at **09:30**.",
    "thinking": "Checked the channel history for the recurring invite…"
  }
}
Rendered in the chat · click the toggle
Message from Assistant
marius: how late is the standup?
Standup is at 09:30.
Checked the channel history for the recurring invite…
18:45

Examples

Commit pushed
{
  "message_container": {
    "type": "embed_message",
    "bot_name": "Github Notifications",
    "badge": "acme/acme-core",
    "color": "gray",
    "title": "alex pushed to master",
    "title_url": "https://github.com/acme/acme-core/commit/1a2b3c4",
    "sub_title": "Fix reverse-geocode city resolution for outlying hamlets",
    "status": { "label": "Commit", "color": "gray", "icon": "commit" },
    "additions": 86,
    "deletions": 38,
    "files_changed": 2
  }
}
Rendered in the chat
Message from Github Notifications acme/acme-core
alex pushed to master Commit
Fix reverse-geocode city resolution for outlying hamlets
18:45 +86 -38 2 files
CI result (no diff stats)
{
  "message_container": {
    "type": "embed_message",
    "bot_name": "CI",
    "badge": "acme/acme-core",
    "color": "green",
    "title": "Build passed on master",
    "status": { "label": "Passing", "color": "green", "icon": "check" }
  }
}
Rendered in the chat
Message from CI acme/acme-core
Build passed on master Passing
18:45

Interactive Action Messages

Action messages carry clickable buttons that trigger multiple sequential actions. They appear with system verification badges and support conditional execution based on previous action results.

Basic Action Message
{
  "type": "action_message",
  "message_container": {
    "type": "system_message",
    "color": "orange",
    "title": "Message Title",
    "description": "Message description"
  },
  "actions": [
    {
      "type": "button",
      "color": "green",
      "text": "Button Text",
      "triggers": [
        {
          "action": "ws:send",
          "payload": {
            "method": "SOME_METHOD",
            "data": "value"
          }
        }
      ]
    }
  ],
  "expire_in_seconds": 300,
  "id": "unique-message-id"
}
Rendered in the chat
Message from mssgs
Message Title
Message description
18:45
Button Text

Required Fields

Field Description
typeMust be "action_message"
message_containerMessage content and styling. Use "system_message" for the verified system badge
actionsArray of button actions
channel_guidTarget channel identifier
server_guidTarget server identifier
idUnique message identifier

Optional Fields

Field Description
expire_in_secondsCountdown timer in seconds
cmsCreation timestamp (defaults to current time)

Visual Styling

Container colors

  • orange: amber warning style
  • green: success / positive action style
  • red: error / destructive action style
  • blue: information / neutral style
  • purple: special / premium style
  • yellow: caution / attention style

Button colors

  • Primary (green, blue, purple): prominent action buttons
  • Secondary (red, orange, yellow): secondary or destructive actions

Trigger System

Buttons execute multiple actions sequentially using the triggers array. Each trigger runs in order, with conditional execution based on the previous trigger's success or failure.

WebSocket Actions

ws:send
{
  "action": "ws:send",
  "payload": {
    "method": "YOUR_METHOD_NAME",
    "param1": "value1",
    "param2": "value2"
  }
}

Message Updates

local:update_message
{
  "action": "local:update_message",
  "message_container": {
    "color": "green",
    "title": "Updated Title",
    "description": "Updated description"
  },
  "actions": []
}

Security

The message type cannot be changed by an update. This prevents impersonation: a bot card can never turn itself into a different kind of message.

Managing buttons on update:

  • Include "actions": [] to remove all buttons after completion
  • Include "actions": [...] to replace buttons with new ones
  • Omit the actions field to keep existing buttons unchanged

Message Removal

local:remove_message
{
  "action": "local:remove_message"
}

Conditional Execution

Triggers can execute conditionally based on the previous trigger's result:

Condition Behavior
noneAlways executes
"previous:success"Execute only if the previous trigger succeeded
"previous:failed"Execute only if the previous trigger failed
"failed"Execute only if the previous trigger failed
Multi-Step Action with Success / Failure Paths
{
  "type": "button",
  "text": "Process Request",
  "color": "green",
  "triggers": [
    {
      "action": "ws:send",
      "payload": {
        "method": "PROCESS_DATA",
        "request_id": "12345"
      }
    },
    {
      "condition": "previous:success",
      "action": "local:update_message",
      "message_container": {
        "color": "green",
        "title": "Processing Complete",
        "description": "Your request was processed successfully"
      }
    },
    {
      "condition": "previous:failed",
      "action": "local:update_message",
      "message_container": {
        "color": "red",
        "title": "Processing Failed",
        "description": "There was an error processing your request"
      }
    }
  ]
}

Expiration Timers

Messages can include countdown timers using expire_in_seconds. The client formats the remaining time automatically:

  • Under 60 seconds: "45 seconds"
  • Under 60 minutes: "5 minutes 30 seconds"
  • Over 60 minutes: "1 hour 15 minutes"
  • Expired: "This message has expired"; expired messages cannot be interacted with

Live Updates

Both message types update in place. The common pattern for AI-style bots:

1

Post a loader card

Send an embed_message with loader: true and a short loader_text.

2

Do the work

Your bot processes the request while the spinner shows.

3

Replace the container

Send UPDATE_MESSAGE with the final message_container: omit loader, add the answer, optionally tuck the reasoning behind thinking.

See it happen

The whole loop in one place: a command fires your webhook, your reply becomes a card, a button tap comes back to you, and your callback update lands in place.

1
mssgs → POST your-webhook
A command fires your webhook

Someone types /deploy. mssgs POSTs the message to your endpoint, including a callback_url for this message.

2
200 ← message_container + actions
Your reply becomes a card

Respond with JSON and the card appears in the channel: title, description, status and buttons.

3
mssgs → POST your-webhook · [Action Triggered]
A button tap comes back to you

The tap fires the trigger to your server again, with a fresh callback_url.

4
PUT {callback_url} → 200
You update the card in place

First a loader while you work, then the final state. Everyone in the channel sees it update live.

# deploys
jasper 18:45 /deploy
Message from Deploy Bot acme/acme-core
Deploy v2.1.0 to production? Ready
All 42 checks passed. Last commit: 1a2b3c4.
18:45
DeployCancel
Message from Deploy Bot acme/acme-core
Deploying… Step 2/3: running migrations
18:45
Message from Deploy Bot acme/acme-core
Deploy complete Live
v2.1.0 is now live on production.
18:45

Best Practices

Use clear button text

Provide descriptive text for buttons to avoid confusion.

Handle failures

Include failure conditions in trigger chains for better UX.

Reasonable timeouts

Use appropriate expire_in_seconds values for time-sensitive actions.

Visual hierarchy

Use colors consistently: green for positive, red for negative.

Button management

Remove buttons with "actions": [] after completion, or replace them to match each workflow state.

Errors & Limits

Error handling

  • Failed triggers stop the execution chain unless a failure condition is met
  • WebSocket errors are logged and treated as failures
  • Message update failures are logged and treated as failures
  • Triggers with failure conditions (previous:failed, failed) execute on errors

Limitations

  • Maximum recommended triggers per button: 10
  • Message updates cannot change the message_container.type field
  • WebSocket payload size limits apply to trigger payloads
  • Expired messages cannot be interacted with

Questions?

We're happy to help you build your bot.

developers@mss.gs