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 work

Run 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 work

The delayed form of run.

runTimer(task, delayTicks, periodTicks)repeating global work

Repeating global bookkeeping. Do not use it to iterate over blocks or mutate entities on Folia.

atEntity(entity, task)entity-owned work

Run on the thread that owns the entity and follow it if it moves between regions.

atEntityTimer(entity, task, delayTicks, periodTicks)repeating entity work

Use 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 work

Use before reading or changing blocks, chunks or world state at a fixed location.

async(task)off-thread work

File, 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.