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:
| Type | Sample value | Where it shows up |
|---|---|---|
player | player, arg-1, victim | event handlers, command targets |
item | 1 diamond, player's tool | inventory, drops |
location | location of player, block's location | teleport, mob spawn |
block | targeted block, event-block | placement, break |
vector | vector(0, 1, 0) | velocity, knockback |
number | 42, player's health | counters, coords |
text | "hello", "&a%player%" | messages, names |
entity | attacker, loop-entity | mobs, NPCs |
world | world "world_nether" | world checks |
timespan | 5 seconds, 2 minutes | delays, cooldowns |
chatcolor | red, &c | formatting |
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.
| Addon | Purpose | Link |
|---|---|---|
| skBee | NBT, scoreboards, world borders, recipes, fastboards, particles | github.com/ShaneBeee/skBee |
| skript-yaml | YAML read and write | github.com/SkriptLang/skript-yaml |
| skript-reflect | Direct Java reflection (raw Bukkit API) | github.com/TPGamesNL/skript-reflect |
| skquery | Older extensions, partly overlaps with skBee | github.com/Tuke-Nuke/SkQuery |
| skript-placeholders | PlaceholderAPI bridge | github.com/APickledWalrus/skript-placeholders |
| skript-db | MySQL/SQLite queries | github.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 allreloads every script./sk reload <name>loads a single file (no.sk)./sk reload configre-readsconfig.skwithout restarting the plugin./sk disable <name>prefixes the file with a dash and unloads it./sk enable <name>reverses that./sk infoshows version and addon list./sk update checkpolls 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 FreeRelated Articles
Folia: Complete Guide to Multithreaded Minecraft Server
Folia by PaperMC splits the world into regions and ticks each one on a separate thread. We break down when Folia actually makes sense, how to set it up, and which plugins are compatible.
TCPShield vs MineGuard: Honest DDoS Protection Comparison for Minecraft in 2026
A detailed comparison of two popular Minecraft DDoS protection services. Features, pricing, support, and how to choose.
BetonQuest: Minecraft Server Quest Setup (Complete 2026 Guide)
Full BetonQuest 2.x guide: installation, package layout, conversations, objectives, conditions, events, journal, 1.x migration and TPS bottlenecks.