Overview
Minestom’s scheduler system provides a flexible way to execute tasks at specific times or intervals. The scheduler supports tick-based scheduling (aligned with the server’s 20 TPS tick rate) and duration-based scheduling (using real-world time).
Tasks are executed in the caller thread by default. For precision-critical operations, consider using a JDK executor service or third-party library.
Getting the Scheduler
Global Scheduler
The global scheduler is accessed through MinecraftServer.getSchedulerManager():
SchedulerManager scheduler = MinecraftServer.getSchedulerManager();
scheduler.scheduleNextTick(() -> {
System.out.println("Executed on next tick");
});
Entity and Instance Schedulers
Entities and instances have their own schedulers for localized task management:
// Entity scheduler
Entity entity = ...;
entity.scheduler().scheduleNextTick(() -> {
entity.setVelocity(new Vec(0, 10, 0));
});
// Instance scheduler
Instance instance = ...;
instance.scheduler().scheduleTask(() -> {
instance.sendMessage(Component.text("Server message!"));
}, TaskSchedule.seconds(30), TaskSchedule.seconds(30));
Scheduler Interface
Core Methods
submitTask
Task submitTask(Supplier<TaskSchedule> task, ExecutionType executionType)
The primitive method for scheduling tasks with custom logic. The supplier is called immediately to get the initial schedule, and called again after each execution to determine the next schedule.AtomicInteger counter = new AtomicInteger(0);
scheduler.submitTask(() -> {
int count = counter.incrementAndGet();
System.out.println("Execution #" + count);
// Run 5 times, then stop
if (count >= 5) return TaskSchedule.stop();
// Schedule next execution in 1 second
return TaskSchedule.seconds(1);
}, ExecutionType.TICK_START);
scheduleTask
Task scheduleTask(Runnable task, TaskSchedule delay, TaskSchedule repeat)
Schedule a task with a delay and optional repeat interval.// Execute after 5 seconds, then repeat every 10 seconds
scheduler.scheduleTask(() -> {
System.out.println("Repeating task");
}, TaskSchedule.seconds(5), TaskSchedule.seconds(10));
scheduleNextTick
Task scheduleNextTick(Runnable task)
Schedule a task to run on the next server tick.scheduler.scheduleNextTick(() -> {
System.out.println("Runs next tick");
});
scheduleNextProcess
Task scheduleNextProcess(Runnable task)
Schedule a task to run on the next process() call (may be within the same tick).scheduler.scheduleNextProcess(() -> {
System.out.println("Runs on next process");
});
scheduleEndOfTick
Task scheduleEndOfTick(Runnable task)
Schedule a task to run at the end of the current tick.scheduler.scheduleEndOfTick(() -> {
System.out.println("Runs at tick end");
});
buildTask
Task.Builder buildTask(Runnable task)
Create a task builder for advanced configuration.scheduler.buildTask(() -> {
System.out.println("Custom task");
})
.delay(TaskSchedule.seconds(5))
.repeat(TaskSchedule.tick(20))
.executionType(ExecutionType.TICK_END)
.schedule();
TaskSchedule
TaskSchedule defines when a task should execute next. All factory methods are static.
Scheduling Patterns
tick
TaskSchedule tick(int ticks)
Schedule execution after a specific number of ticks (20 ticks = 1 second at normal TPS).TaskSchedule.tick(20) // 1 second
TaskSchedule.tick(100) // 5 seconds
TaskSchedule.nextTick() // Next tick (shortcut for tick(1))
duration
TaskSchedule duration(Duration duration)
Schedule execution after a real-world time duration.TaskSchedule.duration(Duration.ofSeconds(30))
TaskSchedule.duration(5, ChronoUnit.MINUTES)
Execute on the next process() call (may be in the same tick).
Stop the task from executing again.
Park the task until manually unparked via Task.unpark().Task task = scheduler.buildTask(() -> {
System.out.println("Unparked!");
})
.delay(TaskSchedule.park())
.schedule();
// Later, unpark the task
task.unpark();
future
TaskSchedule future(CompletableFuture<?> future)
Execute when a CompletableFuture completes.CompletableFuture<String> future = loadDataAsync();
scheduler.buildTask(() -> {
System.out.println("Data loaded!");
})
.delay(TaskSchedule.future(future))
.schedule();
Convenience Methods
// Duration shortcuts
TaskSchedule.seconds(30)
TaskSchedule.minutes(5)
TaskSchedule.hours(1)
TaskSchedule.millis(500)
// Tick shortcuts
TaskSchedule.nextTick() // tick(1)
Task Interface
The Task interface provides methods to manage scheduled tasks.
Returns the unique task identifier.
Returns the scheduler that owns this task.
executionType
ExecutionType executionType()
Returns when the task executes (TICK_START or TICK_END).
Returns true if the task is still scheduled to run.
Cancels the task, preventing future executions.Task task = scheduler.scheduleTask(() -> {
System.out.println("Repeating");
}, TaskSchedule.immediate(), TaskSchedule.seconds(1));
// Cancel after 10 seconds
scheduler.scheduleTask(() -> {
task.cancel();
}, TaskSchedule.seconds(10), TaskSchedule.stop());
Returns true if the task is parked.
Unparks a parked task, allowing it to execute on the next process.Task task = scheduler.buildTask(() -> {
System.out.println("Ready!");
})
.delay(TaskSchedule.park())
.schedule();
// Unpark when condition is met
if (someCondition) {
task.unpark();
}
Task.Builder
The builder pattern provides a fluent API for task configuration.
delay
Builder delay(TaskSchedule schedule)
Set the initial delay before first execution.builder.delay(TaskSchedule.seconds(5))
builder.delay(Duration.ofMinutes(1))
builder.delay(10, ChronoUnit.SECONDS)
repeat
Builder repeat(TaskSchedule schedule)
Set the repeat interval (overrides the task’s return value).builder.repeat(TaskSchedule.tick(20))
builder.repeat(Duration.ofSeconds(30))
builder.repeat(5, ChronoUnit.MINUTES)
executionType
Builder executionType(ExecutionType type)
Set when the task executes within the tick cycle.builder.executionType(ExecutionType.TICK_START) // Default
builder.executionType(ExecutionType.TICK_END)
Build and submit the task to the scheduler.Task task = builder.schedule();
ExecutionType
Controls when tasks execute during the server tick.
Execute at the beginning of the tick (default).
Execute at the end of the tick, after all tick processing.
Practical Examples
Delayed Task
Execute a task once after a delay:
scheduler.buildTask(() -> {
player.sendMessage("5 seconds have passed!");
})
.delay(TaskSchedule.seconds(5))
.schedule();
Repeating Task
Execute a task repeatedly at fixed intervals:
scheduler.scheduleTask(() -> {
instance.sendMessage(Component.text("Server announcement"));
}, TaskSchedule.minutes(1), TaskSchedule.minutes(5));
Conditional Execution
Execute a task a specific number of times:
AtomicInteger counter = new AtomicInteger(0);
scheduler.submitTask(() -> {
int count = counter.incrementAndGet();
System.out.println("Execution #" + count);
// Stop after 10 executions
if (count >= 10) return TaskSchedule.stop();
return TaskSchedule.seconds(1);
}, ExecutionType.TICK_START);
Tick-Based Countdown
AtomicInteger countdown = new AtomicInteger(10);
scheduler.submitTask(() -> {
int remaining = countdown.getAndDecrement();
if (remaining > 0) {
instance.sendMessage(Component.text(remaining + "..."));
return TaskSchedule.tick(20); // 1 second intervals
} else {
instance.sendMessage(Component.text("GO!"));
return TaskSchedule.stop();
}
}, ExecutionType.TICK_START);
Async Operation Integration
CompletableFuture<PlayerData> dataFuture = loadPlayerDataAsync(uuid);
scheduler.buildTask(() -> {
PlayerData data = dataFuture.join();
player.sendMessage("Welcome back, " + data.getName());
})
.delay(TaskSchedule.future(dataFuture))
.schedule();
Parked Task Pattern
// Create a parked task
Task respawnTask = scheduler.buildTask(() -> {
entity.teleport(spawnPoint);
entity.setHealth(entity.getMaxHealth());
})
.delay(TaskSchedule.park())
.schedule();
// Unpark when the player dies
entity.eventNode().addListener(PlayerDeathEvent.class, event -> {
respawnTask.unpark();
});
Task Cancellation
// Start a repeating task
Task annoyingTask = scheduler.scheduleTask(() -> {
player.playSound(Sound.sound(SoundEvent.ENTITY_VILLAGER_AMBIENT,
Sound.Source.MASTER, 1f, 1f));
}, TaskSchedule.immediate(), TaskSchedule.seconds(1));
// Cancel it after 10 seconds
scheduler.buildTask(() -> {
annoyingTask.cancel();
})
.delay(TaskSchedule.seconds(10))
.schedule();
End-of-Tick Processing
Execute cleanup logic at the end of a tick:
scheduler.scheduleEndOfTick(() -> {
// All entity movements have been processed
// Safe to do collision detection or cleanup
instance.getEntities().forEach(entity -> {
checkAndHandleCollisions(entity);
});
});
Thread Safety
The scheduler’s process(), processTick(), and processTickEnd() methods are not thread-safe. They should only be called from the server’s main tick thread.
Tasks are executed in the caller thread by default. To execute tasks on different threads:
ExecutorService executor = Executors.newCachedThreadPool();
// Schedule on main thread, execute on another thread
scheduler.scheduleNextTick(() -> {
executor.submit(() -> {
// Heavy computation on separate thread
ExpensiveData result = performExpensiveCalculation();
// Schedule result handling back on main thread
scheduler.scheduleNextTick(() -> {
handleResult(result);
});
});
});
Best Practices
-
Use tick-based scheduling for game logic: Use
TaskSchedule.tick() for game mechanics that should be synchronized with the server tick rate.
-
Use duration-based scheduling for real-world time: Use
TaskSchedule.duration() for timeouts, announcements, or events based on wall-clock time.
-
Prefer entity/instance schedulers: Use localized schedulers when tasks are tied to specific entities or instances for better organization and automatic cleanup.
-
Cancel tasks when done: Always cancel repeating tasks when they’re no longer needed to avoid memory leaks.
-
Handle exceptions gracefully: Exceptions in tasks are caught and logged, but won’t crash the server. However, the task that threw will be cancelled.
-
Avoid blocking operations: Don’t perform I/O or other blocking operations in scheduled tasks. Use
CompletableFuture with TaskSchedule.future() instead.
Common Patterns
Delayed Entity Removal
Entity entity = new ItemEntity(itemStack);
instance.addEntity(entity, position);
// Remove after 5 minutes
entity.scheduler().buildTask(() -> {
entity.remove();
})
.delay(TaskSchedule.minutes(5))
.schedule();
Periodic Autosave
scheduler.scheduleTask(() -> {
saveAllPlayerData();
System.out.println("Autosave complete");
}, TaskSchedule.minutes(5), TaskSchedule.minutes(5));
Timeout Handler
Task timeoutTask = scheduler.buildTask(() -> {
player.kick(Component.text("AFK timeout"));
})
.delay(TaskSchedule.minutes(10))
.schedule();
// Cancel timeout on player activity
player.eventNode().addListener(PlayerMoveEvent.class, event -> {
timeoutTask.cancel();
// Create a new timeout task
});