Scheduling and Folia
Use keystone.scheduler() as the only scheduling entry point. On Bukkit and Spigot, region-aware
calls collapse onto the main thread. On Folia, the same calls select the scheduler that owns the
entity or location.
KeystoneScheduler scheduler = keystone.scheduler();
Choose by the state you touch
run(task)global server workRun as soon as possible on Bukkit's main thread or Folia's global region. Suitable for plugin bookkeeping that does not touch a specific entity, chunk or block.
runLater(task, delayTicks)delayed global workThe delayed form of
run.runTimer(task, delayTicks, periodTicks)repeating global workRepeating global bookkeeping. Do not use it to iterate over blocks or mutate entities on Folia.
atEntity(entity, task)entity-owned workRun on the thread that owns the entity and follow it if it moves between regions.
atEntityTimer(entity, task, delayTicks, periodTicks)repeating entity workUse for an entity's visual, AI or state loop. Stop or cancel it when that entity is no longer valid.
atLocation(location, task)region-owned workUse before reading or changing blocks, chunks or world state at a fixed location.
async(task)off-thread workFile, HTTP or database work that does not touch Bukkit world state.
scheduler.atEntity(player, () -> player.getInventory().addItem(item));
scheduler.atLocation(location, () -> location.getBlock().setType(Material.STONE));
scheduler.async(() -> loadRowsFromDatabase());
Returning from async work
Do blocking work asynchronously, then schedule the result against the object it will update:
scheduler.async(() -> {
List<Reward> rewards = repository.load(player.getUniqueId());
scheduler.atEntity(player, () -> applyRewards(player, rewards));
});
Do not keep mutable Bukkit objects for longer than needed across that boundary. Prefer ids and immutable values, then resolve or validate the target again on its owning thread.
Teleports
scheduler.teleport(entity, destination).thenAccept(success -> {
if (!success) {
getLogger().warning("Teleport failed for " + entity.getUniqueId());
}
});
teleport uses the platform-safe form and returns CompletableFuture<Boolean>. The continuation's
thread is not a promise that world state is safe to touch; schedule any follow-up world work with
atEntity or atLocation.
Task handles and shutdown
Every scheduling method except teleport returns KeystoneTask. Keep it when you need to cancel a
timer, and check isCancelled() when task state matters. keystone.shutdown() cancels the
framework's resources; your own long-lived state should also be registered with
keystone.onShutdown(...) or closed from onDisable().
Folia declaration
folia-supported: true
scheduler.ownsRegion(location) is useful for assertions and fast paths. It always returns true off
Folia. scheduler.isFolia() is for diagnostics or behaviour that genuinely differs by platform,
not for duplicating the scheduler routing Keystone already performs.
Verify ownership assumptions
Exercise entity and location work on Folia, not only Paper: the Bukkit backend intentionally makes
every region ownership check succeed. Keep database and network work inside async, then return to
the exact entity or location before touching Bukkit state. Retain repeating task handles when they
can end before plugin shutdown and cancel them explicitly.