Skript: scripting basics for Minecraft server admins (2026)

Skript: scripting basics for Minecraft server admins (2026)

Skript lets you ship server logic without learning Java. You write nearly-English text into a .sk file, drop it into plugins/Skript/scripts/, run /sk reload and it works. For an admin who needs a join welcome, a custom /heal, a kill counter or a tiny economy without hunting for a Java dev, this is the fastest path from idea to live mechanic.

As of 2026 the active branch is Skript 2.9.x and up from the SkriptLang team (github.com/SkriptLang/Skript), supporting Paper 1.21+, with Folia in experimental branches only. Below we cover install, .sk file structure, syntax, addons (skBee, skript-yaml, skript-reflect, skquery) and the usual performance landmines.

What Skript actually is and who it really helps

Skript is an interpreter plugin. It reads .sk text files, parses them as a DSL that looks like English, and turns them into Bukkit/Paper events, commands and effects. For an admin that means: you can build a mechanic in one evening that would otherwise need a custom plugin or five overlapping ones from SpigotMC.

Who benefits the most:

  • Small SMP admin who needs 3 to 5 custom commands and a few triggers.
  • Public server admin who wants to roll out events fast (double XP weekend, kill streaks, easter eggs).
  • Developer prototyping an idea before committing to a full Java plugin.

Who should look elsewhere: builders of MMORPG combat systems with thousands of concurrent players and dense per-tick logic. Skript is fine for typical SMP and minigame loads, but if every event runs through dozens of conditions, the hot path is better in Java.

Install and your first /sk reload

Grab the latest stable Skript from GitHub releases at github.com/SkriptLang/Skript/releases or Modrinth at modrinth.com/plugin/skript. In 2026 that means Skript 2.9.x for Paper 1.21+.

Drop the JAR into plugins/, start the server once so the plugin creates its folder. Layout after first boot:

plugins/Skript/
├── config.sk              # global config
├── aliases-english.sk     # item names
├── scripts/
│   ├── -disabled-script.sk   # leading dash means disabled
│   └── examples/
└── lang/

Create your first file plugins/Skript/scripts/welcome.sk:

