---
title: "Bot Messages - mssgs | Embeds, Buttons & Live-updating Cards"
description: "mssgs bot message documentation. Rich embed cards with status badges and diff stats, loader and thinking states, interactive buttons with trigger chains."
canonical: https://mss.gs/en/docs/bots
language: en
---

# 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](https://mss.gs/en/docs). 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.

```json
{
  "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
  }
}
```

#### alex opened pull request #144

Resolve outlying hamlet to dominant nearby city

### 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" |

```json
{
  "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.

```json
{
  "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…"
  }
}
```

marius: how late is the standup?

### Examples

```json
{
  "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
  }
}
```

#### alex pushed to master

Fix reverse-geocode city resolution for outlying hamlets

```json
{
  "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" }
  }
}
```

#### Build passed on master

## 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.

```json
{
  "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"
}
```

#### Message Title

### 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) |

## Build your embed

Edit the fields or the JSON payload. Both stay in sync. Watch the message render exactly as it will in a channel. This is the real webhook body; copy it when it looks right.

### 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

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

### Message Updates

```json
{
  "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

```json
{
  "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 |

```json
{
  "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.

##### A command fires your webhook

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

##### Your reply becomes a card

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

##### A button tap comes back to you

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

##### You update the card in place

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

#### Deploy v2.1.0 to production?

#### Deploy complete

## 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.
