-
Notifications
You must be signed in to change notification settings - Fork 0
Examples
Real, runnable examples that mix the features from Lambdas, Predicates, List operations, Listeners and Async.
Each example is one command you can paste into a .sk file and try in-game.
Mine 10 stone in 30 seconds. Win a diamond, or get scolded.
command /challenge:
trigger:
send "mine 10 stone in 30s" to player
listen for block break where event-block is stone:
countdown: 30 seconds
triggers: 10
on trigger:
send "keep going... (%remaining triggers% left)" to event-player
on completion:
send "you did it!" to event-player
give 1 diamond to event-player
on timeout:
send "too slow!" to event-playerWhat this shows:
-
triggers:counts events for you. -
countdown:ends the listener if the player is too slow. -
remaining triggerstells the player how much is left.
You get protection for 30 seconds or 5 hits, whichever happens first. Eating a golden apple gives you more hits and more time.
command /shield:
trigger:
set {_p} to sender
send "shielded for 30s or 5 hits. eat a golden apple to extend." to {_p}
set {shield::%{_p}%} to listener for damage where victim is {_p}:
countdown: 30 seconds
triggers: 5
on trigger:
cancel event
send "blocked! (%remaining triggers% hits, %remaining countdown% left)" to {_p}
on completion:
send "shield used up" to {_p}
on timeout:
send "shield expired" to {_p}
register {shield::%{_p}%}
set {shield_upgrade::%{_p}%} to listener for consume where event-player is {_p}:
on trigger:
if event-item is not golden apple:
skip trigger
add 1 to {shield::%{_p}%}'s triggers
add 10 seconds to {shield::%{_p}%}'s countdown
send "shield upgraded!" to {_p}
register {shield_upgrade::%{_p}%}What this shows:
- Saving a listener in a variable and starting it with
register. -
skip triggerto ignore the wrong food. - One listener changes another listener's
triggersandcountdown.
Break a diamond ore in 60 seconds. The first one ends the game.
command /firstdiamond:
trigger:
set {_p} to sender
send "break a diamond ore within 60s to win" to {_p}
listen for block break where event-player is {_p}:
countdown: 60 seconds
on trigger:
if event-block is diamond ore:
give 1 diamond to {_p}
send "you got one!" to {_p}
cancel listener
on timeout:
send "time's up" to {_p}What this shows:
-
cancel listenerto stop early when a goal is met. - No
triggers:is needed. The listener ends either bycancel listeneror by timeout.
Pick a number between 1 and 20. You get 5 guesses or 45 seconds.
command /guess:
trigger:
set {_p} to sender
set {target::%{_p}%} to random integer between 1 and 20
send "I'm thinking of a number 1-20. you have 5 guesses, 45s." to {_p}
listen for chat where player is {_p}:
countdown: 45 seconds
triggers: 5
on trigger:
cancel event
if message parsed as integer is not set:
send "not a number, try again" to {_p}
skip trigger
set {_n} to message parsed as integer
if {_n} is {target::%{_p}%}:
send "right! (%remaining countdown% left)" to {_p}
give 1 diamond to {_p}
cancel listener
else if {_n} < {target::%{_p}%}:
send "higher, %remaining triggers% guesses left" to {_p}
else:
send "lower, %remaining triggers% guesses left" to {_p}
on completion:
send "out of guesses (number was %{target::%{_p}%}%)" to {_p}
on timeout:
send "time's up (number was %{target::%{_p}%}%)" to {_p}What this shows:
-
skip triggerto ignore invalid input without using up a guess. -
cancel listenerto end early on a win. -
remaining triggersandremaining countdownused together.
Mine 10 stone. After 5, the listener pauses for a 10-second break. Then it comes back.
command /milestone:
trigger:
set {_p} to sender
send "break 10 stone. it pauses for 10s at the halfway point." to {_p}
set {milestone::%{_p}%} to listener for block break where event-player is {_p}:
countdown: 60 seconds
triggers: 10
on trigger:
if remaining triggers is 5:
send "halfway! pausing 10s..." to {_p}
pause {milestone::%{_p}%}
wait 10 seconds
if {milestone::%{_p}%} is registered:
resume {milestone::%{_p}%}
send "go!" to {_p}
on completion:
send "all 10 done!" to {_p}
on timeout:
send "ran out of time" to {_p}
register {milestone::%{_p}%}What this shows:
-
pauseandresumeon a saved listener. - The countdown is frozen during pause, so the break doesn't eat into the time limit.
- Checking
is registeredbefore resuming, in case the player ended things another way.
A player counts as admin only if they pass every check in the list.
command /adminsetup:
trigger:
clear {is-admin::*}
add lambda (p: player): {_p} is op to {is-admin::*}
add lambda (p: player): name of {_p} is "eult" to {is-admin::*}
send "admin checks ready" to sender
command /admincheck:
trigger:
if {is-admin::*} passes for player: # true only if every check passes
send "welcome, admin" to player
if {is-admin::*} doesn't pass for player: # true if any check fails
send "not quite admin" to playerWhat this shows:
- Inline predicates stored in a list.
-
passes forto test all of them at once. -
doesn't passfor the opposite check.
Make a predicate once, then reuse it as the where filter.
command /stonewatch:
trigger:
set {is-stone} to lambda (b: block): {_b} is stone
send "break some stone..." to player
listen for block break where {is-stone} passes for event-block:
triggers: 5
on trigger:
send "stone! (%remaining triggers% to go)" to event-player
on completion:
send "nice, 5 stone broken" to event-playerWhat this shows:
- A predicate used inside
where. - The same idea works for any event with any value you can test.
Turn an existing function into a lambda you can store and pass around.
function double(amount: number) :: number:
return {_amount} * 2
command /functionlambda:
trigger:
set {_reward} to function lambda "double"
set {_x} to call lambda {_reward} with 5
send "double(5) = %{_x}%" to playerWhat this shows:
-
function lambda "name"to reuse a function as a lambda. - After that, it behaves like any other lambda.
Spawn a couple of watchers, then stop them in bulk. Use /sklambda listeners to see what's alive.
command /spawnwatchers:
trigger:
listen for block break:
on trigger:
send "block broken" to player
listen for chat:
on trigger:
send "chat seen" to player
send "two watchers running, try /sklambda listeners" to player
command /undolast:
trigger:
unregister the last created listener
send "stopped the last watcher" to sender
command /listenerpanic:
trigger:
unregister all listeners # stops EVERY listener on the server
send "all listeners cleared" to senderWhat this shows:
-
unregister the last created listenerto undo just the most recent one. -
unregister all listenersto clear everything (server-wide, so use with care). -
/sklambda listenersto inspect what's still running. See Configuration.
Give each player their own watchers with owner:. They clean up on their own when the player leaves, or you can drop just one player's with owned by.
command /watchme:
trigger:
set {_p} to sender
send "watching your blocks & chat, these auto-clean when you leave." to {_p}
listen for block break:
owner: {_p} # auto-unregisters when {_p} disconnects
where:
event-player is {_p}
on trigger:
send "you broke %event-block%" to {_p}
listen for chat:
owner: {_p}
where:
player is {_p}
on trigger:
send "you said: %message%" to {_p}
command /stopwatching:
trigger:
set {_p} to sender
unregister all listeners owned by {_p} # only this player's, others keep running
send "stopped watching you" to {_p}What this shows:
-
owner:to tie a listener to a player, so it ends itself when they leave. -
unregister all listeners owned by {_p}to clean up one owner's listeners only.
on end: runs however the listener stops, so cleanup lives in exactly one place. end reason tells you why it ended.
command /session:
trigger:
set {_p} to sender
set {session::%{_p}%} to true
send "session started: break 5 blocks in 20s. /endsession to stop early." to {_p}
set {watch::%{_p}%} to listener for block break where player is {_p}:
countdown: 20 seconds
triggers: 5
on trigger:
send "%remaining triggers% blocks left" to {_p}
on completion:
send "all 5 broken!" to {_p}
on timeout:
send "ran out of time" to {_p}
on end: # runs no matter how it ended
delete {session::%{_p}%} # single cleanup path
if end reason is completion:
send "session ended: you won" to {_p}
else if end reason is timeout:
send "session ended: timed out" to {_p}
else:
send "session ended: %end reason%" to {_p}
register {watch::%{_p}%}
command /endsession:
trigger:
set {_p} to sender
if {watch::%{_p}%} is registered:
unregister {watch::%{_p}%} # on end fires with end reason = unregistered
send "stopped your session" to {_p}What this shows:
-
on end:as a single teardown path, however the listener stops. -
end reasonto branch on why it ended (completion, timeout, or unregistered).
Constant predicates that ignore their input. Swap one for the other to flip a gate, or use them as list filters.
command /gatecheck:
trigger:
set {_p} to sender
set {_gate} to always() # swap to never() to close the gate for everyone
if {_gate} passes for {_p}:
send "gate open" to {_p}
else:
send "gate closed" to {_p}
set {_kept::*} to all players where [always() passes for input] # keeps everyone
set {_dropped::*} to all players where [never() passes for input] # keeps no one
send "always() kept %size of {_kept::*}%, never() kept %size of {_dropped::*}%" to {_p}What this shows:
-
always()/never()as drop-in predicates anywhere a predicate is expected. - A feature flag you flip by swapping a single line.
Map, reduce, and sort with a lambda; filter, count, and find with a predicate. Let the addon (and Skript core) do the looping.
command /listops:
trigger:
set {_p} to sender
set {_nums::*} to 3, 5, 2, and 8
# map: transform every element
set {_double} to lambda (n: number) -> number:
return {_n} * 2
set {_doubled::*} to {_nums::*} mapped with {_double}
send "doubled: %{_doubled::*}%" to {_p} # 6, 10, 4, 16
# filter: keep only matching elements (Skript's own `where`, fed a predicate)
set {_big} to lambda (n: number): {_n} > 3
set {_kept::*} to {_nums::*} where [{_big} passes for input]
send "over 3: %{_kept::*}%" to {_p} # 5, 8
# reduce: fold the whole list to one value
set {_add} to lambda (a: number, b: number) -> number:
return {_a} + {_b}
set {_total} to {_nums::*} reduced with {_add}
send "sum = %{_total}%" to {_p} # 18
# count / first: same predicate via Skript's `number of` and `first element of`
send "%number of ({_nums::*} where [{_big} passes for input])% numbers over 3" to {_p}
send "first over 3: %first element of ({_nums::*} where [{_big} passes for input])%" to {_p}
# sort: order a list by a key the lambda pulls out of each element
set {_score} to lambda (pl: player) -> number:
return {_pl}'s level
set {_ranked::*} to all players sorted by {_score} # lowest level first
send "ranked by level: %{_ranked::*}%" to {_p}What this shows:
- One stored lambda or predicate, applied across a whole list.
-
mapped with/reduced with/sorted bytake value lambdas; filtering, counting, and finding pair a predicate with Skript's ownwhere [... passes for input]. See List operations.
every refreshes a display on a timer while the listener runs. cooldown ignores rapid repeats so they don't count.
command /shieldbar:
trigger:
set {_p} to sender
send "shield up: survive 30s or block 5 hits. rapid hits are debounced to 1/sec." to {_p}
listen for damage where victim is {_p}:
countdown: 30 seconds
triggers: 5
cooldown: 1 second # at most one counted hit per second
on trigger:
cancel event
send "blocked! (%remaining triggers% hits left)" to {_p}
every 1 second: # live action-bar readout
send action bar "shield %remaining countdown% | %remaining triggers% hits left" to {_p}
on completion:
send "shield shattered, took 5 hits" to {_p}
on timeout:
send "shield held for the full 30s" to {_p}
on end:
send action bar "" to {_p} # clear the bar however it endedWhat this shows:
-
every 1 second:for a live display that pauses and stops with the listener. -
cooldown:to debounce rapid events, so they don't fireon triggerand don't count towardtriggers. -
on end:to clear the display however the listener stopped.
A timed mining sprint a staff member can freeze for a break. The countdown and the live display both hold while paused, so the break doesn't eat into the clock.
command /sprint:
trigger:
set {_p} to sender
send "mine 15 blocks in 60s. staff can /freeze you for a break." to {_p}
set {sprint::%{_p}%} to listener for block break where player is {_p}:
countdown: 60 seconds
triggers: 15
every 1 second: # live readout while running
send action bar "%remaining triggers% blocks | %remaining countdown% left" to {_p}
on trigger:
send "nice (%remaining triggers% to go)" to {_p}
on pause: # countdown freezes here
send action bar "PAUSED: %remaining countdown% on the clock" to {_p}
send "sprint frozen for a break" to {_p}
on resume:
send "back on, go!" to {_p}
on completion:
send "all 15 done!" to {_p}
on timeout:
send "ran out of time" to {_p}
on end: # runs no matter how it stopped
send action bar "" to {_p} # clear the live bar
register {sprint::%{_p}%}
command /freeze <player>:
trigger:
if {sprint::%arg-1%} is running:
pause {sprint::%arg-1%} # fires on pause, countdown stops ticking
send "froze %arg-1%'s sprint" to sender
else if {sprint::%arg-1%} is paused:
resume {sprint::%arg-1%} # fires on resume
send "unfroze %arg-1%'s sprint" to senderWhat this shows:
-
on pause:/on resume:to react when the listener is frozen and unfrozen. - The countdown and the
everytimer are held during a pause, so a break doesn't burn time. -
is runningvsis pausedto make/freezetoggle.
owner: works for more than players now. A listener auto-unregisters the moment its owner leaves: an entity dies or leaves the loaded world, or a chunk/world unloads. Each watcher's on end: fires with end reason = unregistered. (Players on disconnect are covered by example 10.)
# entity owner: a watcher bound to a spawned zombie, it dies with the zombie
command /entityguard:
trigger:
set {_p} to sender
spawn a zombie at location of {_p}
set {_zombie} to last spawned entity
send "<yellow>spawned a zombie, hit it. the watcher auto-cleans when the zombie dies." to {_p}
listen for damage:
owner: {_zombie} # auto-unregisters when the zombie leaves the world
where:
victim is {_zombie}
on trigger:
wait 1 tick # ensure the damage applies before we check health
send "<aqua>your zombie took a hit (%health of {_zombie}% hp left)" to {_p}
on end:
send "<gray>zombie gone (%end reason%), watcher cleaned itself up" to {_p}
# chunk owner: a watcher bound to the chunk you're standing in, it unloads with the chunk
command /chunkwatch:
trigger:
set {_p} to sender
set {_chunk} to chunk at location of {_p}
send "<yellow>watching this chunk for block breaks. walk away + let it unload to auto-clean." to {_p}
listen for block break:
owner: {_chunk} # auto-unregisters when this chunk unloads
where:
chunk at location of event-block is {_chunk}
on trigger:
send "<aqua>block broken in the watched chunk" to {_p}
on end:
send "<gray>chunk watcher ended (%end reason%)" to {_p}
# world owner: a watcher bound to a world, it stops if that world is unloaded
command /worldwatch:
trigger:
set {_p} to sender
set {_w} to world of {_p}
send "<yellow>watching '%{_w}%' for lightning. unloading the world auto-cleans it." to {_p}
listen for lightning strike:
owner: {_w} # auto-unregisters when this world unloads
where:
event-world is {_w}
on trigger:
send "<aqua>lightning struck in %{_w}%" to {_p}
on end:
send "<gray>world watcher ended (%end reason%)" to {_p}What this shows:
-
owner:accepting an entity, chunk, or world, not just a player. - Auto-cleanup tied to the owner's lifecycle, with
on end:reportingend reason=unregistered.
A bigger, real-world build: sneak-mine an ore and the whole connected vein breaks. The flood-fill is a lambda that captures the ore set from on load and recurses by passing itself as its first argument.
options:
veinminer_enabled: true
veinminer_max_size: 128 # safety cap on vein size, prevents runaway recursion
on load:
if {veinminer::listener} is registered:
unregister {veinminer::listener}
parse if {@veinminer_enabled} is true:
set {veinminer::ores::*} to tag values of tag "ores"
add Andesite, Diorite and Granite to {veinminer::ores::*}
set {_flood} to lambda (self: object, loc: location, ore: itemtype, miner: player) -> object:
if {veinminer::mined::%{_miner}'s uuid%} >= {@veinminer_max_size}:
return 0
set {_min} to {_loc} offset by vector(-1, -1, -1)
set {_max} to {_loc} offset by vector(1, 1, 1)
loop blocks within {_min} and {_max}:
if loop-block is {_ore}:
break loop-block using {_miner}'s tool # drops respect fortune / silk touch
add 1 to {veinminer::mined::%{_miner}'s uuid%}
run lambda {_self} with {_self}, location of loop-block, {_ore}, {_miner}
return 0
set {veinminer::listener} to listener for block break:
on trigger:
if event-player is not sneaking: # hold sneak to vein-mine
skip trigger
# find which configured ore was mined (canonical block-vs-itemtype match)
delete {_ore}
loop {veinminer::ores::*}:
if event-block is loop-value:
set {_ore} to loop-value
if {_ore} is not set: # not a vein-mineable ore
skip trigger
cancel event # we break the whole vein ourselves
set {veinminer::mined::%event-player's uuid%} to 0
# {_flood} is captured from `on load`; pass it to itself to kick off the recursion
run lambda {_flood} with {_flood}, location of event-block, {_ore}, event-player
delete {veinminer::mined::%event-player's uuid%}
register {veinminer::listener}What this shows:
- A lambda that recurses by taking itself as a parameter (
{_self}) and re-running it. - A lambda captured at parse/load time and used much later inside a listener's
on trigger:. - A
triggers-free server-wide listener re-armed cleanly on every reload.
One-liners can return a value now, with return, or just a bare expression.
command /inlinevalue:
trigger:
set {_p} to sender
set {_add} to lambda (a: object, b: object): return {_a} + {_b}
set {_double} to lambda (n: number): {_n} * 2
send "<green>add(3, 4) = %call lambda {_add} with 3, 4%" to {_p} # 7
send "<green>double(21) = %call lambda {_double} with 21%" to {_p} # 42
# because they're values, they drop straight into the list ops from example 13
set {_nums::*} to 3, 5, and 8
send "<aqua>doubled: %{_nums::*} mapped with {_double}%" to {_p} # 6, 10, 16What this shows:
- A one-line lambda returning a value, both with
returnand as a bare expression. - The result slotting straight into
mapped with. See Lambdas.
A lambda closes over the local variables around it: a by-value snapshot is taken when it's created and stays readable inside the body when it's called later, even from another trigger. A parameter shadows a captured local of the same name.
command /closures:
trigger:
set {_p} to sender
# a lambda factory: each returned adder closes over its own {_step}
set {_make_adder} to lambda (step: number) -> object:
set {_adder} to lambda (n: number) -> number:
return {_n} + {_step} # {_step} captured from this factory call
return {_adder}
set {_add5} to call lambda {_make_adder} with 5
set {_add100} to call lambda {_make_adder} with 100
send "<green>add5(10) = %call lambda {_add5} with 10%" to {_p} # 15
send "<green>add100(10) = %call lambda {_add100} with 10%" to {_p} # 110
# a parameter named like a captured local wins: this returns the PARAMETER, not any outer {_step}
set {_shadow} to lambda (step: number) -> number:
return {_step}
send "<gray>param shadows capture: %call lambda {_shadow} with 42%" to {_p} # 42
# the registry pattern: {_sum} is a LOCAL, but the snapshot keeps it alive inside {-combine}
set {_sum} to lambda (a: number, b: number) -> number:
return {_a} + {_b}
set {-combine} to lambda (n: number) -> number:
set {_nums::*} to 10, 20, and {_n}
return {_nums::*} reduced with {_sum} # {_sum} captured at definition time
send "<aqua>combine(5) = %call lambda {-combine} with 5%" to {_p} # 35What this shows:
- A factory whose returned lambdas each close over their own captured value.
- A parameter shadowing a captured local of the same name.
- A captured local kept alive inside a global-stored lambda for later calls. See Lambdas.
Pre-fill leading arguments with %lambda% with %args% bound, and flip a predicate with negated %lambda%. Both compose with everything else.
command /combinators:
trigger:
set {_p} to sender
# bind: pre-fill the first argument, get a one-arg lambda back
set {_add} to lambda (a: number, b: number) -> number:
return {_a} + {_b}
set {_add5} to {_add} with 5 bound
send "<green>add5(10) = %call lambda {_add5} with 10%" to {_p} # 15
send "<green>add5(100) = %call lambda {_add5} with 100%" to {_p} # 105
# bind every argument -> a zero-arg lambda (a thunk)
set {_answer} to {_add} with (40, 2) bound
send "<green>answer() = %call lambda {_answer}%" to {_p} # 42
# negated: flip a predicate
set {_is-op} to lambda (pl: player): {_pl} is op
set {_not-op} to negated {_is-op}
if {_not-op} passes for {_p}:
send "<gray>negated: you are NOT op" to {_p}
else:
send "<gold>negated: you ARE op" to {_p}
# negated drops straight into Skript's filter
set {_visitors::*} to all players where [{_not-op} passes for input]
send "<aqua>%size of {_visitors::*}% non-op player(s) online" to {_p}What this shows:
-
boundfor partial application, including binding every argument into a thunk. See Lambdas. -
negatedto flip a predicate and reuse it in Skript'swherefilter. See Predicates.
on register: runs when the listener becomes active; on end: runs when it stops. For a declared listener the setup fires on register, not at definition.
command /onregister:
trigger:
set {_p} to sender
send "<yellow>arming a watcher with matched setup + teardown..." to {_p}
set {reg_demo::%{_p}%} to listener for block break where player is {_p}:
owner: {_p} # makes it "yours": shows up under `listeners owned by` and auto-cleans on quit
triggers: 3
on register:
send "<green>[setup] watcher armed, break 3 blocks" to {_p}
on trigger:
send "<aqua>%remaining triggers% blocks left" to {_p}
on completion:
send "<gold>all 3 broken!" to {_p}
on end:
send "<gray>[teardown] watcher gone (%end reason%)" to {_p}
register {reg_demo::%{_p}%} # <- 'on register' fires here, not at the `set` aboveWhat this shows:
-
on register:as the setup partner toon end:. - For a saved listener, setup firing at
register, not at theset. See Listeners.
all active listeners and listeners owned by %object% are real lists you can loop, count, or feed to unregister. Note: "owned by" means the listener declared an owner: entry. A where player is {_p} filter does not make it yours. Run /watchme (example 10) or /onregister (example 21) first, since both set owner: {_p}, to see your count climb.
command /listenerquery:
trigger:
set {_p} to sender
send "<gray>%size of all active listeners% listener(s) active server-wide" to {_p}
send "<gray>%size of listeners owned by {_p}% of them are yours" to {_p}
# cleanup via the query + a loop (equivalent to `unregister all listeners owned by {_p}`)
loop listeners owned by {_p}:
unregister loop-value
send "<gray>cleared yours, %size of listeners owned by {_p}% left" to {_p}What this shows:
-
all active listenersandlisteners owned byas countable, loopable lists. - Bulk cleanup by looping the query and unregistering each. See Listeners.
highest/lowest of … by returns the element with the smallest/largest key, and reduced with … from %seed% runs for every element starting from a seed (safe on empty lists, and the seed may be a different type).
command /aggregates:
trigger:
set {_p} to sender
set {_nums::*} to 3, 5, 2, and 8
# seeded reduce: lambda runs for every element, starting from the seed
set {_add} to lambda (a: number, b: number) -> number:
return {_a} + {_b}
send "<green>sum from 100 = %{_nums::*} reduced with {_add} from 100%" to {_p} # 118
clear {_empty::*}
send "<green>empty from 0 = %{_empty::*} reduced with {_add} from 0%" to {_p} # 0 (lambda never runs)
# min/max by a key lambda: returns the element with the smallest/largest key
set {_abs} to lambda (n: number) -> number:
return abs({_n})
set {_signed::*} to -7, 3, and -1
send "<aqua>closest to zero: %lowest of {_signed::*} by {_abs}%" to {_p} # -1
send "<aqua>farthest from zero: %highest of {_signed::*} by {_abs}%" to {_p} # -7
# players: top by level, hands back the player, not the number
set {_score} to lambda (pl: player) -> number:
return {_pl}'s level
send "<gold>top player by level: %highest of all players by {_score}%" to {_p}What this shows:
- A seeded
reduced with … fromthat's safe on an empty list and can change type. -
highest/lowest of … byreturning the winning element rather than its key. See List operations.
A lambda parameter can carry a default. When the caller leaves that trailing argument off, the default fills in.
command /defaults:
trigger:
set {_p} to sender
set {_advance} to lambda (value: number, step: number = 1) -> number:
return {_value} + {_step}
send "<aqua>advance(10) = %call lambda {_advance} with 10%" to {_p} # step defaults to 1 -> 11
send "<aqua>advance(10, 5) = %call lambda {_advance} with 10 and 5%" to {_p} # 15What this shows:
- A parameter written
name: type = value, filled when the caller skips it. - Only trailing arguments can be skipped. See Lambdas.
scanned with keeps every intermediate result of a reduce, not just the final one. Great for a running balance.
command /runningtotal:
trigger:
set {_p} to sender
set {_add} to lambda (a: number, b: number) -> number:
return {_a} + {_b}
set {_deposits::*} to 100, 50, 25, and 200
set {_balance::*} to {_deposits::*} scanned with {_add}
send "<green>balance after each deposit: %{_balance::*}%" to {_p} # 100, 150, 175, 375
# a seed becomes the opening balance and is emitted first
set {_balance::*} to {_deposits::*} scanned with {_add} from 1000
send "<green>from 1000: %{_balance::*}%" to {_p} # 1000, 1100, 1150, 1175, 1375What this shows:
-
scanned withfor cumulative totals. - A seed that opens the list and comes out first. See List operations.
zipped with … using walks two lists side by side and combines each pair. It stops at the shorter list.
command /pricetotal:
trigger:
set {_p} to sender
set {_mul} to lambda (qty: number, price: number) -> number:
return {_qty} * {_price}
set {_quantities::*} to 2, 1, and 5
set {_prices::*} to 10, 50, and 3
set {_lines::*} to {_quantities::*} zipped with {_prices::*} using {_mul}
send "<aqua>line totals: %{_lines::*}%" to {_p} # 20, 50, 15What this shows:
-
zipped with … usingto combine two lists element by element. See List operations.
pipe %value% through %lambdas% feeds a value through a chain of one-argument lambdas, left to right.
command /pipeline:
trigger:
set {_p} to sender
set {_inc} to lambda (n: number) -> number:
return {_n} + 1
set {_double} to lambda (n: number) -> number:
return {_n} * 2
set {_square} to lambda (n: number) -> number:
return {_n} * {_n}
set {_out} to pipe 5 through {_inc}, {_double}, and {_square}
send "<green>5 -> inc -> double -> square = %{_out}%" to {_p} # ((5+1)*2)^2 = 144What this shows:
- A chain of lambdas applied in order, each result feeding the next. See Lambdas.
page N of … by S gives non-overlapping chunks; window N of … by S slides one element at a time. page count tells you how many pages there are.
command /pagelist:
trigger:
set {_p} to sender
set {_items::*} to "a", "b", "c", "d", "e", "f", and "g"
set {_per} to 3
send "<yellow>%size of {_items::*}% items, %page count of {_items::*} by {_per}% pages of %{_per}%:" to {_p}
loop (page count of {_items::*} by {_per}) times:
send "<aqua>page %loop-value%: %page loop-value of {_items::*} by {_per}%" to {_p}
send "<gray>2nd sliding window of 3: %window 2 of {_items::*} by 3%" to {_p} # b, c, dWhat this shows:
-
page N of … by Sfor a paged GUI, withpage countto drive the loop. -
window N of … by Sfor overlapping slices. See List operations.
at least N of, at most N of, and exactly N of … passes check how many predicates in a list pass.
command /access:
trigger:
set {_p} to sender
clear {_reqs::*}
add lambda (pl: player): {_pl} is op to {_reqs::*}
add lambda (pl: player): {_pl} has permission "group.vip" to {_reqs::*}
add lambda (pl: player): {_pl}'s level >= 10 to {_reqs::*}
if at least 2 of {_reqs::*} passes for {_p}:
send "<green>access granted — met 2+ of the 3 checks" to {_p}
else:
send "<red>need at least 2 of: op, vip, level 10+" to {_p}
if exactly 3 of {_reqs::*} passes for {_p}:
send "<gold>full clearance — all three" to {_p}
if at most 0 of {_reqs::*} passes for {_p}:
send "<gray>you meet none of them" to {_p}What this shows:
-
at least/at most/exactly N of … passesnext to the existing all/any/none forms. See Predicates.
cooldown of %listener% reads the cooldown: you set, and it's settable from outside with set / add / remove.
command /minecooldown:
trigger:
set {_p} to sender
set {dig::%{_p}%} to listener for block break where player is {_p}:
cooldown: 2 seconds
on trigger:
send "<aqua>counted! (cooldown %cooldown of {dig::%{_p}%}%)" to {_p}
register {dig::%{_p}%}
send "<yellow>break blocks — at most one counts per %cooldown of {dig::%{_p}%}%." to {_p}
command /slowdig:
trigger:
set {_p} to sender
if {dig::%{_p}%} is registered:
add 1 second to {dig::%{_p}%}'s cooldown
send "<gray>cooldown is now %cooldown of {dig::%{_p}%}%" to {_p}
else:
send "<red>run /minecooldown first" to {_p}What this shows:
-
cooldown of %listener%read inside and outside the listener. - Widening the cooldown live with
add … to …'s cooldown. See Listeners.
owner of %listener% reads back the owner, and pause/resume all listeners owned by %object% freezes or revives every listener scoped to that owner.
command /guardon:
trigger:
set {_p} to sender
listen for block break:
owner: {_p}
where:
player is {_p}
on trigger:
send "<aqua>you broke %event-block%" to {_p}
listen for chat:
owner: {_p}
where:
player is {_p}
on trigger:
send "<aqua>you said: %message%" to {_p}
send "<green>two guards online, owned by %owner of the last created listener%." to {_p}
command /guardpause:
trigger:
pause all listeners owned by sender # freeze every listener scoped to you, one line
send "<yellow>your guards are paused" to sender
command /guardresume:
trigger:
resume all listeners owned by sender
send "<green>your guards are back" to senderWhat this shows:
-
owner of %listener%to read a listener's owner. -
pause/resume all listeners owned byas the pause-counterpart to unregistering by owner. See Listeners.
Script options: now expand inside a listen section, so one value can drive countdown:, cooldown:, and the rest.
options:
grace: 30 seconds
debounce: 1 second
command /optdemo:
trigger:
set {_p} to sender
listen for damage where victim is {_p}:
countdown: {@grace}
cooldown: {@debounce}
triggers: 5
on trigger:
cancel event
send "<blue>blocked (%remaining countdown% left)" to {_p}
on timeout:
send "<green>survived the {@grace} grace period" to {_p}
send "<yellow>shield up for {@grace}, debounced to {@debounce}." to {_p}What this shows:
-
{@option}values resolved insidecountdown:,cooldown:, and other entries. See Listeners.
Hash some text off the main thread, then read the result once it's ready, without freezing the server. The lambda body must be thread-safe: pure computation, no Bukkit API.
command /hashit <text>:
trigger:
set {_p} to sender
set {_hash} to lambda (payload: string) -> string:
return {_payload} hashed with SHA-256 # pure compute, safe off-thread
send "<gray>hashing in the background..." to {_p}
set {_f} to future of calling lambda {_hash} with arg-1
wait for {_f} for at most 5 seconds:
send "<aqua>sha256 = %result of {_f}%" to {_p}
on failure:
send "<red>hashing failed: %failure reason of {_f}%" to {_p}
on timeout:
send "<red>hash took too long" to {_p}What this shows:
-
future of calling lambda … with …runs a lambda on a background thread. See Async. -
wait for %future%:suspends (not blocks) the trigger until it resolves, withon timeout:for the cap. -
result of %future%reads the answer after the wait. -
on failure:catches a background lambda that errored, andfailure reason ofsays why. Both nest inside thewait forblock. Added in 1.4.0.
a new future is a promise you resolve yourself with complete. The second half fires two background jobs and joins them into one wait for all of.
command /promise:
trigger:
set {_p} to sender
# a promise you resolve yourself
set {_token} to a new future
if {_token} is pending:
send "<gray>token not ready yet" to {_p}
complete {_token} with "abc-123"
if future {_token} is done:
send "<aqua>token = %result of {_token}%" to {_p}
# two background jobs, joined into one wait
set {_work} to lambda (n: number) -> number:
return {_n} * {_n} # pure compute, safe off-thread
set {_a} to future of calling lambda {_work} with 6
set {_b} to future of calling lambda {_work} with 9
wait for all of {_a} and {_b} within 10 seconds:
send "<aqua>both done: %result of {_a}% and %result of {_b}%" to {_p}
on failure:
send "<red>a job failed" to {_p}
on timeout:
send "<red>a job stalled" to {_p}What this shows:
-
a new futurewithcomplete %future% with %object%and theis pending/is doneconditions. See Async. -
wait for all of %futures%joins several futures (when-all). Itson failure:fires on the first failure, without waiting for the rest.
Pause a trigger until the player confirms in chat. The event's values (message) are in scope in the body; locals set in either branch carry forward.
command /confirm:
trigger:
set {_p} to sender
send "<yellow>type 'yes' in chat within 15 seconds to confirm." to {_p}
wait for next chat where player is {_p} for at most 15 seconds:
if message is "yes":
set {_ok} to true
on timeout:
set {_ok} to false
if {_ok} is true:
send "<green>confirmed!" to {_p}
else:
send "<red>cancelled (no reply)." to {_p}What this shows:
-
wait for next <event> where <condition> [within …]:pauses for an event with its values in scope. See Async. -
on timeout:and locals carrying past the block.
watch … every re-reads an expression on a timer and runs on change: only when it differs, with old value and new value in scope. A watcher is a normal listener, so /sklambda listeners lists it.
command /watchfood:
trigger:
set {_p} to sender
send "<gray>watching your food level for 2 minutes..." to {_p}
watch (food level of {_p}) every 1 second within 2 minutes:
on change:
send action bar "<gold>food: %old value% -> %new value%" to {_p}
on timeout:
send "<gray>stopped watching food" to {_p}
on end:
send action bar "" to {_p}What this shows:
-
watch %value% every %timespan% within %timespan%:with built-in change detection. See Async. -
old value/new value, pluson timeout:andon end:teardown.
watch when … every polls a condition and fires only on the transitions: on rising: (false → true) and on falling: (true → false). It's "do X the moment Y first becomes true" without a poll loop.
command /lowhealth:
trigger:
set {_p} to sender
send "<gray>alerting you whenever you drop below 6 hearts of health." to {_p}
watch when (health of {_p} < 6) every 1 second within 10 minutes:
on rising:
send "<red>low health!" to {_p}
on falling:
send "<green>back to safe health" to {_p}What this shows:
-
watch when <condition> every %timespan%:is edge-triggered. See Async. -
on rising:andon falling:fire on the transition, not every poll. -
on rising:fires only on a transition, so a condition already true when the watcher starts stays quiet until it goes false and back. Addinitial: false(below) if you want it to fire straight away.
A future doesn't only succeed. fail settles it as broken and wakes every waiting trigger into on failure:. This waits for a player to confirm, and fails the promise if they leave first.
command /awaitconfirm:
trigger:
set {_p} to sender
set {_f} to a new future
listen for quit where event-player is {_p}:
owner: {_p}
triggers: 1
on trigger:
fail {_f} with "player disconnected"
send "<yellow>type anything in chat within 30 seconds to confirm." to {_p}
listen for chat where player is {_p}:
owner: {_p}
triggers: 1
on trigger:
complete {_f} with message
wait for {_f} for at most 30 seconds:
send "<green>confirmed: %result of {_f}%" to {_p}
on failure:
send "<red>could not confirm: %failure reason of {_f}%" to console
on timeout:
send "<yellow>you took too long." to {_p}What this shows:
-
fail %future% with %text%breaks a promise;rejectis the same. See Async. -
failure reason of %future%explains why, and isnothingfor a pending or successful future. - Whichever settles first wins: a later
failorcompleteon a settled future is ignored. - The failure goes to console, not to
{_p}, because the player leaving is the failure.
Added in 1.4.0.
then builds a new future from an existing one, running a lambda on the result once it arrives. The source is untouched, and a failure skips straight past the chain.
command /shorthash <text>:
trigger:
set {_p} to sender
set {_hash} to lambda (payload: string) -> string:
return {_payload} hashed with SHA-256
set {_shorten} to lambda (h: string) -> string:
return first 8 characters of {_h}
set {_full} to future of calling lambda {_hash} with arg-1
set {_short} to {_full} then {_shorten}
wait for {_short} for at most 10 seconds:
send "<aqua>short = %result of {_short}%" to {_p}
send "<gray>full is still there = %result of {_full}%" to {_p}
on failure:
send "<red>%failure reason of {_short}%" to {_p}What this shows:
-
%future% then %lambda%returns a new future; the source stays readable on its own. - If the source fails, the chained future fails with the same reason and the lambda never runs.
- The chained lambda runs on the same background pool, so it must be Bukkit-free too.
- It is
then, notmapped with: that spelling belongs to list mapping.
Added in 1.4.0.
When an answer is sometimes cached and sometimes needs work, hand back a future either way. The caller writes one code path.
command /lookup <text>:
trigger:
set {_p} to sender
set {_slow} to lambda (key: string) -> string:
return "%{_key}% computed the slow way"
if {cache::%arg-1%} is set:
set {_f} to a completed future with {cache::%arg-1%}
else:
set {_f} to future of calling lambda {_slow} with arg-1
wait for {_f} for at most 5 seconds:
send "<aqua>%result of {_f}%" to {_p}
set {cache::%arg-1%} to result of {_f}
on failure:
set {_f2} to a failed future with "lookup failed"
send "<red>%failure reason of {_f2}%" to {_p}What this shows:
-
a completed future with %object%is already resolved, sowait forreturns on the spot. -
a failed future with %text%is already broken, useful for an error path that still returns a future. - The caller
wait fors the same way whether or not any background work happened.
Added in 1.4.0.
taken while and dropped while split a list at the first element that fails a predicate. Position matters, which is what separates them from a filter.
command /streak:
trigger:
set {_p} to sender
set {_scores::*} to 5, 3, 1, -2, and 4
set {_positive} to lambda (n: number): {_n} > 0
send "<aqua>opening streak: %{_scores::*} taken while {_positive} passes%" to {_p}
send "<gray>from the first drop on: %{_scores::*} dropped while {_positive} passes%" to {_p}
# a filter keeps every match instead, wherever it sits
send "<gray>all positives: %{_scores::*} where [{_positive} passes for input]%" to {_p}What this shows:
-
taken whilestops at the first failure; the trailing4stays indropped whileeven though it passes. - The two always partition the list.
- Skript's
whereis the position-independent alternative. See List operations.
Added in 1.4.0.
A watch when watcher normally treats the first poll as its baseline, so something already true stays quiet. initial: declares the state to assume instead.
command /hurtalert:
trigger:
set {_p} to sender
watch when (health of {_p} < 6) every 1 second within 10 minutes:
initial: false
on rising:
send "<red>low health!" to {_p}
on falling:
send "<green>back to safe health" to {_p}What this shows:
-
initial: falseassumes the condition started false, so a player who is already hurt is alerted on the first poll. -
initial: trueis the mirror image foron falling:. - Leave the entry off for the old baseline behaviour. See Async.
Added in 1.4.0.
Listeners and futures both leak quietly. These expressions let you audit listeners from a script; /sklambda futures does the same for futures that never resolved.
command /audit:
trigger:
set {_p} to sender
send "<gray>%amount of all active listeners% listeners active" to {_p}
loop all active listeners:
if age of loop-value > 10 minutes:
send "<yellow>stale: %script of loop-value%, alive %age of loop-value%" to {_p}
unregister loop-valueWhat this shows:
-
script of,age of, andcreation date ofa listener. See Listeners. -
age ofcounts from creation, not registration. - Run
/sklambda futuresfor the future side: origin, how long it has been pending, and how manywait forblocks are suspended on it.
Added in 1.4.0.
Guides
Reference





