Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added proposals/4391-discord-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
374 changes: 374 additions & 0 deletions proposals/4391-simplified-in-room-bot-commands.md

@tulir tulir Dec 24, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation requirements:

  • Client
  • Bot

@tulir tulir Dec 26, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gomuks/gomuks#675 is mostly complete implementation of client and technically also bot, converted from MSC4332. It doesn't have full support for room/event IDs or unions yet, but mostly it should be there

It assumes most of my suggestions in other comments are applied. Specifically:

  • command string instead of designator array
  • "optional": true instead of "required": false
  • primitive schemas nested in an object with "schema_type": "primitive"
  • arrays only at top level and unions only at top level or in arrays)
  • optionally: list of aliases, default_value for default values

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(all of these suggestions were up-streamed into the MSC)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proper bot implementation: maunium/meowlnir#50

Both meowlnir and parts of gomuks use logic from mautrix/go#446 (gomuks web has its own typescript implementation, gomuks terminal uses mautrix-go)

Original file line number Diff line number Diff line change
@@ -0,0 +1,374 @@
# MSC4391: Simplified in-room bot commands
Comment thread
Gnuxie marked this conversation as resolved.

> [!NOTE]
>
> This proposal is a simplification of
> [MSC4332: In-room bot commands](https://github.com/matrix-org/matrix-spec-proposals/pull/4332)
> that specifies only an object format for bot commands, to be embedded within
> message event content. The proposal does not make any impositions on the
> textual command syntax used by bots or clients. To make use of the MSC,
> clients and bots will have to implement a simple parser for the proposal's
> command invocation protocol, which is similar in nature to
> [JSON-RPC](https://www.jsonrpc.org/specification).

> [!NOTE]
>
> While this MSC expects and anticipates clients will start with text parsing of
> commands, for clients to provide their own syntax, and that TUI clients will
> likely always use text parsing. The MSC is designed specifically so that it is
> possible to support clients that will only provide a full command UI.

## Background

Interaction with matrix bots today is done by sending shell-like command within
the body of a Matrix `m.room.message` e.g.
`!draupnir unban @example:example.com --invite --no-confirm`.

This interface has the following problems:

- Users unfamiliar with command line interfaces have to learn heavily by example
in order to start using bots, which may be providing essential functions
within a matrix room or community.

- No feedback is provided within the clients if a user enters incorrect syntax,
an unexpected argument, or a value of an incorrect type. Additionally, bots
have to explain these failure modes to a range of users.

- The `body` emitted in `m.room.message` content is treated as _plain text
fallback_ or markdown by most matrix clients, preferring to render
`org.matrix.custom.html`. This means there are a number of inconsistencies in
clients, particularly around the fallback for room and user pills, which can
complicate and limit argument parsing
https://github.com/the-draupnir-project/Draupnir/issues/131.

- Long help menus that can make it difficult to find the right command. Many
clients already have a concept of "slash commands" which are
[desirable to reuse](https://github.com/matrix-org/matrix-spec/issues/93) and
[come up occasionally](https://github.com/matrix-org/matrix-spec/issues/2170) -
finding a way to populate this feature with bot-specific details is
beneficial.

- [_prompt_](https://github.com/the-draupnir-project/Draupnir?tab=readme-ov-file#prompt-ux)
and [_button_](https://core.telegram.org/api/bots/buttons) interfaces have to
be implemented with `m.reaction` events.

- _partial commands_ have to be implemented with `m.reaction`
[prompts](https://github.com/the-draupnir-project/Draupnir?tab=readme-ov-file#prompt-ux)
and replies containing an arbitrary payload of suggestions or a default. This
also leads to noisy timeline views when there are multiple partial steps to
entering a complete command (e.g. `!draupnir ban @spam:example.com`:
[demo](https://github.com/the-draupnir-project/Draupnir/blob/main/docs/ban-command-prompt.gif)).

## Proposal

This proposal introduces the following in order to solve the above problems:

- _command description_ state events that can be used by a bot to communicate to
clients the parameters of the command and the types of arguments that those
parameters can accept.

- A JSON representation of a command and the user supplied arguments, which a
client can send to the bot within a matrix event. This is a similar idea to a
JSON-RPC request object.

- While a response format is not specified in this MSC, the MSC is designed to
be forward compatible with a follow up MSC for _partial commands_ and
_prompts_.

The proposal makes no attempt to specify the syntax of commands within an
`m.room.message` body or describe the syntax of those commands. This is
specifically to avoid the fallback version of the command becoming the source of
truth over the JSON representation, which was previously a source of
[compatibility issues](https://github.com/matrix-org/matrix-spec-proposals/pull/4332/files#r2313755345)
in [MSC4332](https://github.com/matrix-org/matrix-spec-proposals/pull/4332).
Advanced bots would be required some to make changes to their parsers.
Additionally the argument and overloading semantics in MSC4332 make _partial
commands_ and _prompts_ difficult to design. However, this proposal may be
extended by another proposal to specify a fallback derived from the JSON
representation that may work for many simple bots.

### Command description

A new state event type is introduced: `m.bot.command_description`. When
presenting command options to users, clients SHOULD use this event to suggest
commands, scoped by the `sender` of the description.

The `content` for such an event fits the following implied schema:

```jsonc
{
"type": "m.bot.command_description",
"sender": "@draupnir:draupnir.space",
// derived from `sha256(command + mxid)`
"state_key": "JBDLR6YMe+72yqsEMi/MVdTmjN3ynPThMz+M7QLATZQ=",
Comment thread
Gnuxie marked this conversation as resolved.
"content": {
// space separated string for commands such as "rooms add"
"command": "ban",
"parameters": [
{
"key": "target_room",
"schema": { "schema_type": "primitive", "type": "room_id" },
"description": {
// Descriptions use m.text from MSC1767 Extensible Events to later support MSC3554-style translations.
// See https://spec.matrix.org/v1.15/client-server-api/#mroomtopic_topiccontentblock
// See https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1767-extensible-events.md
// See https://github.com/matrix-org/matrix-spec-proposals/pull/3554
"m.text": [{ "body": "The room ID" }],
},
},

{
"key": "timeout_seconds",
"schema": { "schema_type": "primitive", "type": "integer" },
"description": { "m.text": [{ "body": "The timeout in seconds" }] },
},

{
"key": "apply_to_policy",
"schema": { "schema_type": "primitive", "type": "boolean" },
"description": {
"m.text": [{ "body": "Whether to apply this to the policy" }],
},
// This argument is not required
"optional": true,
},

{
"key": "target_users",
"schema": {
"schema_type": "array",
"items": { "schema_type": "primitive", "type": "user_id" },
},
"description": { "m.text": [{ "body": "The user ID(s)" }] },
},
],
"description": {
// We also use m.text here for the same reason as the argument descriptions above.
"m.text": [{ "body": "An example command with arguments" }],
},
},
}
```

A client may show the arguments and commands similar to Discord:

![](./images/4332-discord-example.png)

#### Invariants of command descriptions

- A command description with parameters that have duplicate keys is invalid and
the command SHOULD be hidden by clients.

- The position of parameter descriptions in the _parameters_ property that are
_required_ is significant and clients SHOULD use the same order when prompting
for arguments.

- The position of parameter descriptions that are not _required_ is not
significant.

- Command descriptions only specify commands for the sender of the
`m.bot.command_description`. If the `sender` is not currently joined to the
room, the command should be hidden.

- Parameters with an _array_ schema may appear in any place, not just at the end
of the parameter list. This is to provide a ubiquitous way of providing
multiple arguments. Clients that parse command text may need to provide
special syntax for lists or multiple arguments in order to support _required_
parameters that have an array schema.

- The state_key MUST be a padded base64 encoded SHA256 digest of the `command`
property of the command description AND the mxid of the sender. This is in
order to ensure that command descriptions are deduplicated and conflict free
between different bots.

### Command invocation

When the user sends the command, the client creates either an `m.room.message`
or an `m.room.bot.command` event with the following `content` shape:

```jsonc
{
// body is client supplied and may not match at all with the bot's fallback syntax.
// body may be omitted entirely in an `m.room.bot.command`.
"body": "@bot:example.org ban !room:example.org 42 true @alice:example.org @bob:example.org",
"msgtype": "m.text",

// Mentions should always be added, to lower the chances of command conflicts.
// Bots SHOULD look for mentions where possible to avoid accidental activations.
"m.mentions": {
"user_ids": ["@bot:example.org"], // should be a single element array, containing the bot's user ID
// from the `m.bot.commands` state event's `state_key` (or `sender`).
// Note: doesn't include other users which may be referenced by the
// command being sent, such as via `user_id` arguments.
},

// This is a new content block so bots don't *need* to do string unpacking when
// commands are sent this way. Bots may still need to unpack `body` when users
// send commands manually or without client support.
"m.bot.command": {
"command": "ban",
"arguments": {
// These are just the arguments and their user-supplied values.
"target_room": {
// Room IDs are special because they can carry routing information too.
// Object types have a type specifier.
"id": "!room:example.org",
"via": ["second.example.org"], // Optional, but recommended.
"type": "room_id",
},
"timeout_seconds": 42, // integers and booleans use appropriate value types (converted from (probably) strings)
"apply_to_policy": true, // tip: clients can convert user input like "yes" to booleans
"target_users": ["@alice:example.org", "@bob:example.org"],
},
},
}
```

Bots can then respond however they normally would to the command input.

Clients SHOULD be aware that some bots may attempt to create conflicts with the
client's built-in commands (such as `/myroomnick`) or the commands of other
bots. Where conflicts with built-in events exist, clients SHOULD NOT show the
bot's option to the user. Where conflicts with other bots exist, clients SHOULD
show the bot's name/user ID in the autocomplete text. For example, "@Giphy /gif
{search}". Clients MAY wish to always disambiguate commands like this to avoid
future conflicts with built-in commands. From an implementation perspective,
clients might cause their built-in commands to always take precedence over any
bot's commands to avoid users becoming confused.

### Type schema

> [!NOTE]
>
> The reason for providing an _integer_ type instead of a _number_ type to match
> the [JSON value](https://datatracker.ietf.org/doc/html/rfc8259#section-6) is
> because matrix.org's canonical JSON
> [does not support encoding floats](https://spec.matrix.org/v1.15/appendices/#canonical-json).

> [!NOTE]
>
> Bots SHOULD NOT rely on clients to validate or build input with respect to the
> input and should perform their own parsing of command arguments.

The following essential types are the predefined `types` for an argument:

- `string` - An arbitrary string.
- `integer` - An arbitrary whole number. May be negative or zero.
- `boolean` - `true` or `false` literal.

Constrained string types:

- `user_id` - Must be a valid
[user ID](https://spec.matrix.org/v1.15/appendices/#user-identifiers) for the
room version encoded as a string.
- `server_name` - Must be a valid
[server name](https://spec.matrix.org/v1.15/appendices/#server-name).
- `room_alias` - Must be a valid
[room alias](https://spec.matrix.org/v1.15/appendices/#room-aliases).

Room reference types:

- `room_id` - An object with the property `room_id` which must be a valid
[room ID](https://spec.matrix.org/v1.15/appendices/#room-ids). The object
SHOULD also contain a `via` property that contains an array of server names
that the room can be joined via.
- `event_id` - An object with the same properties as `room_id` with the addition
of an `event_id` property that must be a valid
[event ID](https://spec.matrix.org/v1.15/appendices/#event-ids).

When an object is provided as the type schema, the type is a derived from a
schema described by the property `schema_type`:
Comment thread
Gnuxie marked this conversation as resolved.

- The `array` schema type specifies the type of the items with the `items`
property. The following additional restrictions apply to `array`:
- Arrays can only appear at the top level, `items` can only be a schema type
of `union`, `primitive`, and `literal`.
- The `union` schema type specifies the types of the variants with the
`variants` property. Which is an array of type schema. The following
restrictions apply to unions:
- Outside of the top-level, only an array of unions is permitted.
- Nested unions are not permitted as they are unnecessary and should be
flatted.
- A union of arrays is not permitted.
- Therefore `variants` can only be a schema type of `primitive` or `literal`.
- The `literal` schema type specifies a literal value with the `value` property
and the type of the literal value with the `literal_type` property.
Comment thread
Gnuxie marked this conversation as resolved.
- For simplicity only `boolean`, `integer`, and `string` may be provided for
the `value` property. This may be extended in future if need is found.

> [!TIP]
>
> Clients can accept a wider variety of inputs for some types, provided they
> reduce them down to the expected value types when sending the command. For
> example, accepting a room permalink for a `room_id` type, or "yes" in place of
> `true` for a `boolean`.

## Extensions

The following extensions/features are best considered by future MSCs:

- A standardised command response similar to JSON-RPC response object. This
would also enable dynamic and arbitrary prompt flow.

- Specifying a minimum power level required to send a command, to hint to users
that a command may be unavailable to them. This wouldn't be enforced by auth
rules, but clients can stop a lot of the accidental usage if they know the
power level the caller must have.

- Specifying a non-`m.room.message` event template to send instead. This could
be useful if the bot wants to minimize "visible" traffic in the room or has
custom event types it wants to use. In future, being able to specify
extensible event content blocks which should be added to the resulting event
may be a better option. In either case, bots should not be able to cause users
to send state events to prevent bots from tricking users into changing power
levels, join rules, etc.

- Support for non-text-like arguments like images, files, etc.

- Some predefined validation on arguments, like a range for integers or
maximum/minimum length of strings.

## Potential issues

@Gnuxie Gnuxie Mar 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When commands are available across an entire space it is a pita to have to resend the state events everywhere. So maybe it would be nice to be able to only specify the commands in a space or take inspiration from sticker pack MSCs if they haven't encountered the same problem.


- Using state events limits a bot's ability to advertise commands if it isn't
given power to do so.

## Alternatives

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm in the process of adding a "response" mixin in a follow up MSC. It would be really useful to also be able to proxy the commands into a mutual room (like draupnir's command room). See the-draupnir-project/Draupnir#1041 (comment)


- Not using state events would work, but can be tricky to manage. This proposal
fills a gap until proposals which solve the problem space more completely are
written and proven by implementation. Sticky events maybe?

- [MSC4332](https://github.com/matrix-org/matrix-spec-proposals/pull/4332). As
stated throughout this proposal, MSC4332 makes a number of complicated
considerations for command syntax that are not backwards compatible for all
bots. In addition the semantics for variadic and positional arguments as they
appear in the MSC make it difficult to implement _options_, _prompts_ and
_partial commands_
https://github.com/matrix-org/matrix-spec-proposals/pull/4332#discussion_r2313755345.
This MSC uses a JSON object to supply arguments to a command and provides
union and array types that can be used to implement variadic arguments and
various styles of options.

## Security considerations

Mentioned in the proposal, clients should be explicitly aware that bots may try
to create confusion for users and override built-in commands or another bot's
commands. For example, a bot may advertise a `myroomnick` command which leads to
the client's functionality not working as expected. Clients should be taking
measures to minimize this confusion from happening, and always showing the
profile of the sender that registered the command.

## Unstable prefix

While this proposal is not considered stable, implementations should use
`org.matrix.msc4391.command_description` in place of `m.bot.command_description`
and `org.matrix.msc4391.command` in place of `m.bot.command`.

## Dependencies

This proposal has no direct dependencies, but benefits more strongly from the
following Extensible Events MSCs:

- [MSC1767 (accepted)](https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/1767-extensible-events.md)
- [MSC3554: Translatable Text](https://github.com/matrix-org/matrix-spec-proposals/pull/3554)