Embeds and Replies
Introduction
Section titled “Introduction”Every bot needs to talk to its users, and in Discord that usually means embeds. They’re the rich, colorful message blocks that make your bot look polished instead of like it’s shouting plain text into the void.
Warden gives you a small but powerful set of tools for this: preset embeds with sensible defaults, a reply() helper that removes the guesswork of when to use reply vs editReply vs followUp, and a confirm() one-liner for yes/no prompts. Together, they let you focus on what your bot says rather than the mechanics of how it says it.
Preset embeds
Section titled “Preset embeds”The Embed class provides four static methods, each with a preset color and semantic meaning. They all accept a string (the embed description) and return a standard discord.js EmbedBuilder, so you can keep chaining if you need to.
Embed.success()
Section titled “Embed.success()”A green embed for positive outcomes — actions completed, tasks finished, things working as expected.
import {Embed} from '@warden/core';
Embed.success('Member has been banned successfully.');Use this when a moderation action went through, a setting was saved, or anything else your bot did worked.
Embed.error()
Section titled “Embed.error()”A red embed for failures — something went wrong, an action couldn’t be completed, or the user made an invalid request.
Embed.error('Could not ban this member. They have a higher role than me.');Reserve this for actual errors and hard failures. For things that are merely cautionary, use .warning() instead.
Embed.info()
Section titled “Embed.info()”A blue embed for neutral information — status updates, instructions, data displays.
Embed.info('This member has 3 active warnings and 1 previous mute.');This is your everyday workhorse. Any time you’re presenting information without a strong positive or negative connotation, .info() is the right choice.
Embed.warning()
Section titled “Embed.warning()”A yellow/amber embed for caution — something the user should be aware of, a potentially destructive action about to happen, or a soft validation issue.
Embed.warning('This will ban the member and delete 7 days of their messages. Are you sure?');Warnings sit between info and error. The action isn’t broken, but the user should pay attention before proceeding.
Chaining with EmbedBuilder
Section titled “Chaining with EmbedBuilder”Every preset method returns a standard EmbedBuilder from discord.js. This means you can chain any EmbedBuilder method directly:
Embed.success('Member banned.') .setTitle('Moderation Action') .setFields( { name: 'Target', value: target.user.tag, inline: true }, { name: 'Moderator', value: interaction.user.tag, inline: true }, { name: 'Reason', value: reason }, ) .setTimestamp() .setThumbnail(target.user.displayAvatarURL());The preset just gives you a sensible starting point — color and description already set. Everything else is up to you.
The reply() helper
Section titled “The reply() helper”Discord interactions have a lifecycle. You can reply() to them once, then editReply() to change that response, or followUp() to send additional messages. Getting the timing wrong leads to “Interaction has already been replied to” errors or, worse, silently dropped messages.
Warden’s reply() helper makes this a non-issue. It automatically picks the right method based on the interaction’s current state:
| Interaction State | What reply() Does |
|---|---|
| Not yet replied to | Calls interaction.reply() |
| Already replied or deferred | Calls interaction.editReply() |
| Needs a follow-up | Calls interaction.followUp() |
import {reply, Embed} from '@warden/core';
// First call -- sends the initial replyawait reply(interaction, Embed.info('Processing your request...'));
// Later in the same execute() -- edits the replyawait reply(interaction, Embed.success('Done! Member has been muted for 1 hour.'));You can pass reply() an embed, a string, or a full message options object with embeds, components, and other fields:
// Just an embedawait reply(interaction, Embed.success('Banned.'));
// Full message payloadawait reply(interaction, { embeds: [Embed.warning('Are you sure?')], components: [Row(Btn.danger('Yes', 'confirm'))], ephemeral: true,});Ephemeral replies
Section titled “Ephemeral replies”Ephemeral messages are only visible to the user who triggered the interaction. They’re perfect for error messages, permission failures, or any response that shouldn’t clutter the channel:
await reply(interaction, { embeds: [Embed.error('You do not have permission to use this command.')], ephemeral: true,});The confirm() dialog
Section titled “The confirm() dialog”A surprisingly large number of moderation flows boil down to “Are you sure?” Warden’s confirm() function handles this in a single line. It sends a confirmation prompt with “Confirm” and “Cancel” buttons and returns a boolean:
import {confirm, Embed, reply} from '@warden/core';
const confirmed = await confirm(interaction, 'Are you sure you want to ban this member?');
if (confirmed) { // Proceed with the ban await reply(interaction, Embed.success('Member has been banned.'));} else { await reply(interaction, Embed.info('Action cancelled.'));}That’s the entire flow. No manual button creation, no collector setup, no timeout handling. confirm() takes care of everything and gives you back true or false.
If the user doesn’t click anything before the timeout expires, confirm() returns false — treating inaction as cancellation, which is the safe default for destructive operations.
You can also pass an embed instead of a plain string for a richer prompt:
const confirmed = await confirm(interaction, { embeds: [ Embed.warning('This will permanently ban **UserTag** and delete 7 days of messages.'), ],});Practical examples
Section titled “Practical examples”Moderation action embed with full details
Section titled “Moderation action embed with full details”When a moderation action is taken, you typically want to show more than just “Done.” Here’s a rich ban notification:
import {command, Command, permissions, CommandInteraction, Params, Embed, reply} from '@warden/core';
@command({ name: 'ban', description: 'Ban a member from the server' })@permissions({ user: ['BanMembers'], bot: ['BanMembers'] })export default class BanCommand implements Command { public async execute(interaction: CommandInteraction, params: Params): Promise<void> { const target = interaction.options.getUser('user')!; const reason = interaction.options.getString('reason') ?? 'No reason provided'; const days = interaction.options.getInteger('days') ?? 0;
await interaction.guild!.members.ban(target, { deleteMessageSeconds: days * 86400, reason });
const embed = Embed.success(`**${target.tag}** has been banned.`) .setTitle('Member Banned') .setThumbnail(target.displayAvatarURL()) .setFields( { name: 'Member', value: `${target.tag} (${target.id})`, inline: true }, { name: 'Moderator', value: interaction.user.tag, inline: true }, { name: 'Reason', value: reason }, { name: 'Messages Deleted', value: `${days} day(s)`, inline: true }, ) .setTimestamp();
await reply(interaction, embed); }}Ephemeral error messages
Section titled “Ephemeral error messages”Error messages should almost always be ephemeral. They’re relevant only to the person who triggered the interaction, and showing them publicly can be noisy or embarrassing:
import {command, Command, CommandInteraction, Params, Embed, reply} from '@warden/core';
@command({ name: 'warn', description: 'Warn a member' })export default class WarnCommand implements Command { public async execute(interaction: CommandInteraction, params: Params): Promise<void> { const target = interaction.options.getUser('user')!;
if (target.bot) { await reply(interaction, { embeds: [Embed.error('You cannot warn a bot.')], ephemeral: true, }); return; }
if (target.id === interaction.user.id) { await reply(interaction, { embeds: [Embed.error('You cannot warn yourself.')], ephemeral: true, }); return; }
// ... issue the warning ...
await reply(interaction, Embed.success(`**${target.tag}** has been warned.`)); }}Notice how the error cases return early with ephemeral messages, but the success case is public so the channel knows the action was taken.
Ban confirmation with confirm()
Section titled “Ban confirmation with confirm()”Here’s the cleanest possible ban-with-confirmation flow:
import {command, Command, permissions, CommandInteraction, Params, Embed, reply, confirm} from '@warden/core';
@command({ name: 'ban', description: 'Ban a member from the server' })@permissions({ user: ['BanMembers'], bot: ['BanMembers'] })export default class BanCommand implements Command { public async execute(interaction: CommandInteraction, params: Params): Promise<void> { const target = interaction.options.getUser('user')!; const reason = interaction.options.getString('reason') ?? 'No reason provided';
const confirmed = await confirm( interaction, `Are you sure you want to ban **${target.tag}**?\nReason: ${reason}`, );
if (!confirmed) { await reply(interaction, Embed.info('Ban cancelled.')); return; }
await interaction.guild!.members.ban(target, { reason }); await reply(interaction, Embed.success(`**${target.tag}** has been banned. Reason: ${reason}`)); }}Three lines for the confirmation flow: ask, check, act. Everything else — the buttons, the collector, the timeout, the cleanup — is handled for you.
Logging to a moderator channel
Section titled “Logging to a moderator channel”A common pattern is to reply to the moderator in the command channel and also log the action to a dedicated moderator log channel. Since Embed returns a standard EmbedBuilder, you can reuse the same embed in both places:
import {command, Command, permissions, CommandInteraction, Params, Embed, reply, Config} from '@warden/core';
@command({ name: 'mute', description: 'Timeout a member' })@permissions({ user: ['ModerateMembers'], bot: ['ModerateMembers'] })export default class MuteCommand implements Command { constructor(private config: Config) {}
public async execute(interaction: CommandInteraction, params: Params): Promise<void> { const target = interaction.options.getUser('user')!; const duration = interaction.options.getInteger('duration')!; const reason = interaction.options.getString('reason') ?? 'No reason provided';
const member = await interaction.guild!.members.fetch(target.id); await member.timeout(duration * 60_000, reason);
// Reply to the moderator await reply(interaction, Embed.success(`**${target.tag}** has been muted for ${duration} minutes.`));
// Log to the mod channel const logChannelId = this.config.get('moderation.logChannel'); const logChannel = interaction.guild!.channels.cache.get(logChannelId);
if (logChannel?.isTextBased()) { const logEmbed = Embed.info(`**${target.tag}** was muted.`) .setTitle('Moderation Log: Mute') .setFields( { name: 'Member', value: `<@${target.id}>`, inline: true }, { name: 'Moderator', value: `<@${interaction.user.id}>`, inline: true }, { name: 'Duration', value: `${duration} minutes`, inline: true }, { name: 'Reason', value: reason }, ) .setTimestamp();
await logChannel.send({ embeds: [logEmbed] }); } }}The reply in the command channel is a quick success message. The log channel gets a more detailed embed with all the context a moderator reviewing logs later would need.