Skip to main content
The task scheduler allows you to run code at specific times, with delays, or repeatedly. This is essential for timers, countdowns, auto-save systems, and more.

Task Types

PocketMine-MP has two main types of tasks:
  1. Regular Tasks - Run on the main server thread
  2. Async Tasks - Run on separate worker threads
Async tasks cannot access the Server instance, worlds, players, or entities directly. Use them only for CPU-intensive operations.

Creating a Task

Tasks extend the Task class and implement onRun():
src/MyPlugin/tasks/MyTask.php
<?php

declare(strict_types=1);

namespace MyPlugin\tasks;

use pocketmine\scheduler\Task;
use MyPlugin\Main;

class MyTask extends Task {
    
    public function __construct(
        private Main $plugin
    ){}
    
    public function onRun() : void {
        // This code runs when the task executes
        $this->plugin->getLogger()->info("Task executed!");
    }
}

Scheduling Tasks

All scheduling is done through the plugin’s scheduler:

Run Once (Immediate)

Execute a task once, immediately:
$this->getScheduler()->scheduleTask(new MyTask($this));

Run Once (Delayed)

Execute a task once, after a delay:
// Run after 20 ticks (1 second)
$this->getScheduler()->scheduleDelayedTask(new MyTask($this), 20);

// Run after 5 seconds (100 ticks)
$this->getScheduler()->scheduleDelayedTask(new MyTask($this), 100);
PocketMine-MP runs at 20 ticks per second (TPS). 1 tick = 0.05 seconds = 50ms.

Run Repeatedly

Execute a task repeatedly at a fixed interval:
// Run every 20 ticks (every second)
$this->getScheduler()->scheduleRepeatingTask(
    new MyTask($this),
    20  // period in ticks
);

// Run every 5 seconds
$this->getScheduler()->scheduleRepeatingTask(
    new MyTask($this),
    100
);

Run Repeatedly (Delayed Start)

Execute a task repeatedly, but start after a delay:
// Wait 60 ticks (3 seconds), then run every 20 ticks (1 second)
$this->getScheduler()->scheduleDelayedRepeatingTask(
    new MyTask($this),
    60,  // delay before first run
    20   // period between runs
);

Closure Tasks

For simple tasks, use ClosureTask instead of creating a class:
use pocketmine\scheduler\ClosureTask;

// Run once
$this->getScheduler()->scheduleTask(new ClosureTask(
    function() : void {
        $this->getLogger()->info("Task ran!");
    }
));

// Run delayed
$this->getScheduler()->scheduleDelayedTask(
    new ClosureTask(function() : void {
        $this->getLogger()->info("Delayed task!");
    }),
    40  // 2 seconds
);

// Run repeatedly
$this->getScheduler()->scheduleRepeatingTask(
    new ClosureTask(function() : void {
        $this->broadcastMessage("Repeating task!");
    }),
    20  // every second
);

Canceling Tasks

Tasks can be cancelled using their TaskHandler:
// Save the handler when scheduling
$handler = $this->getScheduler()->scheduleRepeatingTask(
    new MyTask($this),
    20
);

// Cancel the task later
$handler->cancel();

Cancel from Inside Task

class CountdownTask extends Task {
    
    private int $count = 10;
    
    public function __construct(
        private Main $plugin
    ){}
    
    public function onRun() : void {
        if($this->count <= 0) {
            $this->plugin->getServer()->broadcastMessage("Countdown finished!");
            $this->getHandler()->cancel();
            return;
        }
        
        $this->plugin->getServer()->broadcastMessage(
            "Countdown: " . $this->count
        );
        $this->count--;
    }
}

// Schedule: runs every second, cancels automatically at 0
$this->getScheduler()->scheduleRepeatingTask(
    new CountdownTask($this),
    20
);

Cancel All Plugin Tasks

Cancel all tasks when plugin disables:
protected function onDisable() : void {
    $this->getScheduler()->cancelAllTasks();
}
Plugin tasks are automatically cancelled when the plugin is disabled.

