Skip to main content
Foundation provides a unified API for scheduling tasks that works seamlessly across Bukkit, Spigot, Paper, and Folia.

Task scheduling methods

All task methods are available through the Common class and return a SimpleTask instance.

Delayed tasks

runLater

Schedule a task to run on the main thread after a delay:
import org.mineacademy.fo.Common;
import org.mineacademy.fo.model.SimpleTask;

// Run after 1 tick (default)
Common.runLater(() -> {
    player.sendMessage("This runs 1 tick later!");
});

// Run after specific delay (in ticks)
Common.runLater(20, () -> {
    player.sendMessage("This runs after 1 second!");
});

// Store the task reference to cancel it later
SimpleTask task = Common.runLater(100, () -> {
    player.sendMessage("This might get cancelled!");
});
20 ticks = 1 second in Minecraft. Use this conversion when scheduling time-based tasks.

runLaterAsync

Schedule a task to run asynchronously after a delay:
// Run async immediately (0 tick delay)
Common.runAsync(() -> {
    // Perform heavy computation off the main thread
    String data = fetchDataFromAPI();
    
    // Switch back to main thread for Bukkit API calls
    Common.runLater(() -> {
        player.sendMessage("Data loaded: " + data);
    });
});

// Run async after delay
Common.runLaterAsync(20, () -> {
    // Async task after 1 second
    performExpensiveCalculation();
});
Never call Bukkit API methods from async tasks! Always use Common.runLater() to switch back to the main thread before interacting with Bukkit objects.

Repeating tasks

runTimer

Schedule a repeating task on the main thread:
// Run every 20 ticks (1 second) with no initial delay
SimpleTask timer = Common.runTimer(20, () -> {
    player.sendMessage("Tick!");
});

// Run every 20 ticks with 40 tick initial delay
Common.runTimer(40, 20, () -> {
    updateScoreboard(player);
});

// Cancel after some time
Common.runLater(200, timer::cancel);

runTimerAsync

Schedule a repeating task asynchronously:
// Auto-save data every 5 minutes (6000 ticks)
Common.runTimerAsync(6000, () -> {
    saveDataToDatabase();
});

// With initial delay
Common.runTimerAsync(100, 6000, () -> {
    performPeriodicCheck();
});

SimpleTask API

The SimpleTask class wraps Bukkit’s BukkitTask and provides Folia compatibility.

Task control

SimpleTask task = Common.runLater(100, () -> {
    player.sendMessage("Hello!");
});

// Check if task is sync or async
boolean isSync = task.isSync();

// Get the task ID
int id = task.getTaskId();

// Check if cancelled
if (task.isCancelled()) {
    return;
}

// Cancel the task
task.cancel();

// Get the owning plugin
Plugin owner = task.getOwner();

Folia compatibility

SimpleTask automatically handles Folia’s different scheduling system:
// This works on both Bukkit and Folia!
SimpleTask task = Common.runLater(20, () -> {
    player.sendMessage("Works everywhere!");
});

task.cancel(); // Calls the correct cancel method for the platform
Foundation automatically detects if you’re running on Folia using Remain.isFolia() and uses the appropriate scheduling mechanism.

Common patterns

Database operations

Always perform database operations asynchronously:
public void loadPlayerData(Player player) {
    Common.runAsync(() -> {
        // Fetch from database (slow operation)
        PlayerData data = database.loadData(player.getUniqueId());
        
        // Apply data on main thread
        Common.runLater(() -> {
            if (player.isOnline()) {
                applyPlayerData(player, data);
            }
        });
    });
}

public void savePlayerData(Player player, PlayerData data) {
    Common.runAsync(() -> {
        // Save to database asynchronously
        database.saveData(player.getUniqueId(), data);
    });
}

Delayed cleanup

public void removeTemporaryBlock(Block block) {
    // Store original type
    Material original = block.getType();
    
    // Place temporary block
    block.setType(Material.BARRIER);
    
    // Restore after 5 seconds
    Common.runLater(100, () -> {
        block.setType(original);
    });
}

Countdown timers

public void startCountdown(Player player, int seconds) {
    final int[] remaining = {seconds};
    
    SimpleTask timer = Common.runTimer(20, () -> {
        if (remaining[0] <= 0) {
            player.sendMessage("Time's up!");
            return;
        }
        
        player.sendMessage("Time remaining: " + remaining[0] + "s");
        remaining[0]--;
    });
    
    // Auto-cancel when done
    Common.runLater(seconds * 20 + 1, timer::cancel);
}

Periodic checks

