Slash commands, plugins & webhooks
OriginChats has three ways to add behavior beyond plain chat:
- Slash commands are named actions that users run from the message box, such as
/banor/hello. They come from the server, from connected apps, or from plugins. - Webhooks let an outside service post messages to a channel over HTTP.
- Plugins are small JavaScript programs that the server operator installs. They can add slash commands and react to incoming commands.
Slash commands
Section titled “Slash commands”A slash command definition looks like this:
{ "name": "remind", "description": "Set a reminder", "options": [ { "name": "text", "description": "What to remind you about", "type": "str", "required": true }, { "name": "unit", "description": "Time unit", "type": "enum", "choices": ["minutes", "hours"], "required": false } ], "ephemeral": false, "whitelistRoles": null, "blacklistRoles": null, "registeredBy": "reminder-bot"}Each option has a type: str, int, float, bool, enum, or user. A user argument is a username string. An enum needs a non-empty choices list. Options are required unless required is false.
registeredBy says where a command comes from:
registeredBy |
Source | Answered by |
|---|---|---|
originChats |
Built into the server. | The server, straight away. |
| A username | An app connected over WebSocket that sent slash_register. |
That app, with slash_response. |
plugin:<name> |
A plugin. | The plugin’s onSlash function, straight away. |
Finding commands
Section titled “Finding commands”slash_list returns the commands the caller is allowed to use. A command can limit who uses it in three ways:
whitelistRoles: the caller needs at least one of these roles.blacklistRoles: the caller must have none of these roles.permission: the caller needs this server permission. Built-in commands use this. Apps can set it inslash_registertoo. The server enforces it, but leaves it out ofslash_list,slash_add, andslash_remove.
When commands are registered or removed, every connected client receives slash_add or slash_remove with a commands list. These events aren’t filtered by permission.
Running a command
Section titled “Running a command”A user runs a command with slash_call in a channel or thread they can view. The server:
- Finds the command by name. If more than one source registered the same name, the caller must send
usernameto pick one. - Checks the caller’s roles and permission against the command.
- Checks the arguments against the options.
- For Rotur accounts, checks that the call is signed. See sign slash calls.
- Runs the command, or forwards it to the app that registered it.
Replies
Section titled “Replies”A reply becomes a message from the command’s source, with an interaction object that names the command and who ran it.
- Public replies are saved to the channel and broadcast as
message_new. - Ephemeral replies, from commands with
ephemeral: true, haveephemeral: trueon the message. They aren’t saved. A built-in or plugin reply goes only to the connection that ran the command. An app’s reply goes to all of the caller’s connections.
Commands from apps
Section titled “Commands from apps”An app registers its commands with slash_register. The registration belongs to that WebSocket connection. Sending slash_register again replaces the app’s list, and closing the connection removes its commands.
When a user runs an app command:
- The caller gets
{"cmd": "slash_call", "id": "<call_id>", "val": true}to confirm the call was forwarded. - The app’s connection gets a
slash_callevent with the callid,val.command,val.args, the invoker, the channel, and any signing fields. - The app answers once with
slash_response, using the sameid, within five minutes. After that, the call is forgotten and the answer fails withUnknown slash call.
Automod checks app replies before they’re posted.
Built-in commands
Section titled “Built-in commands”The server registers these commands itself. See slash commands for their options and replies.
| Command | What it does | Needs | Reply |
|---|---|---|---|
/ban |
Ban a user, optionally deleting their messages. | manage_users |
Public |
/unban |
Lift a ban. | manage_users |
Public |
/mute |
Time a user out for a number of seconds. | manage_users |
Public |
/unmute |
Clear a user’s timeout. | manage_users |
Public |
/sever |
Hide two users from each other. | manage_users |
Ephemeral |
/unsever |
Undo /sever. |
manage_users |
Ephemeral |
/slow |
Set slow mode in the current channel. | manage_channels |
Public |
/sticky |
Keep a message at the bottom of the current channel. | manage_messages |
Ephemeral |
/nick |
Set or clear your own nickname. | Nothing | Ephemeral |
The moderation commands are explained in moderation, and sticky messages in messages.
Webhooks
Section titled “Webhooks”A webhook is a secret URL that posts messages to one channel. Anything that can send an HTTP request can use it, with no WebSocket connection or account.
Creating a webhook
Section titled “Creating a webhook”Create one with webhook_create for a text or voice channel. All webhook commands need manage_server.
The webhook’s token is its password. Anyone who has it can post. These commands return the token:
webhook_createandwebhook_regenerate.webhook_list, for every webhook it lists.
webhook_get leaves the token out. If a token leaks, regenerate it.
Posting a message
Section titled “Posting a message”Send JSON to /webhooks?token=<token> with Content-Type: application/json:
{ "content": "The build passed.", "username": "Build service", "avatar_url": "https://example.com/ci.png"}A successful post returns HTTP 204. The server saves the message in the webhook’s channel and broadcasts message_new. The message’s user is originChats, and its webhook object holds the display name and avatar. username and avatar_url override the webhook’s own name and avatar for that message.
A post needs content or embeds. text and message are accepted as other names for content. See HTTP endpoints for the full request format, limits, and errors.
Webhook posts go through automod. They don’t get link previews or send push notifications.
GitHub webhooks
Section titled “GitHub webhooks”A webhook URL can also receive GitHub’s push events and turn them into a summary message.
To set one up in GitHub, add a repository webhook with:
- Payload URL:
https://<your-server>/webhooks?token=<token> - Content type:
application/json - Events: “Just the push event.”
The server treats a request as a GitHub event when it has an X-GitHub-Event header and a body with repository and ref. It handles only push events. Each push becomes a message from a webhook named GitHub, with one embed:
{ "title": "main: 2 commits", "url": "https://github.com/example/app/compare/1a2b3c4...5d6e7f8", "color": 2369838, "author": { "name": "mist - example/app", "url": "https://github.com/example/app", "icon_url": "https://avatars.githubusercontent.com/u/1?v=4" }, "description": "[`5d6e7f8`](https://github.com/example/app/commit/5d6e7f8...) Fix login redirect\n[`9a8b7c6`](https://github.com/example/app/commit/9a8b7c6...) Update docs", "timestamp": "2026-09-24T12:00:00Z"}- The title is
<branch>: <n> commits, or<branch> created,<branch> deleted, or<branch> force-pushed. - The description has one line per commit: the short hash linked to the commit, then the first line of the commit message.
- The title links to GitHub’s compare view, or to the repository if there isn’t one.
Plugins
Section titled “Plugins”A plugin is a JavaScript program that runs inside the server. It can add slash commands and watch incoming WebSocket commands. Only users with manage_server can install and manage plugins.
How plugins run
Section titled “How plugins run”Plugin code runs in a QuickJS sandbox with no access to files, the network, or the host. Each call starts a fresh runtime, so plugins can’t keep state in variables between calls. A call has a 2-second time limit and a 1 MiB memory limit. The plugin’s source plus the call’s data must be under 768 KiB.
Plugins are pure functions. The server passes in a JSON object, and the plugin returns a JSON object that tells the server what to do.
Plugin files
Section titled “Plugin files”Each plugin is a folder in the database directory at plugins/<name>/. The name can use 1 to 64 letters, digits, _, and -.
| File | Purpose |
|---|---|
plugin.json |
The manifest. |
main.js |
The code. Must not be empty, and must be under 512 KiB. |
data.json |
The plugin’s key-value store, managed by the server. |
files/ |
Extra files managed with the plugin file commands. Each is limited to 256 KiB. Plugin code can’t read them. |
The folder can be a git checkout, so you can update a plugin with git pull and then plugin_reload.
The manifest
Section titled “The manifest”{ "name": "hello", "description": "Say hello and answer !ping", "version": "0.1.0", "enabled": true, "slash": [ { "name": "hello", "description": "Say hello" } ], "events": ["message_new"]}namemust match the folder name.descriptionandversionmust be non-empty strings.enableddefaults totrue. A disabled plugin stays installed but doesn’t run.slashlists up to 16 slash command definitions, in the same format apps use. They appear inslash_listwithregisteredByset toplugin:<name>.eventslists up to 32 WebSocket command names the plugin wants to see, such asmessage_new.
Answering slash commands with onSlash
Section titled “Answering slash commands with onSlash”When a user runs one of the plugin’s commands, the server calls onSlash with:
{ "plugin": "hello", "command": "hello", "args": {}, "channel": "general", "threadId": "", "user": { "id": "mist", "username": "mist" }, "config": {}, "data": {}}threadId is always an empty string. config holds the plugins.<name> block from the server configuration, and data holds the plugin’s store.
Return { "response": "<text>" } to reply, or { "error": "<text>" } to fail the call with that error. The reply is posted from plugin:<name>.
function onSlash(p) { if (p.command === "hello") { return { response: "hello, " + p.user.username + "!" }; } return { error: "unknown command: " + p.command };}Watching commands with onEvent
Section titled “Watching commands with onEvent”For each incoming WebSocket command named in events, the server calls onEvent before running the command. This happens after the server checks the caller’s command permission and timeout, and before the command’s own checks. The payload has plugin, command, the incoming msg, user, config, and data.
onEvent can return:
| Field | Effect |
|---|---|
msg |
Replaces the incoming command. Later plugins and the server see the new version. |
block: true |
Stops the command. No later plugins run and the server doesn’t handle it. |
responses |
Extra frames sent back to the caller. |
Return {} to let the command through unchanged. If onEvent throws or times out, the server logs it and carries on as if the plugin weren’t there.
function onEvent(e) { var content = e.msg && typeof e.msg.content === "string" ? e.msg.content : ""; if (e.command === "message_new" && content.indexOf("!ping") !== -1) { return { messages: [{ channel: e.msg.channel, content: "pong" }] }; } return {};}Side effects
Section titled “Side effects”Both functions can also return these fields:
| Field | Effect |
|---|---|
dataSet |
An object of keys to save in the plugin’s store. |
dataDelete |
A list of keys to remove from the store. |
messages |
A list of { "channel": "<name>", "content": "<text>" } messages to post. |
Messages are posted as system messages from originChats in the channel itself, not in a thread. The server doesn’t check the triggering user’s permissions for them.
Managing plugins
Section titled “Managing plugins”All of these commands need manage_server and are recorded in the audit log.
| Task | Command |
|---|---|
| List installed plugins | plugin_list, plugin_get |
| Install from a manifest and code, or from a git URL | plugin_install |
| Turn on or off | plugin_enable, plugin_disable |
| Reload from disk | plugin_reload |
| Remove | plugin_uninstall |
Manage files in files/ |
plugin_file_list, plugin_file_get, plugin_file_put |
When a plugin’s commands change, clients get slash_add and slash_remove events:
- Installing, enabling, or reloading one plugin sends
slash_removefor its old commands andslash_addfor its new ones. - Disabling or uninstalling a plugin sends
slash_remove. - Reloading every plugin, with
plugin_reloadand noname, sends onlyslash_remove. Clients should callslash_listagain afterwards.