Complete Examples

Auto-Save Task

src/MyPlugin/tasks/AutoSaveTask.php
<?php

declare(strict_types=1);

namespace MyPlugin\tasks;

use pocketmine\scheduler\Task;
use MyPlugin\Main;

class AutoSaveTask extends Task {
    
    public function __construct(
        private Main $plugin
    ){}
    
    public function onRun() : void {
        // Save all player data
        foreach($this->plugin->getServer()->getOnlinePlayers() as $player) {
            $this->plugin->savePlayerData($player);
        }
        
        // Save plugin data
        $this->plugin->getConfig()->save();
        
        $this->plugin->getLogger()->info("Auto-saved player data");
    }
}

// In Main.php onEnable():
protected function onEnable() : void {
    // Auto-save every 5 minutes (6000 ticks)
    $this->getScheduler()->scheduleRepeatingTask(
        new AutoSaveTask($this),
        6000
    );
}

Countdown Timer

src/MyPlugin/tasks/GameCountdownTask.php
<?php

declare(strict_types=1);

namespace MyPlugin\tasks;

use pocketmine\scheduler\Task;
use MyPlugin\Main;

class GameCountdownTask extends Task {
    
    private int $secondsLeft;
    
    public function __construct(
        private Main $plugin,
        int $seconds
    ){
        $this->secondsLeft = $seconds;
    }
    
    public function onRun() : void {
        if($this->secondsLeft <= 0) {
            $this->plugin->startGame();
            $this->getHandler()->cancel();
            return;
        }
        
        // Announce at specific intervals
        if($this->secondsLeft <= 5 || $this->secondsLeft % 10 === 0) {
            $this->plugin->getServer()->broadcastMessage(
                "§eGame starting in §c" . $this->secondsLeft . "§e seconds!"
            );
        }
        
        $this->secondsLeft--;
    }
}

// Start 30 second countdown
$this->getScheduler()->scheduleRepeatingTask(
    new GameCountdownTask($this, 30),
    20  // runs every second
);

Repeating Announcement

protected function onEnable() : void {
    // Announce every 5 minutes
    $messages = [
        "§eWelcome to our server!",
        "§eVisit our website: example.com",
        "§eDonate to support the server!"
    ];
    
    $index = 0;
    
    $this->getScheduler()->scheduleRepeatingTask(
        new ClosureTask(function() use (&$index, $messages) : void {
            $this->getServer()->broadcastMessage($messages[$index]);
            $index = ($index + 1) % count($messages);
        }),
        6000  // 5 minutes
    );
}

Delayed Teleport

public function teleportWithDelay(Player $player, Position $destination) : void {
    $player->sendMessage("§eTeleporting in 3 seconds...");
    
    // Save player's position to check if they moved
    $originalPos = $player->getPosition();
    
    $this->getScheduler()->scheduleDelayedTask(
        new ClosureTask(function() use ($player, $destination, $originalPos) : void {
            // Check if player moved
            if($player->getPosition()->distance($originalPos) > 0.5) {
                $player->sendMessage("§cTeleport cancelled - you moved!");
                return;
            }
            
            $player->teleport($destination);
            $player->sendMessage("§aTeleported!");
        }),
        60  // 3 seconds
    );
}

Async Tasks

Async tasks run on separate threads and should be used for CPU-intensive operations like:
  • World generation
  • Data compression
  • Large file operations (without using Server APIs)
  • Complex calculations
Async tasks CANNOT:
  • Access Server, worlds, players, entities
  • Call plugin methods directly
  • Modify game state
Only use async tasks for isolated, CPU-bound work.

Creating an Async Task

src/MyPlugin/tasks/MyAsyncTask.php
<?php

declare(strict_types=1);

namespace MyPlugin\tasks;

use pocketmine\scheduler\AsyncTask;

class MyAsyncTask extends AsyncTask {
    
    private string $data;
    
    public function __construct(string $data) {
        $this->data = $data;
    }
    