private SimpleTask regionCheckTask;

public void startRegionMonitoring() {
    regionCheckTask = Common.runTimer(20, () -> {
        for (Player player : Bukkit.getOnlinePlayers()) {
            Region region = getRegionAt(player.getLocation());
            
            if (region != null && !region.canEnter(player)) {
                player.teleport(region.getExitLocation());
                player.sendMessage("You cannot enter this region!");
            }
        }
    });
}

public void stopRegionMonitoring() {
    if (regionCheckTask != null) {
        regionCheckTask.cancel();
        regionCheckTask = null;
    }
}

Advanced usage

Chain multiple async operations

public void processPlayerJoin(Player player) {
    Common.runAsync(() -> {
        // Step 1: Load player data
        PlayerData data = loadFromDatabase(player.getUniqueId());
        
        Common.runLater(() -> {
            // Step 2: Apply data on main thread
            applyData(player, data);
            
            Common.runAsync(() -> {
                // Step 3: Log login to external API
                logLoginToAPI(player.getUniqueId());
                
                Common.runLater(() -> {
                    // Step 4: Final main thread operations
                    player.sendMessage("Welcome back!");
                });
            });
        });
    });
}

Task cancellation on disable

public class MyPlugin extends SimplePlugin {
    
    private final List<SimpleTask> activeTasks = new ArrayList<>();
    
    public void scheduleTask(Runnable task) {
        SimpleTask simpleTask = Common.runLater(task);
        activeTasks.add(simpleTask);
    }
    
    @Override
    protected void onPluginStop() {
        // Cancel all active tasks
        for (SimpleTask task : activeTasks) {
            if (!task.isCancelled()) {
                task.cancel();
            }
        }
        activeTasks.clear();
    }
}

Conditional repeating task

public void repeatUntilCondition(Runnable action, Supplier<Boolean> condition) {
    SimpleTask[] taskHolder = new SimpleTask[1];
    
    taskHolder[0] = Common.runTimer(20, () -> {
        if (condition.get()) {
            taskHolder[0].cancel();
            return;
        }
        
        action.run();
    });
}

// Usage example
repeatUntilCondition(
    () -> player.sendMessage("Waiting..."),
    () -> player.getLevel() >= 10
);

Performance considerations

1

Use async for heavy operations

Database queries, file I/O, network requests, and complex calculations should always run asynchronously.
// Good
Common.runAsync(() -> {
    expensiveCalculation();
});

// Bad - blocks main thread
expensiveCalculation();
2

Minimize sync/async switching

Excessive switching between threads can hurt performance. Group operations when possible.
// Better
Common.runAsync(() -> {
    Data data1 = fetchData1();
    Data data2 = fetchData2();
    Data data3 = fetchData3();
    
    Common.runLater(() -> {
        applyAllData(data1, data2, data3);
    });
});

// Worse - too much switching
Common.runAsync(() -> {
    Data data1 = fetchData1();
    Common.runLater(() -> applyData(data1));
});
Common.runAsync(() -> {
    Data data2 = fetchData2();
    Common.runLater(() -> applyData(data2));
});
3

Cancel tasks when no longer needed

Always cancel repeating tasks to prevent memory leaks.
private final Map<UUID, SimpleTask> playerTasks = new HashMap<>();

public void startPlayerTask(Player player) {
    SimpleTask task = Common.runTimer(20, () -> {
        // Do something
    });
    playerTasks.put(player.getUniqueId(), task);
}

public void stopPlayerTask(Player player) {
    SimpleTask task = playerTasks.remove(player.getUniqueId());
    if (task != null && !task.isCancelled()) {
        task.cancel();
    }
}

Thread safety

Most Bukkit API methods are not thread-safe. Only call them from the main thread.
Safe to call from async tasks:
  • Reading configuration files
  • Database operations
  • File I/O operations
  • Mathematical calculations
  • Network requests
Must be called from main thread:
  • Player/Entity modifications
  • Block/World changes
  • Inventory operations
  • Event firing
  • Bukkit API calls
// Example: Safe async data processing
Common.runAsync(() -> {
    // Safe: Reading data
    List<String> lines = Files.readAllLines(dataFile);
    
    // Safe: Processing data
    List<ItemStack> items = parseItems(lines);
    
    // UNSAFE: Don't do this!
    // player.getInventory().addItem(items);
    
    // Safe: Switch to main thread
    Common.runLater(() -> {
        for (ItemStack item : items) {
            player.getInventory().addItem(item);
        }
    });
});

Build docs developers (and LLMs) love