Discord Bots skill for Codex
Slash commands that always answer in time. Codex builds Discord bots around application commands and components instead of message parsing, defers any interaction that may take longer than three seconds, and registers commands from a separate deploy script. It requests only the gateway intents the features use, verifies HTTP interactions with Ed25519, and develops in a private test guild before anything reaches real servers.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- discord-bots Installs to ~/.agents/skills/discord-bots/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install discord-bots The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project discord-bots Installs to .agents/skills/discord-bots/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/discord-bots.zipunzip -o discord-bots.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) discord-bots PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $discord-bots, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds Discord bots with discord.js v14 or discord.py 2.x around application commands - slash command registration, interaction replies and deferrals, buttons, select menus, modals, autocomplete, gateway intents, permissions and HTTP interaction endpoints with Ed25519 verification. Use when creating a Discord bot, adding slash commands, fixing "The application did not respond" or Unknown interaction errors, choosing gateway intents, or deploying a bot to production.
Use it when
- Starting a Discord bot or adding slash commands, buttons or modals
- Interactions fail with Unknown interaction or no response errors
- Choosing gateway intents or preparing for bot verification
- Running Discord commands from a serverless HTTP endpoint
Use something else when
- The bot is for Slack workspaces: use slack-bots
- The bot is for Telegram chats: use telegram-bots
How to check the result
Every command, button and modal works in a private test guild, a handler with an artificial four-second delay still succeeds through deferReply, and the invite URL lists only needed permissions.
The mistake it prevents
Doing slow work before replying, so the three-second interaction window closes and Discord shows The application did not respond.
Done well means: the bot uses slash commands and components rather than reading message content, every interaction is answered or deferred within 3 seconds, it requests only the intents and permissions it uses, commands are registered by a separate deploy script, and the token never appears in code, logs or git history.
Before writing code
- Read
AGENTS.mdand the package manifest. Check the library and version:discord.js14.x (Node 18+) ordiscord.py2.x (app_commands). Older code usingmessage.contentprefixes ordiscord.jsv12/v13 APIs needs migration, not extension. - Secrets:
DISCORD_TOKEN,DISCORD_CLIENT_ID(application ID),DISCORD_GUILD_IDfor a dev server. If a token was ever committed, tell the user to reset it in the Developer Portal. - Decide the runtime:
| Situation | Choose |
|---|---|
| Bot reacts to events (joins, messages, voice) or keeps state | Gateway bot (long-running process) |
| Only slash commands and components, serverless hosting | HTTP interactions endpoint (no gateway) |
- Sending messages to real servers, mass DMs, or role and moderation actions (kick, ban, delete messages) need explicit user confirmation. Develop in a private test guild.
Installing packages and connecting to Discord need network; in a Codex sandbox, ask for approval, or unit-test handlers with stubbed interaction objects.
Intents and permissions
| Intent | Privileged | Needed for |
|---|---|---|
Guilds | No | Almost everything; slash commands |
GuildMessages | No | Message events (metadata only without MessageContent) |
MessageContent | Yes | Reading message text outside mentions and DMs |
GuildMembers | Yes | Member join/leave, full member lists |
GuildPresences | Yes | Online status, activities |
Privileged intents must be toggled in the Developer Portal; once the bot is in 100 or more
servers it needs verification and approval for them. Design around slash commands so you do not
need MessageContent. Generate the invite URL with scopes bot applications.commands and only
the permissions the commands use.
Project layout (discord.js)
src/
commands/ping.ts # { data: SlashCommandBuilder, execute(interaction) }
index.ts # client, event routing
deploy-commands.ts # registers commands via REST, run on demand
// src/commands/remind.ts
import { SlashCommandBuilder, MessageFlags, type ChatInputCommandInteraction } from "discord.js";
export const data = new SlashCommandBuilder()
.setName("remind")
.setDescription("Set a reminder")
.addStringOption((o) => o.setName("what").setDescription("Reminder text").setRequired(true).setMaxLength(200))
.addIntegerOption((o) => o.setName("minutes").setDescription("Delay").setMinValue(1).setMaxValue(10080).setRequired(true));
export async function execute(interaction: ChatInputCommandInteraction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral }); // buys 15 minutes
const what = interaction.options.getString("what", true);
const minutes = interaction.options.getInteger("minutes", true);
await saveReminder(interaction.user.id, interaction.channelId, what, minutes);
await interaction.editReply(`Reminder set for ${minutes} min from now.`);
}
// src/index.ts
import { Client, Events, GatewayIntentBits } from "discord.js";
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = commands.get(interaction.commandName);
try {
await command?.execute(interaction);
} catch (err) {
console.error(err);
const msg = { content: "Something went wrong.", flags: MessageFlags.Ephemeral } as const;
interaction.deferred || interaction.replied ? await interaction.followUp(msg) : await interaction.reply(msg);
}
});
client.login(process.env.DISCORD_TOKEN);
// src/deploy-commands.ts: guild commands for dev, global for production
import { REST, Routes } from "discord.js";
const rest = new REST().setToken(process.env.DISCORD_TOKEN!);
const body = allCommands.map((c) => c.data.toJSON());
const route = process.env.DISCORD_GUILD_ID
? Routes.applicationGuildCommands(process.env.DISCORD_CLIENT_ID!, process.env.DISCORD_GUILD_ID)
: Routes.applicationCommands(process.env.DISCORD_CLIENT_ID!);
await rest.put(route, { body });
Never register commands on every startup; it wastes rate limit and can wipe commands when the
list is incomplete. PUT replaces the whole set.
discord.py equivalent
import discord
from discord import app_commands
intents = discord.Intents.default()
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
DEV_GUILD = discord.Object(id=int(os.environ["DISCORD_GUILD_ID"]))
@tree.command(name="remind", description="Set a reminder", guild=DEV_GUILD)
@app_commands.describe(what="Reminder text", minutes="Delay in minutes")
async def remind(interaction: discord.Interaction, what: str, minutes: app_commands.Range[int, 1, 10080]):
await interaction.response.defer(ephemeral=True)
await save_reminder(interaction.user.id, what, minutes)
await interaction.followup.send(f"Reminder set for {minutes} min.")
@client.event
async def setup_hook():
await tree.sync(guild=DEV_GUILD) # move to a manual command or script for production
Response rules
| Situation | Do |
|---|---|
| Work under 3 s | reply() |
| Work may exceed 3 s | deferReply() immediately, then editReply() within 15 min |
| Button or select click | update() to edit the original, or deferUpdate() then edit |
| Need a form | showModal() as the first response (cannot follow a defer) |
| Autocomplete | respond() with up to 25 choices within 3 s; no deferral possible |
Component customId is at most 100 characters; encode an action and an ID
(approve:1234), keep larger state in your database. A message holds up to 5 action rows of up
to 5 buttons. Message content is capped at 2,000 characters; split or attach a file beyond that.
HTTP interactions endpoint
Discord signs each request with Ed25519: headers X-Signature-Ed25519 and
X-Signature-Timestamp, message = timestamp + raw body, verified with the application’s public
key. Reply to type 1 (PING) with { "type": 1 }. Use a maintained verifier (for example the
discord-interactions package’s verifyKey) rather than hand-rolling crypto, and verify before
parsing JSON. Discord sends invalid signatures on purpose when you save the URL to check that
you reject them.
Gotchas
- “The application did not respond”: no reply or defer within 3 s, or the handler threw first.
- “Unknown interaction” (10062): replying after 3 s without deferring, or replying twice.
- Ephemeral: use
flags: MessageFlags.Ephemeral; theephemeral: trueoption is deprecated in current discord.js 14 releases. - Global commands can take time to show in clients; develop with guild commands.
- Rate limits are handled by the library’s REST queue; do not wrap calls in your own tight retry loops. Bulk DMs and mass mentions get bots flagged.
- Sharding is required from 2,500 guilds (
ShardingManagerin discord.js,AutoShardedClientin discord.py). - Permission checks: set
setDefaultMemberPermissionson admin commands and still checkinteraction.memberPermissionsserver-side.
Verification
- Run
deploy-commandsagainst the test guild, then invoke each command, button, select and modal; check both success and error paths. - Add an artificial 4 s delay to one handler in a branch and confirm deferral keeps it working.
- For HTTP endpoints, saving the Interactions Endpoint URL in the portal succeeds (Discord’s validation pings pass).
- Confirm the invite URL requests only the listed permissions.
Report template
**Library:** discord.js 14.x, gateway bot, intents: Guilds
**Commands:** /remind (string, integer), /reminders (list, ephemeral), registered via deploy-commands.ts
**Components:** cancel button customId cancel:<reminderId>
**Permissions:** Send Messages, Embed Links (invite URL in README)
**Tested in:** private test guild; production command registration awaiting approval What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Discord Bots"
short_description: "Slash commands that always answer in time"
default_prompt: "$discord-bots Create a Discord bot with a /remind slash command using discord.js" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.