    // Runs on worker thread (cannot access Server)
    public function onRun() : void {
        // Do heavy computation
        $result = [];
        for($i = 0; $i < 1000000; $i++) {
            $result[] = hash('sha256', $this->data . $i);
        }
        
        // Store result (must be serializable)
        $this->setResult(count($result));
    }
    
    // Runs on main thread when task completes
    public function onCompletion() : void {
        $result = $this->getResult();
        // Can access Server here
    }
}

Scheduling Async Tasks

$task = new MyAsyncTask("some data");
$this->getServer()->getAsyncPool()->submitTask($task);

Using storeLocal/fetchLocal

To pass non-serializable data (like plugin reference) from main thread to completion:
class MyAsyncTask extends AsyncTask {
    
    public function __construct(Main $plugin) {
        // Store plugin reference (won't be serialized)
        $this->storeLocal("plugin", $plugin);
    }
    
    public function onRun() : void {
        // Cannot access plugin here
        $result = "computed value";
        $this->setResult($result);
    }
    
    public function onCompletion() : void {
        // Retrieve plugin reference
        /** @var Main $plugin */
        $plugin = $this->fetchLocal("plugin");
        
        $result = $this->getResult();
        $plugin->getLogger()->info("Task completed: " . $result);
    }
}

Time Conversions

Helper for converting time to ticks:
class TimeHelper {
    public const TICKS_PER_SECOND = 20;
    public const TICKS_PER_MINUTE = 20 * 60;
    public const TICKS_PER_HOUR = 20 * 60 * 60;
    
    public static function secondsToTicks(float $seconds) : int {
        return (int) ($seconds * self::TICKS_PER_SECOND);
    }
    
    public static function minutesToTicks(float $minutes) : int {
        return (int) ($minutes * self::TICKS_PER_MINUTE);
    }
}

// Usage:
$this->getScheduler()->scheduleDelayedTask(
    new MyTask($this),
    TimeHelper::secondsToTicks(5)  // 5 seconds
);

$this->getScheduler()->scheduleRepeatingTask(
    new MyTask($this),
    TimeHelper::minutesToTicks(1)  // every minute
);

Task Handler Methods

$handler = $this->getScheduler()->scheduleRepeatingTask(new MyTask($this), 20);

// Cancel the task
$handler->cancel();

// Check if cancelled
if($handler->isCancelled()) {
    // Task is cancelled
}

// Check if it's a repeating task
if($handler->isRepeating()) {
    // This is a repeating task
}

// Check if it has a delay
if($handler->isDelayed()) {
    // This task has a delay before first run
}

// Get the task instance
$task = $handler->getTask();

Best Practices

Use ClosureTask for simple operations:
$this->getScheduler()->scheduleDelayedTask(
    new ClosureTask(fn() => $player->sendMessage("Hello!")),
    20
);
Create Task classes for complex logic:
class ComplexTask extends Task {
    // Multiple methods, properties, logic
}
Always cancel repeating tasks:
private ?TaskHandler $repeatingTask = null;

protected function onEnable() : void {
    $this->repeatingTask = $this->getScheduler()->scheduleRepeatingTask(...);
}

protected function onDisable() : void {
    $this->repeatingTask?->cancel();
}
Don’t schedule too many tasks: Each task has overhead. Instead of 1000 individual tasks, use one task that processes multiple items.Use delays wisely: Don’t schedule a task every tick (20 times per second) unless absolutely necessary. This impacts performance.

Debugging Tasks

class DebugTask extends Task {
    
    private int $runCount = 0;
    
    public function __construct(
        private Main $plugin
    ){}
    
    public function onRun() : void {
        $this->runCount++;
        $this->plugin->getLogger()->debug(
            "Task ran {$this->runCount} times"
        );
        
        // Auto-cancel after 10 runs
        if($this->runCount >= 10) {
            $this->plugin->getLogger()->info("Task finished");
            $this->getHandler()->cancel();
        }
    }
}

Next Steps

Configuration

Manage plugin settings and data

Resources

Work with embedded resources

Build docs developers (and LLMs) love