Scheduler
Schedule delayed tasks, repeat actions on a timer, run work off the main thread, and build sequential chains — all with a clean, fluent API.
Introduction
The Scheduler module gives you full control over when your code runs. Whether you need to trigger something after a short delay, repeat an action every few seconds, perform expensive work off the main game thread, or sequence a series of timed steps — the Scheduler covers all of these scenarios without requiring you to manage threads or tick counters manually.
Core concepts
- Delay — run code once after N ticks (20 ticks = 1 second)
- Repeat — run code every N ticks, optionally for a fixed number of times
- Async — run a
Callableoff the main thread, then handle the result back on the main thread - Chain — execute a sequence of tasks, each after a specified delay from the previous
- Cancel / Pause / Resume — manage any running task via its
ScheduledTaskhandle
Minecraft runs at 20 ticks per second (TPS). All durations in the Scheduler are expressed in ticks. Use 20 for one second, 200 for ten seconds, and so on.
EriScheduler — Static API
EriScheduler is the main entry point. All methods are static, so you never need to
instantiate anything. Every scheduling call returns a ScheduledTask that you can use
to pause, resume, or cancel the task later.
Delayed execution
Run a block of code once after a given number of ticks:
// Execute once after 20 ticks (1 second)
ScheduledTask task = EriScheduler.delay(20, () -> {
player.sendMessage(new TextComponentString("1 second later!"));
});
Fixed-count repetition
Run a block every N ticks for a maximum number of executions:
// Run every 20 ticks, up to 5 times
ScheduledTask task = EriScheduler.repeat(20, 5, () -> {
System.out.println("Tick!");
});
Infinite repetition
Run a block every N ticks indefinitely until cancelled:
// Run every 100 ticks (5 seconds) forever
ScheduledTask task = EriScheduler.repeat(100, () -> {
System.out.println("Every 5 seconds");
});
Cancel all tasks
// Stop every running task immediately
EriScheduler.cancelAll();
Method reference
| Method | Parameters | Returns | Description |
|---|---|---|---|
delay |
int ticks, Runnable task |
ScheduledTask |
Execute task once after ticks ticks. |
repeat |
int ticks, int maxCount, Runnable task |
ScheduledTask |
Execute task every ticks ticks, at most maxCount times. |
repeat |
int ticks, Runnable task |
ScheduledTask |
Execute task every ticks ticks indefinitely. |
async |
Callable<T> callable |
AsyncExecutor<T> |
Run callable off the main thread and return an async handle. |
chain |
— | TaskChain |
Create a new sequential task chain. |
cancelAll |
— | void |
Cancel every active task managed by the scheduler. |
activeCount |
— | int |
Return the number of currently active (non-cancelled) tasks. |
ScheduledTask — Task reference
Every call to EriScheduler.delay() or EriScheduler.repeat() returns a
ScheduledTask object. Hold onto this reference if you need to control the task after
scheduling it.
ScheduledTask task = EriScheduler.repeat(20, () -> doStuff());
// Pause without losing the execution count
task.pause();
// Resume from where it was paused
task.resume();
// Stop permanently
task.cancel();
// Query the task's current state
boolean running = task.isRunning();
boolean cancelled = task.isCancelled();
boolean paused = task.isPaused();
// How many times has the runnable been executed so far?
int count = task.getExecutionCount();
Method reference
| Method | Returns | Description |
|---|---|---|
pause() |
ScheduledTask |
Temporarily suspend the task. The tick counter is frozen until resume() is called. |
resume() |
ScheduledTask |
Resume a paused task. Has no effect if the task is not paused. |
cancel() |
void |
Permanently stop the task. It cannot be restarted after cancellation. |
isRunning() |
boolean |
Returns true if the task is active and not paused or cancelled. |
isCancelled() |
boolean |
Returns true if cancel() was called on this task. |
isPaused() |
boolean |
Returns true if the task is currently paused. |
getExecutionCount() |
int |
Returns the total number of times the runnable has been executed. |
pause() and resume() return this, so you can chain them: task.pause().resume() (though that would be a no-op). More practically, store the reference and call them in response to user actions.
TaskChain — Sequential chain
A TaskChain lets you queue a series of tasks where each one fires after a specified
delay following the previous step. Call EriScheduler.chain() to create one,
add steps with .then(delay, runnable), and start the sequence with .run().
This is ideal for cutscene-style sequences, tutorial steps, timed announcements, or any workflow where things need to happen in a strict order with controlled pauses between them.
EriScheduler.chain()
.then(0, () -> System.out.println("Now"))
.then(20, () -> System.out.println("1s later"))
.then(40, () -> System.out.println("2s later"))
.run();
The delay value in .then(delay, runnable) is relative to the previous step,
not to when .run() was called. In the example above:
step 1 fires at tick 0, step 2 at tick 20, step 3 at tick 60 (20 + 40).
Method reference
| Method | Parameters | Returns | Description |
|---|---|---|---|
then |
int delay, Runnable task |
TaskChain |
Append a step that runs task after delay ticks from the preceding step. |
run |
— | void |
Start executing the chain from the first step. |
AsyncExecutor — Async tasks
EriScheduler.async() runs a Callable on a background thread, keeping the
main game thread responsive. The returned AsyncExecutor handle lets you attach
callbacks that run back on the main thread once the async work is done, or handle errors gracefully.
Use this for anything that might block — HTTP requests, file I/O, heavy computation — to avoid causing lag spikes in the game.
EriScheduler.async(() -> {
// Runs off the main thread
return fetchDataFromWeb();
}).onError(e -> {
System.err.println("Error: " + e.getMessage());
}).thenSync(result -> {
// Runs back on the main thread once the async work completes
player.sendMessage(new TextComponentString("Data: " + result));
});
Method reference
| Method | Parameters | Returns | Description |
|---|---|---|---|
onError |
Consumer<Throwable> handler |
AsyncExecutor<T> |
Register a callback invoked on the main thread if the async callable throws an exception. |
thenSync |
Consumer<T> callback |
AsyncExecutor<T> |
Register a callback invoked on the main thread with the result value once the async work completes successfully. |
The async callable runs on a background thread. Accessing Minecraft, World,
EntityPlayer, or any Minecraft game object from inside the callable will cause
thread-safety issues and potentially crash the game. Only do I/O or pure computation there.
Use thenSync() to interact with Minecraft objects.
Complete example — Countdown
This example shows a practical countdown timer that sends messages to the player every second,
then announces "Go!" when it reaches zero. It combines repeat with a fixed execution
count and an inline state variable.
final int[] count = {5};
ScheduledTask countdown = EriScheduler.repeat(20, 6, () -> {
if (count[0] > 0) {
player.sendMessage(new TextComponentString(count[0] + "..."));
count[0]--;
} else {
player.sendMessage(new TextComponentString("Go!"));
}
});
The task runs 6 times total (once per second for 5 seconds, then the "Go!" message on the 6th tick).
After 6 executions the scheduler automatically stops it. The final int[] trick is
necessary because Java 8 lambdas can only capture effectively final variables — wrapping
the counter in a single-element array works around this limitation.
If you need to abort the countdown before it finishes (e.g., the player disconnects), just call
countdown.cancel() at any point. The remaining executions will never fire.
Countdown with a chain variant
You can also express the same countdown as a TaskChain for explicit control over each step:
EriScheduler.chain()
.then(0, () -> player.sendMessage(new TextComponentString("5...")))
.then(20, () -> player.sendMessage(new TextComponentString("4...")))
.then(20, () -> player.sendMessage(new TextComponentString("3...")))
.then(20, () -> player.sendMessage(new TextComponentString("2...")))
.then(20, () -> player.sendMessage(new TextComponentString("1...")))
.then(20, () -> player.sendMessage(new TextComponentString("Go!")))
.run();