On This Page

Build a Small Bot

Combine commands, arguments, embeds, events, guards, cooldowns and shutdown in one file

This bot fits in one file and combines what most bots need: Commands with an argument, a help command, an embed, a welcome message for new members, a permission check, a cooldown, error reporting and a clean stop. It builds on the quick start, so start there if the SDK is not installed yet

The Complete Bot

Save this as bot.js, or bot.ts for TypeScript, in the quick start's folder. Like the JavaScript and TypeScript quick start bots, it exports its options as bot, so the test that fluxerly init writes for those starters still runs it. A project from --template effect needs the Effect version instead, saved as bot.ts:

small-bot/bot.jssmall-bot/bot.ts
import { builders, defineBot, describeError, format, guards, runBot } from "@neontechspace/fluxerly"

// Optional channel that receives welcome messages
const welcomeChannelId = process.env.WELCOME_CHANNEL_ID

export const bot = defineBot({
    // Handler and command failures arrive here instead of stopping the bot
    onError: (report) => {
        const source = report.command ? `Command ${report.command}` : `Handler ${report.event ?? report.kind}`
        console.error(`${source} failed:\n${describeError(report.error)}`)
    },
    setup: () => {
        if (!welcomeChannelId) console.info("WELCOME_CHANNEL_ID is not set, so the bot sends no welcome messages")
    },
    events: {
        guildMemberAdd: async ({ event: member, client, signal }) => {
import { builders, defineBot, describeError, format, guards, runBot } from "@neontechspace/fluxerly"

// Optional channel that receives welcome messages
const welcomeChannelId = process.env.WELCOME_CHANNEL_ID

export const bot = defineBot({
    // Handler and command failures arrive here instead of stopping the bot
    onError: (report) => {
        const source = report.command ? `Command ${report.command}` : `Handler ${report.event ?? report.kind}`
        console.error(`${source} failed:\n${describeError(report.error)}`)
    },
    setup: () => {
        if (!welcomeChannelId) console.info("WELCOME_CHANNEL_ID is not set, so the bot sends no welcome messages")
    },
    events: {
        guildMemberAdd: async ({ event: member, client, signal }) => {

Run It

Start the bot with the .env file from the quick start, or with the project's start script:

node --env-file=.env bot.jsnode --env-file=.env bot.ts

For welcome messages, add the ID of the channel that should receive them to .env. A channel's ID is the last number in its link, which has the form https://fluxer.app/channels/<community ID>/<channel ID>:

.env
FLUXER_BOT_TOKEN=paste-the-token-here
WELCOME_CHANNEL_ID=paste-the-channel-id-here

Without WELCOME_CHANNEL_ID, the bot prints a note at startup and runs everything else. Then try it in a channel the bot can read and reply to:

  • Send !help to list the commands
  • Send !ping to get Pong!
  • Send !greet Ada Lovelace to get Hello, Ada Lovelace!
  • Send !greet alone to see what is missing and how to use the command
  • Send !announce Movie night on Friday to post an announcement card. Only members who can manage messages may use it
  • A new member joining the community gets a welcome message in the configured channel

Press Ctrl+C to stop the bot

How It Works

As in the quick start, defineBot holds the bot's options: The handlers, the commands and the settings. The last line passes them to runBot together with the token, which connects and runs until the bot stops. That line runs only when Node.js runs the file, so a test can import bot and pass it to createTestBot instead, as Testing a bot shows

Commands

The commands option of runBot turns messages that start with the prefix ! into commands. Each key of the inner commands object is a command name, and description feeds generated help. The reply function answers the message that ran the command, and it accepts a plain string

Each command returns the result of its reply. If the reply fails, the SDK reports that failure to onError, the error hook described below, like a thrown error. The commands guide covers aliases, groups, help pages and more argument types

Generated Help

The help command calls help() from its context, which lists every command with its description and usage. It returns pages of at most 2,000 characters, so each page fits in one message, and this bot needs only the first one

A Typed Argument

The greet command declares one argument, name, as text. With rest: true, it takes everything after the command name, so a name can contain spaces. The router converts the argument before execute runs, and in TypeScript values.name is a string

A command used wrongly gets a short explanation, such as Missing name. Usage: !greet <name...>. The runBot function answers rejected commands this way by default, and onReject: "silent" turns the answers off

Mentions in a reply do not notify anyone unless the message allows it, so !greet @everyone notifies nobody

A Cooldown

The cooldown setting lets each user run greet once every 5 seconds. A second attempt within that time is rejected, and the bot says once how long to wait. Use per: "channel" or per: "guild" to share the limit across a channel or a whole community

The router keeps cooldowns in memory, so they reset when the bot restarts. The commands guide explains how to share cooldowns between processes

A Permission Guard

A guard decides whether a command may run. The announce command uses guards.requirePermissions(["ManageMessages"]), which allows it only for members who have the ManageMessages permission in that channel. It checks the person who sent the command, not the bot, and it denies the command in direct messages

The guard looks up the community, member, roles and channel, reading them from Fluxer when they are not cached. A denied member gets a reply such as Using this command requires these permissions: Manage messages, at most once every 5 seconds. The communities and permissions guide explains how permissions are calculated

An Embed Reply

An embed is a formatted card with a title, text, color and footer. The builders.embed() chain sets its parts, and the finished builder goes into the embeds list of a reply. Building sends nothing, and limits such as the title length are checked when the message is sent. The bot needs the EmbedLinks permission in the channel, which new communities grant to everyone. Without it, Fluxer rejects the whole reply, and the failure reaches onError. The messages guide shows more message options

A Welcome Message

The events option registers a handler for each event name. The guildMemberAdd event arrives when someone joins a community the bot is in, and its handler receives the member as event, the client and a cancellation signal

The handler skips bots. It then reads the welcome channel with client.channels.fetch. Like every request, it returns a Result: Either a success, whose value holds the channel, or a failure, whose error says what went wrong, and isErr() tells them apart. The handler uses the channel to check that it belongs to the community the member joined, so a bot in several communities never announces one community's members in another. Passing signal lets the SDK cancel these requests when the bot stops

The mention in the welcome text notifies the new member because allowedMentions lists them. If the channel read fails, the handler returns the failed Result, which the SDK reports to onError

Error Reporting

The onError function receives every failure that has no caller to return it to: A failed command, event handler or reply. The report names the command or event, and describeError prints the full error with its code, hint and causes, with credentials masked. The bot keeps running after a failure. Without onError, the SDK logs each failure itself

The logging guide shows how to change what the SDK prints

Clean Shutdown

By default, Ctrl+C or a stop request from a process manager stops the bot. The SDK gives running work up to 5 seconds to finish, cancels the rest, closes the connection and finishes its cleanup, and then runBot returns

When the bot could not connect or stopped for another reason, runBot logs that failure once and sets the process exit code to 1, so a shell or process manager sees a failed run. A missing token is reported the same way before the bot connects, and other invalid settings throw an error at once. The reliability guide covers what happens between start and stop

The Effect Version

The same bot with the Effect API returns an Effect from each handler and command. A failed channel read fails the handler's Effect, which the SDK reports to onError. With processSignals: true, Ctrl+C completes the program successfully. A failure that stops the bot is logged and sets a failing exit code, as with the default API

small-bot/bot-effect.ts
import { Effect } from "effect"
import { builders, defineBot, describeError, format, guards, runBot } from "@neontechspace/fluxerly/effect"

// Optional channel that receives welcome messages
const welcomeChannelId = process.env.WELCOME_CHANNEL_ID

export const bot = defineBot({
    // Handler and command failures arrive here instead of stopping the bot
    onError: (report) => {
        const source = report.command ? `Command ${report.command}` : `Handler ${report.event ?? report.kind}`
        return Effect.logError(`${source} failed:\n${describeError(report.error)}`)
    },
    setup: () =>
        welcomeChannelId
            ? Effect.void
            : Effect.logInfo("WELCOME_CHANNEL_ID is not set, so the bot sends no welcome messages"),

All three versions are in the small-bot example folder. The Effect learning path introduces the Effect concepts used here