Reference for the JavaScript bot DSL, handler contexts, payload shapes, and outbound Discord operations.
This repository lets you write Discord bots in JavaScript while the Go host handles Discord connectivity, slash-command sync, event dispatch, and outbound operations. The JavaScript side stays small and expressive:
defineBot(...)The main idea is simple: the bot repository owns the process, but the bot behavior lives in JavaScript.
⚠️ Runtime Environment Bot scripts run inside a Goja JavaScript engine embedded in Go, not Node.js.
- Available modules:
require("discord"),require("timer"),require("database"),require("ui")- Unavailable:
fs,path,http,fetch,process, npm packages, or any Node.js standard library- No file system access from JS. Deliver generated content as Discord file attachments via
ctx.discord.channels.send()withfiles: [...]
A bot repository is a directory tree full of bot scripts. Each script exports one bot definition through require("discord").
A typical repository looks like this:
examples/discord-bots/
ping/index.js
poker/index.js
knowledge-base/index.js
support/index.js
moderation/index.js
The CLI discovers bots by scanning the repository, loading each script, and reading its metadata. That is why the bot name, description, commands, components, modals, and autocomplete registrations all matter: they are not just documentation, they are the contract the host uses to route interactions.
| Helper | Purpose |
|---|---|
defineBot(builderFn) | Create one bot from a builder callback |
configure(options) | Set bot metadata and runtime config fields |
command(name, spec?, handler) | Register a slash command |
userCommand(name, handler) | Register a user context menu command |
messageCommand(name, handler) | Register a message context menu command |
subcommand(rootName, name, spec?, handler) | Register a subcommand handler |
event(name, handler) | Register a gateway/event handler |
component(customId, handler) | Handle button or select-menu interactions |
modal(customId, handler) | Handle modal submissions |
autocomplete(commandName, optionName, handler) | Return autocomplete choices |
require("ui") | Go-side fluent builders for Discord messages, embeds, components, modals, and screen helpers |
require("ui") — Go-side UI DSL buildersrequire("ui") exposes a native builder DSL implemented in Go and surfaced through Goja Proxy traps. This module exists for cases where you want JavaScript to keep the fluent authoring experience, but you want the host to own validation, type checks, and response-shape control.
The most important design rule is that ui returns typed builders, not plain JS objects. Call .build() at the end of a chain to get a typed Discord payload or the host's normalizedResponse fast path.
| Helper | Purpose |
|---|---|
ui.message() | Build a message / interaction response payload |
ui.embed(title?) | Build an embed payload |
ui.card(title?) | Embed-style helper with .meta() for inline key/value fields |
ui.button(customId, label, style?) | Build a button component |
ui.select(customId) | Build a string select menu |
ui.userSelect(customId) | Build a user select menu |
ui.roleSelect(customId) | Build a role select menu |
ui.channelSelect(customId) | Build a channel select menu |
ui.mentionableSelect(customId) | Build a mentionable select menu |
ui.form(customId, title) | Build a modal form payload |
ui.row(...components) | Wrap components in an action row |
ui.pager(prevId, nextId, controls?) | Build previous/next pager buttons |
ui.actions(definitions) | Build a button row from {id, label, style} definitions |
ui.confirm(confirmId, cancelId, options?) | Build a confirmation response with buttons |
ui.ok(content) | Convenience ephemeral success response |
ui.error(content) | Convenience ephemeral error response |
ui.emptyResults(query?) | Convenience empty-results response |
ui.flow(namespace, options?) | Generate stable component IDs and flow helpers |
const ui = require("ui")
return ui.message()
.content("Search results")
.embed(ui.embed("Results").description("Found 3 items"))
.row(ui.button("search:next", "Next", "primary"))
.build()
A few rules matter here:
ui.message().field(...) is an error, and the error should tell you that you probably meant ui.embed().ui.embed() builder to message.embed(...), not a plain object..followUp() when you explicitly want a new message. The DSL allows the host to distinguish between update-in-place and follow-up responses.Modal fields are keyed by customId, so the form builder uses a customId-first API:
ui.form("feedback:submit", "Feedback")
.text("title", "Title")
.textarea("feedback", "Your feedback")
.build()
On submit, those customId values become the keys in ctx.values.
The ui module also includes helpers for stable screen behavior:
ui.flow(namespace) generates deterministic component IDs like namespace:nextui.pager(...) builds pager button rowsui.actions(...) turns small definitions into button rowsui.card(...) is a convenient embed variant for browsable itemsIn the showcase bot, these helpers drive search screens, card galleries, and review flows that update in place instead of spamming the channel with new messages.
defineBot(builderFn)defineBot(...) is the entrypoint every bot script exports.
const { defineBot } = require("discord")
module.exports = defineBot(({ command, event, configure }) => {
configure({
name: "ping",
description: "A minimal example bot",
})
command("ping", { description: "Reply with pong" }, async () => {
return { content: "pong" }
})
})
The builder callback receives the registration helpers you ask for. If you only destructure command and event, those are the only hooks you plan to use. Most bots will ask for at least command, event, and configure.
configure(options)configure(...) stores metadata on the bot and can also describe runtime config fields.
configure({
name: "knowledge-base",
description: "Search and summarize internal docs from JavaScript",
category: "knowledge",
run: {
fields: {
index_path: {
type: "string",
help: "Optional path label for the active docs index",
default: "builtin-docs",
},
read_only: {
type: "bool",
help: "Disable write operations for future mutations",
default: true,
},
},
},
})
The host keeps arbitrary metadata keys. The common ones are:
name — the canonical bot name shown by bots list and bots helpdescription — a human-readable summarycategory — a group label used by your own conventionsrun — runtime configuration schema for bots runEach field under run.fields becomes a CLI flag when you run the bot through bots <bot> run.
Rules to remember:
ctx.confighelp is shown in bots help <bot>ctx.configFor example, the field index_path becomes the flag --index-path and is read in JavaScript as ctx.config.index_path.
Supported runtime field types are:
stringbool / booleanint / integernumber / floatstring_list / string-list / string[]command(name, spec?, handler)command(...) registers a slash command.
command("echo", {
description: "Echo text back",
options: {
text: {
type: "string",
description: "Text to echo back",
required: true,
},
},
}, async (ctx) => {
return {
content: ctx.args.text,
ephemeral: true,
}
})
The runtime reads the spec object and converts it into a Discord application command.
The keys that matter are:
description — the command description shown by Discordoptions — a map or array of option definitionsUse an object map when you want simple, readable declarations. Use an array if you want to preserve an explicit option list.
options: {
query: {
type: "string",
description: "Search query",
required: true,
autocomplete: true,
},
limit: {
type: "integer",
description: "Maximum results",
},
}
The framework supports Discord's application command option types with these JavaScript-friendly names:
| Framework type | Discord API type | Value | What the user sees |
|---|---|---|---|
string | STRING | 3 | Free text input |
int, integer | INTEGER | 4 | Whole number input |
bool, boolean | BOOLEAN | 5 | True/false toggle |
number, float | NUMBER | 10 | Decimal number input |
user | USER | 6 | @mention picker for users |
channel | CHANNEL | 7 | #channel picker |
role | ROLE | 8 | @role picker |
mentionable | MENTIONABLE | 9 | User or role picker |
sub_command | SUB_COMMAND | 1 | Nested command |
sub_command_group | SUB_COMMAND_GROUP | 2 | Nested command group |
Not yet supported: attachment (Discord type 11, value 11) — file upload as a command option.
Does not exist in Discord's API: There are no date, datetime, calendar, time-range, or date-picker option types. If you need time-based selection, use a string (ISO date), integer (relative hours), or a message ID as an anchor point.
| Field | Meaning | Valid for types |
|---|---|---|
type | Option type (see table above) | all |
description | Option description shown in Discord | all |
required | Marks the option as required | all except sub_command / sub_command_group |
autocomplete | Enables autocomplete for that option | string, integer, number |
choices | Static choices for the option | string, integer, number |
minLength | Minimum length for string options | string |
maxLength | Maximum length for string options | string |
minValue | Minimum numeric value | integer, number |
maxValue | Maximum numeric value | integer, number |
channel_types | Restrict channel picker to specific types | channel |
Important rules:
autocomplete: true cannot be combined with static choicesThe handler receives a context object. For slash commands, the most important fields are:
ctx.args — parsed option values keyed by option namectx.options — alias of ctx.argsctx.config — runtime config values from configure({ run: ... })ctx.reply(...) — send the initial responsectx.defer(...) — acknowledge the interaction and finish laterctx.edit(...) — edit the deferred or initial responsectx.followUp(...) — send an additional follow-upctx.showModal(...) — open a modalctx.discord — call Discord operations directlyctx.log — structured logger for this bot contextctx.store — per-runtime in-memory stateIf your command does work that might take longer than Discord likes, call await ctx.defer({ ephemeral: true }), do the work, and then await ctx.edit(...) with the result.
userCommand(name, handler)userCommand(...) registers a user context menu command. Users see it when they right-click a user and choose Apps.
userCommand("Show Avatar", async (ctx) => {
const target = ctx.args.target
if (!target || !target.id) {
return { content: "Could not resolve user", ephemeral: true }
}
const avatarUrl = target.avatar
? `https://cdn.discordapp.com/avatars/${target.id}/${target.avatar}.png?size=512`
: `https://cdn.discordapp.com/embed/avatars/${(parseInt(target.discriminator) || 0) % 5}.png`
return { content: `${target.username}'s avatar: ${avatarUrl}`, ephemeral: true }
})
User commands do not accept a spec or options. Discord ignores description and options for context menu commands.
ctx.args.target — the resolved user object: { id, username, avatar, discriminator, bot }reply, defer, edit, discord, log, etc.) are also availablemessageCommand(name, handler)messageCommand(...) registers a message context menu command. Users see it when they right-click a message and choose Apps.
messageCommand("Quote Message", async (ctx) => {
const target = ctx.args.target
const author = target.author && target.author.username || "unknown"
const content = target.content || "(empty)"
return { content: `> ${content}\n— **${author}**`, ephemeral: true }
})
Message commands do not accept a spec or options.
ctx.args.target — the resolved message object: { id, content, author, guildID, channelID }subcommand(rootName, name, spec?, handler)subcommand(...) registers a handler for a subcommand under a root slash command. Discord represents subcommands as options of type SUB_COMMAND on the root command.
subcommand("fun", "roll", {
description: "Roll a die"
}, async (ctx) => {
const sides = ctx.args.sides || 6
const result = Math.floor(Math.random() * sides) + 1
return { content: `🎲 You rolled a **${result}** (1-${sides})`, ephemeral: true }
})
You must also register the root command with command(...) so Discord knows the full command structure. The root command's spec should declare the subcommand options:
command("fun", {
description: "Fun and games",
options: {
roll: {
type: "sub_command",
description: "Roll a die",
options: {
sides: { type: "integer", description: "Number of sides", default: 6 }
}
}
}
}, async (ctx) => {
// Fallback handler when /fun is called without a subcommand
return { content: "Use /fun roll or /fun coin" }
})
The subcommand(...) registration only creates the handler mapping. The command(...) registration tells Discord what the command looks like.
ctx.args — parsed option values from the subcommand's own optionsctx.command.name — the root command namectx.command.subName — the subcommand nameevent(name, handler)event(...) registers a handler for a gateway event.
The runtime currently exposes these event names:
readyguildCreateguildMemberAddguildMemberUpdateguildMemberRemovemessageCreatemessageUpdatemessageDeletereactionAddreactionRemoveExample:
event("messageCreate", async (ctx) => {
const content = String((ctx.message && ctx.message.content) || "").trim()
if (content === "!pingjs") {
await ctx.reply({ content: "pong from messageCreate" })
}
})
The context depends on the event, but these fields are common:
ctx.message — the current message or message-like payloadctx.before — the previous value for update/delete style eventsctx.user — the user involved in the eventctx.member — the guild member involved in the eventctx.guild — the guild context when availablectx.channel — the channel context when availablectx.reaction — reaction payload for reaction eventsctx.me — the bot’s current user recordctx.discord — outbound Discord operationsctx.reply(...) — send a channel response when the event supports itFor update/delete events, ctx.before is especially useful because it gives you the previous state. For reaction events, ctx.reaction.emoji.name is the easiest way to inspect what was added or removed.
If you do not call ctx.reply(...), the runtime can use the handler’s return value as a response for event-style workflows. In practice, many bots use events for side effects and explicit replies rather than relying on return values, because that keeps the flow easier to follow.
component(customId, handler)component(...) handles button clicks and select-menu interactions by customId.
component("ping:panel", async () => {
return {
content: "Panel button clicked from JavaScript",
ephemeral: true,
}
})
Use it when your command returns a message with buttons or selects and you want those controls to be interactive.
The most useful fields are:
ctx.component.customId — the clicked component IDctx.component.type — button, select, userSelect, roleSelect, mentionableSelect, or channelSelectctx.values — selected values for select menusctx.reply(...) — respond to the component interactionctx.defer(...) — acknowledge the click when you need more timectx.edit(...) — edit the original interaction responsectx.showModal(...) — open a modal from a component clickButtons do not carry values. Select menus do. For a single-select menu, ctx.values is an array that usually contains one string. For multi-select menus, it can contain more.
modal(customId, handler)modal(...) handles modal submissions by customId.
modal("feedback:submit", async (ctx) => {
const summary = ctx.values && ctx.values.summary || "(empty)"
const details = ctx.values && ctx.values.details || "(none)"
return {
content: `Thanks for the feedback: ${summary}\nDetails: ${details}`,
ephemeral: true,
}
})
The key field is:
ctx.values — an object keyed by text input custom IDsFor text inputs, the runtime collects the submitted values into a plain object so you can read them directly by name.
autocomplete(commandName, optionName, handler)autocomplete(...) supplies suggestions while the user is typing a slash-command option.
autocomplete("search", "query", async (ctx) => {
const current = String(ctx.focused && ctx.focused.value || "")
return [
{ name: "Architecture", value: "architecture" },
{ name: "Testing", value: "testing" },
{ name: `Custom: ${current || "query"}`, value: current || "custom" },
]
})
ctx.focused — the currently focused optionctx.args — the parsed option map for the commandctx.command — the command metadatactx.config — runtime config valuesctx.discord — outbound Discord operationsReturn an array of choices. Each choice should have:
namevalueThe runtime will keep at most 25 choices, because that is Discord’s limit.
Most handlers can return either a string or an object. Returning a string becomes the message content. Returning an object lets you build richer replies.
Common response fields are:
contentembedscomponentsfilesallowedMentionsttsephemeralreplyToExample:
return {
content: "Search results",
embeds: [
{
title: "Architecture",
description: "Bot wiring, handlers, and runtime layers.",
color: 0x5865F2,
},
],
ephemeral: true,
}
For slow work, use the defer/edit pattern:
await ctx.defer({ ephemeral: true })
await ctx.edit({ content: "Searching for arch..." })
await sleep(2000)
await ctx.edit({ content: "Results for arch: ..." })
This is the right pattern for commands that need to perform a search, call an API, or otherwise wait before replying.
ctx.storectx.store is a per-runtime in-memory key/value store.
It is useful for lightweight bot state such as counters, current hand state, or temporary caches.
const hits = ctx.store.get("hits", 0)
ctx.store.set("hits", hits + 1)
Available methods:
get(key, defaultValue)set(key, value)delete(key)keys(prefix)namespace(...parts)Important behavior:
namespace(...) to keep per-bot or per-channel state separatectx.logctx.log is a structured logger for the current bot context.
ctx.log.info("js discord bot connected", {
user: ctx.me && ctx.me.username,
script: ctx.metadata && ctx.metadata.scriptPath,
})
Available levels:
infodebugwarnerrorAny field object you pass is merged into the structured log output.
ctx.discordctx.discord exposes outbound Discord operations for when a command or event needs to do more than answer the original interaction. The same namespace is available from command, event, component, modal, and autocomplete handlers.
ctx.discord.guilds| Operation | Purpose |
|---|---|
ctx.discord.guilds.fetch(guildId) | Fetch a guild snapshot |
ctx.discord.roles| Operation | Purpose |
|---|---|
ctx.discord.roles.list(guildId) | List roles in a guild |
ctx.discord.roles.fetch(guildId, roleId) | Fetch one role by ID |
ctx.discord.threads| Operation | Purpose |
|---|---|
ctx.discord.threads.fetch(threadId) | Fetch a thread snapshot |
ctx.discord.threads.join(threadId) | Join a thread |
ctx.discord.threads.leave(threadId) | Leave a thread |
ctx.discord.threads.start(channelId, payload) | Start a thread from a channel or source message |
ctx.discord.channels| Operation | Purpose |
|---|---|
ctx.discord.channels.send(channelId, payload) | Send a normal message to a channel |
ctx.discord.channels.fetch(channelId) | Fetch a channel snapshot |
ctx.discord.channels.setTopic(channelId, topic) | Update a channel topic |
ctx.discord.channels.setSlowmode(channelId, seconds) | Update channel slowmode |
ctx.discord.messages| Operation | Purpose |
|---|---|
ctx.discord.messages.fetch(channelId, messageId) | Fetch one channel message |
ctx.discord.messages.list(channelId, payload) | List recent channel messages with before, after, around, and limit options |
ctx.discord.messages.edit(channelId, messageId, payload) | Edit an existing channel message |
ctx.discord.messages.delete(channelId, messageId) | Delete a channel message |
ctx.discord.messages.react(channelId, messageId, emoji) | Add a reaction to a channel message |
ctx.discord.messages.pin(channelId, messageId) | Pin a message |
ctx.discord.messages.unpin(channelId, messageId) | Unpin a message |
ctx.discord.messages.listPinned(channelId) | List pinned messages in a channel |
ctx.discord.messages.bulkDelete(channelId, messageIds) | Bulk-delete a list of messages |
ctx.discord.members| Operation | Purpose |
|---|---|
ctx.discord.members.fetch(guildId, userId) | Fetch one guild member |
ctx.discord.members.list(guildId, payload) | List guild members with pagination options |
ctx.discord.members.addRole(guildId, userId, roleId) | Add a role to a member |
ctx.discord.members.removeRole(guildId, userId, roleId) | Remove a role from a member |
ctx.discord.members.timeout(guildId, userId, payload) | Set or clear a member timeout |
ctx.discord.members.kick(guildId, userId, payload) | Kick a member |
ctx.discord.members.ban(guildId, userId, payload) | Ban a member |
ctx.discord.members.unban(guildId, userId) | Remove a ban |
The list helpers accept plain objects so you can pass the same shape from a command, a modal, or a helper function.
ctx.discord.messages.list(channelId, { before, after, around, limit })
before, after, or aroundlimit defaults to 25 and is capped at 100ctx.discord.members.list(guildId, { after, limit })
limit defaults to 25 and is capped at 1000ctx.discord.messages.bulkDelete(channelId, messageIds)
{ messageIds: [...] }ctx.discord.members.timeout(guildId, userId, payload)
{ durationSeconds }, { until }, or { clear: true }ctx.discord.members.ban(guildId, userId, payload)
{ reason, deleteMessageDays }To fetch more messages than a single call allows, paginate backwards with before:
async function fetchAllMessages(ctx, channelId, maxMessages) {
const allMessages = []
let lastMessageId = null
const pageSize = 100 // Discord max per request
while (true) {
const options = { limit: pageSize }
if (lastMessageId) {
options.before = lastMessageId
}
const batch = await ctx.discord.messages.list(channelId, options)
if (!batch || batch.length === 0) {
break
}
allMessages.push(...batch)
lastMessageId = batch[batch.length - 1].id
if (maxMessages && allMessages.length >= maxMessages) {
allMessages.splice(maxMessages)
break
}
}
// Discord returns newest first; reverse to chronological order
return allMessages.reverse()
}
Use this pattern when building archive, export, or backfill commands that need the full history of a channel or thread.
The fetch/list helpers return plain JavaScript objects and arrays of objects. They are intentionally easy to inspect and serialize. Common fields include:
id, name, ownerID, description, memberCount, featuresid, guildID, name, position, permissions, mentionable, hoistid, parentID, name, type, archived, locked, autoArchiveDurationid, guildID, name, type, topic, position, rateLimitPerUserid, content, guildID, channelID, authorid, guildId, nick, roles, pending, joinedAtChannel send and interaction response payloads support a shared shape:
content — message textembeds — one or more embedscomponents — buttons and select menusfiles — attachments like { name, content, contentType }allowedMentions — mention policytts — text-to-speech flagreplyTo — message reference for channel messagesFor ctx.discord.channels.send(...), the payload is a normal message payload. For interaction responses, you can also use ephemeral: true to keep the response private.
A file attachment looks like this:
files: [
{
name: "report.txt",
content: "This report was created inside the JS bot runtime.",
},
]
A reply reference looks like this:
replyTo: {
messageId: "orig-1",
channelId: "chan-1",
}
The Discord bot runtime also exposes a small timer module that is useful for demos and deferred workflows.
const { sleep } = require("timer")
await sleep(2000)
Use this for simulated search, background work, or tests that need a visible pause. It is not a Discord API, but it is part of the runtime environment that the example bots use.
require("database")require("database") (or require("db")) is the runtime module for durable data access from JavaScript bots. Use it when state must survive restarts or when the bot owns real application data such as knowledge entries, reviews, or long-lived records.
⚠️ The database module is not pre-configured. Your JS code must call
database.configure(...)once before usingquery(...)orexec(...).
| Method | Description |
|---|---|
database.configure(driver, dsn) | Open a connection. driver is "sqlite3"; dsn is the file path or :memory: for an in-memory DB. Call this once at startup. |
database.query(sql, ...args) | Run a SELECT and return an array of row objects. |
database.exec(sql, ...args) | Run a statement (INSERT, UPDATE, CREATE TABLE, etc.) and return { success, rowsAffected, lastInsertId }. |
database.close() | Close the connection. Usually not needed for SQLite. |
const database = require("database")
// Set up once (in the ready event or at module load time):
database.configure("sqlite3", "./data/bot.sqlite")
database.exec(`CREATE TABLE IF NOT EXISTS notes (id TEXT PRIMARY KEY, body TEXT)`)
// Then use from any handler:
const rows = database.query(`SELECT id, body FROM notes ORDER BY id LIMIT 10`)
Pair require("database") with configure({ run: { fields: { "db-path": {...} } }) so the SQLite path is configurable from the CLI:
const database = require("database")
const DEFAULT_DB_PATH = "./data/bot.sqlite"
module.exports = defineBot(({ event, command, configure }) => {
configure({
name: "my-bot",
run: {
fields: {
"db-path": {
type: "string",
help: "SQLite path for persistent storage",
default: DEFAULT_DB_PATH,
},
},
},
})
event("ready", async (ctx) => {
const dbPath = ctx.config && ctx.config.db_path || DEFAULT_DB_PATH
database.configure("sqlite3", dbPath)
database.exec(`CREATE TABLE IF NOT EXISTS notes (id TEXT PRIMARY KEY, body TEXT)`)
ctx.log.info("database initialized", { path: dbPath })
})
command("notes", {
description: "List recent notes",
}, async () => {
const rows = database.query(`SELECT id, body FROM notes ORDER BY id`)
return { content: `Found ${rows.length} notes`, ephemeral: true }
})
})
Run with a custom path:
discord-bot bots my-bot run --db-path /var/lib/my-bot/storage.sqlite
ctx.store insteadctx.store | require("database") | |
|---|---|---|
| Persists restarts | ❌ | ✅ |
| Survives process restart | ❌ | ✅ |
| Queryable by arbitrary filters | ❌ | ✅ (SQL) |
| Best for | per-session screen state, counters, caches | durable records, search indexes, user data |
The knowledge-base bot (examples/discord-bots/knowledge-base/) is the canonical reference for using require("database") with runtime config, schema migration, seed data, and SQL-based search. Key files:
index.js — bot definition with __verb__("run", { fields: { "db-path": {...} } })lib/store.js — store factory that calls database.configure(...) and owns all SQL operationslib/search.js — ranked search over SQLite rowslib/capture.js — candidate extraction from messages and modal submissionsYou can use one or the other, not both. If you want typed suggestions, keep autocomplete: true and remove choices.
defer() for slow commandsIf a command does real work after submission, call ctx.defer(...) and then edit the response later. That keeps Discord happy and gives the user immediate feedback.
customId in multiple botsCustom IDs must be unique across loaded bots. The host will reject duplicate component IDs, modal IDs, and autocomplete pairs.
ctx.store survives restartsIt does not. If you need durability, store state in a real database — see section 7½ on require("database") for how to set that up.
ctx.discord.channels.send(...) to behave like an ephemeral interaction replyIt is a normal channel message. Use interaction replies when you want ephemeral/private behavior.
| Problem | Likely cause | Fix |
|---|---|---|
bot selector is required when trying to run a bot | The named bot was omitted from the canonical named-bot path | Use discord-bot bots <bot> run |
javascript bot script is required on direct run or sync-commands | No explicit --bot-script or DISCORD_BOT_SCRIPT was provided | Prefer bots <bot> run or pass --bot-script explicitly |
| A slash command never appears in Discord | Commands were not synced after editing the bot | Run with --sync-on-start or use the sync command path |
| A component or modal interaction says no handler exists | The customId does not match the registered component(...) or modal(...) key | Keep customId values stable and unique |
ctx.config is missing expected fields | The bot did not declare configure({ run: ... }) fields or the flags were omitted | Add the run schema and pass the generated flags to bots run |
| A message/channel moderation helper fails | The bot lacks the required channel permissions | Check message-management, channel-management, and read/history permissions for the target channel |
build-and-run-discord-js-bots — step-by-step tutorial for creating and running botsgo-side-ui-dsl-for-discord-bots — tutorial for the Go-backed require("ui") DSL and in-place interaction updatesexamples/discord-bots/ping/index.js — button, select, modal, autocomplete, and outbound ops showcaseexamples/discord-bots/poker/index.js — a richer bot with game state and action adviceexamples/discord-bots/knowledge-base/index.js — runtime config and docs-search exampleexamples/discord-bots/show-space/index.js — venue operations bot with show announcements, DB-backed records, pin cleanup, and debug role-troubleshooting toolsexamples/discord-bots/README.md — repository-level usage notes and command examplesexamples/discord-bots/interaction-types/index.js — demo of all command and interaction types