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.
{
"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
}
}
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_request | git-pull-request | PR opened / open |
pull_request_closed | git-pull-request-closed | PR closed unmerged |
merge / merged | git-merge | PR merged |
commit | git-commit | Pushed commit |
issue | circle-dot | Issue opened / open |
issue_closed | circle-check | Issue closed |
check | circle-check | CI passed / success |
Suggested color mapping (GitHub)
| Event | status.label |
status.color |
status.icon |
|---|---|---|---|
| PR opened | Open | green | pull_request |
| PR draft | Draft | gray | pull_request |
| PR merged | Merged | purple | merged |
| PR closed unmerged | Closed | red | pull_request_closed |
| Issue opened | Open | green | issue |
| Issue closed | Closed | purple | issue_closed |
| Commit pushed | Commit | gray | commit |
| CI passed | Passing | green | check |
| CI failed | Failing | red | none |
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" |
{
"message_container": {
"type": "embed_message",
"bot_name": "Assistant",
"color": "blue",
"loader": true,
"loader_text": "Thinking…",
"loader_sub_text": "Searching your documents"
}
}
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.
{
"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…"
}
}
Examples
{
"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
}
}
{
"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" }
}
}
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.
{
"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"
}
Required Fields
| Field | Description |
|---|---|
type | Must be "action_message" |
message_container | Message content and styling. Use "system_message" for the verified system badge |
actions | Array of button actions |
channel_guid | Target channel identifier |
server_guid | Target server identifier |
id | Unique message identifier |
Optional Fields
| Field | Description |
|---|---|
expire_in_seconds | Countdown timer in seconds |
cms | Creation timestamp (defaults to current time) |
Visual Styling
Container colors
orange: amber warning stylegreen: success / positive action stylered: error / destructive action styleblue: information / neutral stylepurple: special / premium styleyellow: 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
{
"action": "ws:send",
"payload": {
"method": "YOUR_METHOD_NAME",
"param1": "value1",
"param2": "value2"
}
}
Message Updates
{
"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
actionsfield to keep existing buttons unchanged
Message Removal
{
"action": "local:remove_message"
}
Conditional Execution
Triggers can execute conditionally based on the previous trigger's result:
| Condition | Behavior |
|---|---|
| none | Always 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 |
{
"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:
Post a loader card
Send an embed_message with loader: true and a short loader_text.
Do the work
Your bot processes the request while the spinner shows.
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.
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.
200 ← message_container + actions
Your reply becomes a card
Respond with JSON and the card appears in the channel: title, description, status and buttons.
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.
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.
1a2b3c4.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.typefield - 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