Events
Listen to level-ups, level-downs, and XP changes with the event system.
The XP plugin emits events after every XP operation. Use these to build custom features like announcements, analytics, achievements, and more.
All events are emitted after data is persisted to Flashcore, so listeners always see the latest state.
Event types
Three events are available:
levelUp
Fires when a user's level increases.
import { events } from '@robojs/xp'
events.onLevelUp(async ({ guildId, userId, oldLevel, newLevel, totalXp, storeId }) => {
console.log(`${userId} leveled up from ${oldLevel} to ${newLevel}`)
})import { events } from '@robojs/xp'
events.onLevelUp(async ({ guildId, userId, oldLevel, newLevel, totalXp, storeId }) => {
console.log(`${userId} leveled up from ${oldLevel} to ${newLevel}`)
})Payload:
Prop
Type
levelDown
Fires when a user's level decreases (e.g., from XP removal).
events.onLevelDown(async ({ guildId, userId, oldLevel, newLevel, totalXp, storeId }) => {
console.log(`${userId} dropped from level ${oldLevel} to ${newLevel}`)
})events.onLevelDown(async ({ guildId, userId, oldLevel, newLevel, totalXp, storeId }) => {
console.log(`${userId} dropped from level ${oldLevel} to ${newLevel}`)
})Payload: Same shape as levelUp, but newLevel is always less than oldLevel.
xpChange
Fires on every XP modification (add, remove, set, or message award).
events.onXPChange(async ({ guildId, userId, oldXp, newXp, delta, reason, storeId }) => {
console.log(`${userId} XP changed by ${delta} (reason: ${reason})`)
})events.onXPChange(async ({ guildId, userId, oldXp, newXp, delta, reason, storeId }) => {
console.log(`${userId} XP changed by ${delta} (reason: ${reason})`)
})Payload:
Prop
Type
Listener API
on / off / once
The generic event API supports registering, removing, and one-time listeners.
import { events } from '@robojs/xp'
// Persistent listener
events.on('levelUp', handler)
// One-time listener (removed after first trigger)
events.once('xpChange', handler)
// Remove a listener (must be the same function reference)
events.off('levelUp', handler)import { events } from '@robojs/xp'
// Persistent listener
events.on('levelUp', handler)
// One-time listener (removed after first trigger)
events.once('xpChange', handler)
// Remove a listener (must be the same function reference)
events.off('levelUp', handler)Convenience methods
Shorthand methods for common patterns:
events.onLevelUp(handler) // events.on('levelUp', handler)
events.onLevelDown(handler) // events.on('levelDown', handler)
events.onXPChange(handler) // events.on('xpChange', handler)events.onLevelUp(handler) // events.on('levelUp', handler)
events.onLevelDown(handler) // events.on('levelDown', handler)
events.onXPChange(handler) // events.on('xpChange', handler)Emission order
Both the core API and the message award handler emit events in this order:
levelUporlevelDown(if level changed)xpChange(always)
Design listeners to be order-agnostic when possible. If your listener needs data from both events, use xpChange alone since it fires for every operation.
Filtering by store
When using multi-store, all events include a storeId field. Filter events to respond only to specific stores.
import { events } from '@robojs/xp'
events.onLevelUp((event) => {
if (event.storeId === 'reputation') {
console.log('Reputation level up:', event.newLevel)
} else if (event.storeId === 'default') {
console.log('XP level up:', event.newLevel)
}
})import { events } from '@robojs/xp'
events.onLevelUp((event) => {
if (event.storeId === 'reputation') {
console.log('Reputation level up:', event.newLevel)
} else if (event.storeId === 'default') {
console.log('XP level up:', event.newLevel)
}
})Built-in listeners
The plugin registers internal listeners at module load:
levelUp-- Reconciles role rewards (default store only)levelDown-- Removes roles ifremoveRewardOnXpLossis enabled (default store only)xpChange,levelUp,levelDown-- Invalidates leaderboard cache (per-store, lazy)
Role reward reconciliation only runs for the default store. Custom stores never trigger role changes.
Best practices
- Register listeners in
src/robo/start/files so they activate on Robo startup. - Use
async/awaitin listeners. Errors in listeners are logged but do not propagate to the caller. - Include
reasonwhen calling XP operations to make analytics and audit logs more useful. - For heavy work (API calls, database writes), consider using a queue rather than processing inline.
import { events } from '@robojs/xp'
export default () => {
events.onXPChange(async ({ guildId, userId, delta, reason, storeId }) => {
if (storeId !== 'default') return
await analyticsService.track('xp_change', {
guild: guildId,
user: userId,
amount: delta,
reason
})
})
}import { events } from '@robojs/xp'
export default () => {
events.onXPChange(async ({ guildId, userId, delta, reason, storeId }) => {
if (storeId !== 'default') return
await analyticsService.track('xp_change', {
guild: guildId,
user: userId,
amount: delta,
reason
})
})
}