on join:
    send "&aWelcome %player% to the server!" to player
    add 1 to {joins::%player's uuid%}

In console or in-game with skript.admin:

/sk reload welcome

Skript replies with how many triggers loaded and whether errors fired. Join the server, see the green welcome line. That is your first working script.

.sk file structure: events, commands, functions

A .sk file is a stack of top-level blocks. Each block starts at column zero, and everything inside is indented exactly 4 spaces or 1 tab. Skript is strict about indentation: mixing tabs and spaces in the same file throws inconsistent indentation and the file refuses to load.

The three main block types:

1. Event handler. Fires on a server event.

on death of a player:
    broadcast "&c%victim% died at %location of victim%"

2. Command. Registers a custom command.

command /fly [<player>]:
    permission: server.fly
    permission message: &cYou cannot fly here.
    trigger:
        if arg-1 is set:
            set {_target} to arg-1
        else:
            set {_target} to player
        if {_target} is flying:
            set flight mode of {_target} to false
            send "&7Flight disabled for &e%{_target}%" to player
        else:
            set flight mode of {_target} to true
            send "&aFlight enabled for &e%{_target}%" to player

3. Function. Reusable logic, like a procedure.

function welcomeMessage(p: player):
    send "&aWelcome back, %{_p}%!" to {_p}
    if difference between {lastSeen::%{_p}'s uuid%} and now is greater than 7 days:
        send "&7You were gone for a while. Server is on Paper 1.21.4 now." to {_p}
    set {lastSeen::%{_p}'s uuid%} to now

on join:
    welcomeMessage(player)

The filename does not change behavior, only /sk reload <file> ergonomics. A leading - on the filename disables the script without deleting it.

Data types: player, item, location, block, vector, number, text

Skript is typed but inference does most of the work for you. The basics:

TypeSample valueWhere it shows up
playerplayer, arg-1, victimevent handlers, command targets
item1 diamond, player's toolinventory, drops
locationlocation of player, block's locationteleport, mob spawn
blocktargeted block, event-blockplacement, break
vectorvector(0, 1, 0)velocity, knockback
number42, player's healthcounters, coords
text"hello", "&a%player%"messages, names
entityattacker, loop-entitymobs, NPCs
worldworld "world_nether"world checks
timespan5 seconds, 2 minutesdelays, cooldowns
chatcolorred, &cformatting

Conversions happen via expressions like block at location, location of entity, name of item. No explicit casting needed.

A real on join handler

Realistic welcome script with first-join detection, VIP variants and online tracking:

on first join:
    broadcast "&6%player% &ejoined for the first time! Welcome!"
    give 1 stone pickaxe of efficiency 1 to player
    give 16 cooked beef to player
    teleport player to {spawn}

on join:
    if player has permission "server.vip":
        send "&6&l[VIP] &eWelcome back, %player%" to player
    else:
        send "&aWelcome back, %player%" to player
    add 1 to {stats::joins::%player's uuid%}
    set {stats::lastjoin::%player's uuid%} to now
    wait 2 ticks
    set tab list header "&aServer Online" and footer "&7Players: %size of all players%" for player

on quit:
    set {stats::lastquit::%player's uuid%} to now

first join fires once per UUID, join fires every login. They do not conflict, both run for a brand-new player.

Custom commands: /heal with perms and arguments

Skript registers commands automatically when scripts load, you do not touch plugin.yml.

command /heal [<player>] [<integer>]:
    description: Heals a player to full or to a specific amount
    aliases: /h
    permission: server.heal
    permission message: &cMissing permission &7server.heal
    cooldown: 30 seconds
    cooldown message: &cWait %remaining time% before using /heal again.
    cooldown bypass: server.heal.bypass
    trigger:
        if arg-1 is set:
            if sender does not have permission "server.heal.others":
                send "&cYou cannot heal others." to sender
                stop
            set {_target} to arg-1
        else:
            if sender is not a player:
                send "&cConsole must specify a player." to sender
                stop
            set {_target} to sender
        if arg-2 is set:
            set health of {_target} to arg-2
        else:
            heal {_target}
        feed {_target}
        send "&aHealed %{_target}%" to sender
        if {_target} is not sender:
            send "&aYou were healed by &e%sender%" to {_target}

Things worth noting: cooldown is built in, permission message shows on missing perm, aliases adds /h, stop aborts the trigger. Arguments come through arg-1, arg-2.

Variables: local, list, persistent

Skript has three kinds:

Local ({_name}) lives only inside the current trigger or function:

on damage:
    set {_dmg} to damage
    set {_attacker} to attacker
    if {_dmg} is greater than 10:
        send "Big hit by %{_attacker}%" to victim

Global ({name}) gets persisted to variables.csv and survives restarts:

set {spawn} to location of player
set {server.motd} to "&aPaper 1.21.4"

List/indexed ({name::%key%} or {name::*}) is effectively a dict:

add 1 to {kills::%attacker's uuid%}
loop {kills::*}:
    send "%loop-index%: %loop-value%" to player
clear {kills::%player's uuid%}
delete {kills::%player's uuid%}

Important habit: always key off uuid, not name. Players can change names and you lose data. Also do not stuff heavy objects like full inventories into persistent variables casually, the file balloons fast.

For serious projects, move persistent state into SQL via skript-db or YAML via skript-yaml. The CSV holds up to 50k to 100k entries before load times start hurting.

Useful addons: skBee, skript-yaml, skript-reflect, skquery

Vanilla Skript covers maybe 70% of what you want. The rest is addons.

AddonPurposeLink
skBeeNBT, scoreboards, world borders, recipes, fastboards, particlesgithub.com/ShaneBeee/skBee
skript-yamlYAML read and writegithub.com/SkriptLang/skript-yaml
skript-reflectDirect Java reflection (raw Bukkit API)github.com/TPGamesNL/skript-reflect
skqueryOlder extensions, partly overlaps with skBeegithub.com/Tuke-Nuke/SkQuery
skript-placeholdersPlaceholderAPI bridgegithub.com/APickledWalrus/skript-placeholders
skript-dbMySQL/SQLite queriesgithub.com/btk5h/skript-db

In 2026 skBee 3.x covers most of what skquery used to do plus modern Paper APIs. If you only install one addon, make it skBee.

skBee scoreboard example:

on join:
    create new scoreboard named "stats_%player's uuid%"
    set line 1 of player's scoreboard to "&7&m-----------"
    set line 2 of player's scoreboard to "&aKills: &f%{kills::%player's uuid%} ? 0%"
    set line 3 of player's scoreboard to "&cDeaths: &f%{deaths::%player's uuid%} ? 0%"
    set line 4 of player's scoreboard to "&7&m-----------"
    set title of player's scoreboard to "&6Server Stats"
    set player's displayed scoreboard to player's scoreboard

Common patterns: kill counter, tiny economy, welcome

Kill counter with leaderboard.

on death of a player:
    if attacker is a player:
        add 1 to {kills::%attacker's uuid%}
        add 1 to {deaths::%victim's uuid%}
        send "&a+1 kill &7(total: %{kills::%attacker's uuid%}%)" to attacker

command /killtop:
    trigger:
        send "&6&l=== Top 10 Killers ===" to player
        set {_sorted::*} to sorted indexes of {kills::*} in descending order
        loop {_sorted::*}:
            if loop-iteration > 10:
                stop loop
            set {_uuid} to loop-value
            set {_name} to name of offline player from uuid {_uuid}
            send "&e%loop-iteration%. &f%{_name}% &7- &c%{kills::%{_uuid}%}% kills" to player

Tiny economy, no Vault.

on join:
    if {money::%player's uuid%} is not set:
        set {money::%player's uuid%} to 100

command /balance [<player>]:
    aliases: /bal, /money
    trigger:
        if arg-1 is set:
            set {_t} to arg-1
        else:
            set {_t} to player
        send "&aBalance of &e%{_t}%&a: &6%{money::%{_t}'s uuid%} ? 0%$" to player

command /pay <player> <integer>:
    trigger:
        if arg-1 is sender:
            send "&cYou cannot pay yourself." to sender
            stop
        if arg-2 is less than 1:
            send "&cAmount must be positive." to sender
            stop
        if {money::%sender's uuid%} is less than arg-2:
            send "&cNot enough money." to sender
            stop
        remove arg-2 from {money::%sender's uuid%}
        add arg-2 to {money::%arg-1's uuid%}
        send "&aPaid &6%arg-2%$ &ato &e%arg-1%" to sender
        send "&aReceived &6%arg-2%$ &afrom &e%sender%" to arg-1

Performance and pitfalls

Skript is powerful, but naive code wrecks TPS. Hard rules:

1. Do not loop all players every tick.

Bad:

every tick:
    loop all players:
        set action bar of loop-player to "&aServer"

Good:

every 5 seconds:
    loop all players:
        set action bar of loop-player to "&aServer"

A tick is 20 times per second. Action bars at 2 to 4 Hz feel identical and cost an order of magnitude less.

2. Async wherever possible.

Heavy work (HTTP calls, big YAML reads, SQL) goes inside async:

command /lookup <text>:
    trigger:
        send "&7Looking up..." to player
        set {_p} to player
        execute async:
            set {_data} to text from "https://api.example.com/lookup/%arg-1%"
            execute sync:
                send "&aResult: &f%{_data}%" to {_p}

Without async, the server freezes for the full request duration.

3. Do not stuff items and inventories into persistent vars unless you mean it. Serializing them is expensive and bloats variables.csv. For loot crates, store the recipe (type plus amount plus meta), not the live ItemStack.

4. Prefer wait over nested every blocks. Bad: every 1 second: inside on join:. Good: wait 1 second or a timer driven by a variable.

5. Profile with spark. /spark profiler --thread server shows whether Skript eats your tick. Lots of Trigger.execute frames in the stack means you have a heavy event handler somewhere, almost always an every tick or a fat on damage.

Debugging: /sk reload, /sk debug

Core admin commands, perm skript.admin:

  • /sk reload all reloads every script.
  • /sk reload <name> loads a single file (no .sk).
  • /sk reload config re-reads config.sk without restarting the plugin.
  • /sk disable <name> prefixes the file with a dash and unloads it.
  • /sk enable <name> reverses that.
  • /sk info shows version and addon list.
  • /sk update check polls for updates.

When a script fails to load, errors hit the console with the exact line. Usual suspects: mismatched indentation, typo in event name, missing addon for a syntax you copied off a forum.

For deeper debugging, sprinkle telemetry:

on damage:
    broadcast "DEBUG: %attacker% hit %victim% for %damage% (final: %final damage%)"
    if attacker is a player:
        broadcast "DEBUG: weapon = %attacker's tool%, projectile = %projectile%"

In production, replace broadcast with send to console or just remove it. There is also the skUnity Parser for syntax checks without touching the live server.

FAQ

Is Skript fine for a big public server?

For a typical PvP/SMP/minigame setup at 100 to 300 concurrent it is fine if you respect the perf rules. At 1000+ concurrent with thousands of events per second, port the hot paths to Java and keep Skript for one-off events and custom commands.

Where do I find ready-made scripts?

skUnity Forums, SkriptHub, GitHub. Read before you deploy. Random forum scripts often hide an every tick over all players that will tank your TPS.

Why does Skript yell inconsistent indentation?

You mixed tabs and spaces. Open the file with whitespace rendering on (VS Code: View > Render Whitespace) and pick one. 4 spaces is the safe default.

What happens to variables on a hard crash?

Skript flushes variables.csv on an interval, default 5 minutes (configurable in config.sk as save interval). Tighten it to 1 minute if you can absorb the IO. For real durability, push critical state through skript-db into SQL or rsync the CSV every 10 minutes.

Does Skript run on Folia?

As of 2026 mainline SkriptLang/Skript does not fully support Folia, only experimental branches. Region-aware servers work with caveats (no cross-region access without the right scheduler). Check github.com/SkriptLang/Skript/issues for the live status before betting your server on it.

How do I protect a Skript-heavy server from DDoS?

Skript runs at the game-event layer and does nothing for L4 floods. Put a network filter in front, like MineGuard. Skript is in-game logic, not packet plumbing.

Skript is still the fastest way to turn an idea into a real mechanic without spinning up a Java project. Install it, write your first welcome.sk, then /heal, then a kill counter, and within a week you have a stack of custom logic tuned to your server. Just keep every tick away from heavy loops and remember async for anything that touches disk or network.


Protect Your Server from DDoS Attacks

Free protection with 5-minute setup. 1 TB bandwidth included.

Try for Free


Related Articles