Sync
Introduction
Section titled “Introduction”Discord needs to be told about your slash commands before users can invoke them. By default Warden handles this for you on startup — the bot boots, scans your project, compares the local command definitions against what Discord has on file, and pushes any differences before it ever connects to the gateway. That’s convenient during development, but in production you’ll often want more control over when the registration happens. That’s where warden sync comes in.
warden sync is a console command (run in your terminal) that performs the Discord registration step without booting the rest of the bot. It boots just enough of the framework to discover your slash commands via scan(), diffs them against Discord’s current registration, and pushes whatever’s changed — no gateway connection, no rate-limit worries, no partial startup. It’s a natural fit for CI/CD pipelines, production deploys, or any situation where you want to treat “publish the commands” as a discrete step instead of something that happens implicitly on every bot start.
Basic usage
Section titled “Basic usage”At its simplest, nothing more than this:
pnpm warden syncWarden scans your project, compares your local slash commands against Discord’s records, and pushes the differences. If nothing has changed, no API calls are made at all:
[info] Syncing commands globally✓ Registering commands with Discord...✓ Sync completeBy default sync scans src/**/*.ts, which is the right choice during local development. In production you’ll usually want to point it at your compiled output with --scan:
pnpm warden sync --scan 'dist/**/*.js'Targeting a guild
Section titled “Targeting a guild”Global slash commands can take up to an hour to propagate through Discord’s CDN — not a fit for local iteration. During development you’ll usually want to register against a specific guild instead, where changes appear instantly. Pass --guild:
pnpm warden sync --guild 987654321[info] Syncing commands to guild 987654321✓ Registering commands with Discord...✓ Sync completeThis does the same thing .registerTo(guildId) does in your bot’s bootstrap file — handy when you want to deploy commands to a dev server without touching your bootstrap, or when your CI pipeline needs to target different guilds per environment.
How it works
Section titled “How it works”Under the hood, warden sync performs the same command diffing that happens on boot — just without connecting to the Discord gateway. The process is:
-
Boot — the framework initializes, processes decorators, resolves the DI container
-
Scan —
scan()discovers all your slash command classes and builds the command tree -
Fetch — the current registrations are retrieved from Discord’s REST API
-
Diff — local commands are compared property-by-property against registered ones
-
Push — only new, changed, or removed commands are sent to Discord
Because it uses the REST API exclusively, the sync is fast and lightweight. It’s safe to run in automated pipelines without worrying about gateway session limits — you could run it on every deploy and it’d still be a no-op when nothing’s changed.
warden sync:clean
Section titled “warden sync:clean”Over time, stale registrations can accumulate in Discord — especially when you’re actively developing and switching between guild-scoped and global registration. The sync:clean console command removes any registrations that no longer have a corresponding class in your codebase:
pnpm warden sync:clean --guild 987654321A typical run looks like this:
✓ Scanning local commands...[info] Found 12 local command(s)✓ Fetching remote commands...┌──────────────────┬─────────────────────┐│ name │ id │├──────────────────┼─────────────────────┤│ old-warn │ 1098765432109876543 ││ test-command │ 1098765432109876544 │└──────────────────┴─────────────────────┘? Delete 2 stale command(s)? (y/N) › yes[████████████████████████████] 100% (2/2)✓ Removed 2 stale command(s)sync:clean always asks for confirmation before deleting anything. That’s the conservative default; in CI you’ll want to skip the prompt by passing --force:
pnpm warden sync:clean --guild 987654321 --forceOr you can take the read-only preview with --dry-run to see what would be deleted without actually touching Discord.
When is this needed?
Section titled “When is this needed?”You’ll typically reach for sync:clean in these scenarios:
Switching from guild to global registration. When you remove .registerTo() from your bootstrap file, commands start registering globally — but the old guild-scoped registrations remain. Users see duplicate commands until you clean up the guild registrations:
# After removing .registerTo(env('DEV_GUILD_ID'))pnpm warden sync:clean --guild 987654321Removing commands from your codebase. If you delete a command class, the regular sync replaces the registration set — but only for the scope you’re syncing. If you switch scopes (guild → global or vice versa), stale entries remain in the old scope until sync:clean removes them.
Renaming commands. When you rename /warn to /caution, sync registers the new name. The old registration stays until it’s explicitly cleaned up in the same scope you’re running against.
Environment variables
Section titled “Environment variables”Both sync and sync:clean read Discord credentials from the environment — the same ones your bot bootstrap uses, so you shouldn’t have to configure anything twice:
| Variable | Purpose |
|---|---|
DISCORD_TOKEN | Bot token used to authenticate REST calls |
DISCORD_APPLICATION_ID | Application ID the commands are registered against |
If either is missing, the command fails fast with a styled error instead of making partial API calls.
CI/CD integration
Section titled “CI/CD integration”The sync console command is designed to fit naturally into deploy pipelines. A typical GitHub Actions setup looks like this:
# GitHub Actions exampledeploy: steps: - name: Install dependencies run: pnpm install
- name: Build run: pnpm build
- name: Sync commands run: pnpm warden sync --scan 'dist/**/*.js' env: DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} DISCORD_APPLICATION_ID: ${{ secrets.DISCORD_APPLICATION_ID }}
- name: Deploy run: # your deployment stepBecause the sync uses command diffing, running it on every deploy is completely safe. If the commands haven’t changed since the last deploy, no API calls are made; if they have, only the differences are pushed.
Separate sync from boot
Section titled “Separate sync from boot”When warden sync runs in CI/CD, you’ll usually want to skip the sync on bot startup so it doesn’t happen twice. Pass {sync: false} to .start():
new Bot() .discover(d => d .interactions('./dist/{commands,buttons,modals,menus}/**/*.js') .events('./dist/events/**/*.js')) .intents(i => i.defaults()) .partials(p => p.defaults()) .start({sync: false});This gives you a clean separation of concerns: the pipeline handles command registration, and the bot just handles runtime execution.
Skipping sync on boot
Section titled “Skipping sync on boot”Zooming out a bit — by default, .start() syncs commands with Discord before connecting to the gateway. This is convenient during development but not always what you want in production.
You can skip it entirely with {sync: false}:
// Commands are NOT synced on bootnew Bot() .discover(d => d .interactions('./dist/{commands,buttons,modals,menus}/**/*.js') .events('./dist/events/**/*.js')) .intents(i => i.defaults()) .partials(p => p.defaults()) .start({sync: false});This is useful when:
- You sync commands in your CI/CD pipeline with
warden sync - You’re running multiple bot instances and only want one of them to sync
- You want the fastest possible startup time
When to use what
Section titled “When to use what”A quick cheat sheet for the most common situations:
| Scenario | Approach |
|---|---|
| Local development | Let .start() sync on boot (the default) |
| Staging environment | warden sync in the deploy pipeline, start({sync: false}) |
| Production | warden sync in the deploy pipeline, start({sync: false}) |
| Cleaning up after renaming | warden sync:clean --guild <id> |
| Switching guild to global | warden sync:clean --guild <id>, then warden sync |
| Multiple bot instances | warden sync once in the pipeline, all instances use start({sync: false}) |