Skip to content

Sync

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.

At its simplest, nothing more than this:

Terminal window
pnpm warden sync

Warden 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 complete

By 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:

Terminal window
pnpm warden sync --scan 'dist/**/*.js'

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:

Terminal window
pnpm warden sync --guild 987654321
[info] Syncing commands to guild 987654321
✓ Registering commands with Discord...
✓ Sync complete

This 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.

Under the hood, warden sync performs the same command diffing that happens on boot — just without connecting to the Discord gateway. The process is:

  1. Boot — the framework initializes, processes decorators, resolves the DI container

  2. Scanscan() discovers all your slash command classes and builds the command tree

  3. Fetch — the current registrations are retrieved from Discord’s REST API

  4. Diff — local commands are compared property-by-property against registered ones

  5. 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.

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:

Terminal window
pnpm warden sync:clean --guild 987654321

A 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:

Terminal window
pnpm warden sync:clean --guild 987654321 --force

Or you can take the read-only preview with --dry-run to see what would be deleted without actually touching Discord.

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:

Terminal window
# After removing .registerTo(env('DEV_GUILD_ID'))
pnpm warden sync:clean --guild 987654321

Removing 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.

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:

VariablePurpose
DISCORD_TOKENBot token used to authenticate REST calls
DISCORD_APPLICATION_IDApplication ID the commands are registered against

If either is missing, the command fails fast with a styled error instead of making partial API calls.

The sync console command is designed to fit naturally into deploy pipelines. A typical GitHub Actions setup looks like this:

# GitHub Actions example
deploy:
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 step

Because 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.

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.

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 boot
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 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

A quick cheat sheet for the most common situations:

ScenarioApproach
Local developmentLet .start() sync on boot (the default)
Staging environmentwarden sync in the deploy pipeline, start({sync: false})
Productionwarden sync in the deploy pipeline, start({sync: false})
Cleaning up after renamingwarden sync:clean --guild <id>
Switching guild to globalwarden sync:clean --guild <id>, then warden sync
Multiple bot instanceswarden sync once in the pipeline, all instances use start({sync: false})