LogoRobo.js

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:

  1. levelUp or levelDown (if level changed)
  2. 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 if removeRewardOnXpLoss is 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/await in listeners. Errors in listeners are logged but do not propagate to the caller.
  • Include reason when 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.
xp-analytics.tsXP change tracking
src/robo/start/xp-analytics.ts
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
		})
	})
}
src/robo/start/xp-analytics.js
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
		})
	})
}

Next steps

On this page