Step-by-step guide for creating a bot, adding interactions, and running it from the named-bot repository.
This guide shows the full day-one path for a new bot developer:
bots list and bots helpThe goal is practical fluency. By the end, you should be able to build a complete bot from scratch, run it locally, and know where to look when something fails.
⚠️ 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: [...]
defineBot(...) and registers commands, events, and handlers.ctx.discord.* to call Discord APIs; the host handles authentication, rate limits, and reconnections.Bots in this repo are not individual ad hoc scripts. They are named bot implementations under examples/discord-bots/.
A bot usually lives at:
examples/discord-bots/<bot-name>/index.js
If the bot needs helper code, put that in a nearby lib/ directory:
examples/discord-bots/<bot-name>/
index.js
lib/
helpers.js
data.js
The existing examples are good starting points:
ping/ — the richest API showcasepoker/ — game state, help, buttons, and modalsknowledge-base/ — runtime config and docs searchsupport/ — deferred replies and follow-upsmoderation/ — message-triggered workflowsStart every new bot by looking at the current repository inventory.
go run ./cmd/discord-bot bots --bot-repository ./examples/discord-bots list
This tells you the canonical bot names. Those names are what you pass to bots help and bots run.
Then inspect the bot you care about:
go run ./cmd/discord-bot bots --bot-repository ./examples/discord-bots help ping
That command shows the bot’s description, slash commands, events, and any runtime config fields.
The easiest way to succeed is to start with one command and one event.
const { defineBot } = require("discord")
module.exports = defineBot(({ command, event, configure }) => {
configure({
name: "demo",
description: "A minimal Discord JS bot",
category: "examples",
})
command("ping", {
description: "Reply with pong",
}, async () => {
return { content: "pong" }
})
event("ready", async (ctx) => {
ctx.log.info("demo bot ready", {
user: ctx.me && ctx.me.username,
})
})
})
configure(...) makes the bot discoverablecommand(...) creates a slash command users can runevent("ready", ...) proves the gateway connection is aliveOnce this works, you can grow the bot safely.
When a command needs to do work after submission, do not block without acknowledging the interaction. Use the defer/edit pattern.
const { sleep } = require("timer")
command("search", {
description: "Search for a topic",
options: {
query: {
type: "string",
description: "Topic to search for",
required: true,
autocomplete: true,
},
},
}, async (ctx) => {
await ctx.defer({ ephemeral: true })
await ctx.edit({
content: `Searching for ${ctx.args.query}...`,
ephemeral: true,
})
// Simulate work or call an API.
await sleep(2000)
await ctx.edit({
content: `Results for ${ctx.args.query}: Architecture, Testing, Runbooks`,
ephemeral: true,
})
})
This is the right pattern when the user should get immediate feedback, then a delayed result.
The autocomplete handler is separate from the command handler. Add it alongside the command:
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" },
]
})
That gives the user suggestions while typing, then a deferred result after submission.
Prefer the require("ui") builder DSL for Discord UI payloads. It is easier to read than raw component JSON, and the Go host validates the final message, embed, row, button, select, and modal shapes before Discord sees them. For the longer dedicated walkthrough, run discord-bot help go-side-ui-dsl-for-discord-bots.
const ui = require("ui")
command("ping", {
description: "Reply with a rich message",
}, async () => {
return ui.message()
.content("pong")
.ephemeral()
.embed(
ui.embed("Ping panel")
.description("This response was built with the UI DSL.")
.field("Why use it?", "Less raw JSON, better validation", false)
)
.row(
ui.button("ping:panel", "Open panel", "primary"),
ui.select("ping:topic")
.placeholder("Choose a topic")
.option("Architecture", "architecture")
.option("Testing", "testing")
)
.build()
})
component("ping:panel", async () => {
return ui.message()
.ephemeral()
.content("Panel button clicked from JavaScript")
.build()
})
component("ping:topic", async (ctx) => {
const selected = Array.isArray(ctx.values) && ctx.values.length > 0 ? ctx.values[0] : "(none)"
return ui.message()
.ephemeral()
.content(`Selected topic: ${selected}`)
.build()
})
Use them when you want a message to stay interactive after the initial slash command response.
A good mental model is:
ui.message(), ui.embed(), buttons, and selectsYou can return raw Discord-shaped objects when debugging or when you need an escape hatch, but new bot code should use the UI DSL first. Raw payloads make it easier to miss action rows, typo component fields, or accidentally build shapes Discord rejects.
ui.form()Modals are great when you need more than a single slash-command field. Use ui.form() instead of hand-building action rows and text inputs.
const ui = require("ui")
command("feedback", {
description: "Open a feedback modal",
}, async (ctx) => {
await ctx.showModal(
ui.form("feedback:submit", "Feedback")
.text("summary", "Summary")
.required()
.min(5)
.max(100)
.textarea("details", "Details")
.max(500)
.build()
)
})
modal("feedback:submit", async (ctx) => {
return ui.message()
.ephemeral()
.content(`Thanks for the feedback: ${ctx.values.summary}\nDetails: ${ctx.values.details || "(none)"}`)
.build()
})
Use a modal when you want a structured form with multiple text inputs. It is much better than stuffing long text into a single slash-command option.
The customId values you pass to .text(customId, label) and .textarea(customId, label) become keys in ctx.values, so keep them short and stable.
Some bots need a few values at startup, but those values are not Discord command arguments. Use configure({ run: { fields: ... }}).
configure({
name: "knowledge-base",
description: "Search and summarize internal docs from JavaScript",
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,
},
},
},
})
Then read those values in JavaScript with ctx.config:
const indexPath = ctx.config && ctx.config.index_path || "builtin-docs"
For each field:
For example:
index_path becomes --index-pathread_only becomes --read-onlyrequire("database")Use require("database") (or require("db")) when your bot needs state that survives restarts — knowledge bases, user records, counters, search indexes. The module exposes a simple SQL interface backed by go-sqlite3.
⚠️ The database module is not pre-configured. Your JS code must call
database.configure(...)once before usingquery(...)orexec(...).
const database = require("database")
// Call this once, typically in the "ready" event or at startup:
database.configure("sqlite3", "./data/bot.sqlite")
// Then use it from any handler:
database.exec(`CREATE TABLE IF NOT EXISTS notes (id TEXT PRIMARY KEY, body TEXT)`)
const rows = database.query(`SELECT id, body FROM notes ORDER BY id LIMIT 10`)
| Method | What it does |
|---|---|
database.configure(driver, dsn) | Open a connection. driver is "sqlite3"; dsn is the path or :memory: for a temporary in-memory DB. |
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) and return { success, rowsAffected, lastInsertId }. |
database.close() | Close the connection. Usually not needed for SQLite. |
The db_path runtime config field makes the SQLite path configurable from the CLI:
configure({
name: "my-bot",
run: {
fields: {
"db-path": {
type: "string",
help: "SQLite path for persistent storage",
default: "./data/bot.sqlite",
},
},
},
})
const database = require("database")
const DEFAULT_DB_PATH = "./data/bot.sqlite"
module.exports = defineBot(({ event, command, configure }) => {
event("ready", async (ctx) => {
const dbPath = ctx.config && ctx.config.db_path || DEFAULT_DB_PATH
database.configure("sqlite3", dbPath)
// Initialize schema on first run:
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
Use :memory: when you want a fresh temporary DB per session:
database.configure("sqlite3", ":memory:")
database.exec(`CREATE TABLE ...`)
The DB is wiped when the process exits.
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. The bot:
--db-path (defaulting to ./examples/discord-bots/knowledge-base/data/knowledge.sqlite)database.query(...) to search entries and database.exec(...) to insert, update, and set statusKey files:
index.js — bot definition with __verb__("run", { fields: { "db-path": {...} } }) and event/command registrationslib/store.js — the store factory that calls database.configure(...) and owns all SQL operationslib/capture.js — candidate extraction from messages and modal submissionslib/search.js — ranked search over SQLite rowsThe normal workflow in this repository is:
go run ./cmd/discord-bot bots --bot-repository ./examples/discord-bots ping run --bot-token "$DISCORD_BOT_TOKEN" --application-id "$DISCORD_APPLICATION_ID" --guild-id "$DISCORD_GUILD_ID" --sync-on-start
bots — the named-bot subcommand group--bot-repository ./examples/discord-bots — where the CLI should discover bot scriptsping run — run the bot named ping--bot-token — the Discord bot token--application-id — the Discord application/client ID--guild-id — optional fast sync target for development--sync-on-start — replace the bot’s slash commands before opening the gatewayIf your bot has runtime config fields, add them after the selector:
go run ./cmd/discord-bot bots --bot-repository ./examples/discord-bots knowledge-base run \
--bot-token "$DISCORD_BOT_TOKEN" \
--application-id "$DISCORD_APPLICATION_ID" \
--guild-id "$DISCORD_GUILD_ID" \
--index-path ./docs/local-index \
--read-only \
--sync-on-start
If something behaves strangely, print the resolved bot and runtime config before opening Discord:
go run ./cmd/discord-bot bots --bot-repository ./examples/discord-bots ping run \
--bot-token "$DISCORD_BOT_TOKEN" \
--application-id "$DISCORD_APPLICATION_ID" \
--print-parsed-values
This is useful when you want to confirm:
Once the bot is running and synced, test the behavior in this order:
/ping and confirm the command responds.component(...) handler fires.modal(...) handler receives submitted values./search and confirm you see an immediate private acknowledgement followed by the final result.!pingjs.For the sample bots in this repo, the most useful smoke tests are:
ping — slash command, button, select menu, modal, autocomplete, and outbound opspoker — help flow, game state, rank evaluation, and action adviceknowledge-base — runtime config plus docs searchOnce the minimal bot works, split it into clear pieces.
Recommended structure:
examples/discord-bots/my-bot/
index.js
lib/
helpers.js
search.js
ui.js
A good bot file usually contains:
configure(...)| Problem | Likely cause | Fix |
|---|---|---|
bot selector is required | bots run was called without a bot name | Add run <bot-name> |
bot "x" not found | The bot name does not match bots list | Use the exact name from bots list |
| Slash command sync fails with option ordering errors | Required options were not ordered first in the source data | Declare required options first, then sync again |
| Autocomplete never appears | The option is missing autocomplete: true | Add autocomplete and remove static choices |
| A button click says no handler exists | The customId does not match any component(...) registration | Make the IDs match exactly and keep them unique |
| Modal submit fails | The modal was not registered or the customId changed | Keep the modal customId stable |
ctx.config is empty | No runtime config fields were declared or the flags were omitted | Add configure({ run: ... }) and pass the matching flags |
ctx.defer does nothing useful | The handler deferred but never edited or followed up | Call ctx.edit(...) or ctx.followUp(...) after the work finishes |
| Discord permission errors | The bot token lacks permission in the guild or channel | Check bot permissions and channel access |
The best starting points in this repository are:
examples/discord-bots/ping/index.js — richest API showcaseexamples/discord-bots/poker/index.js — a complete command set with help and modalsexamples/discord-bots/knowledge-base/index.js — durable storage with SQLite, runtime config, and full search workflowTreat them as copyable templates, not just demos.
discord-js-bot-api-reference — API reference for the builder, contexts, payloads, and operationsexamples/discord-bots/README.md — repository command examples and runtime notesexamples/discord-bots/ping/index.js — full JS showcase botexamples/discord-bots/poker/index.js — help-oriented bot with game-state commandsexamples/discord-bots/knowledge-base/lib/store.js — the canonical database store implementation