Skip to content

Listeners

Ahmad Saleem edited this page Aug 25, 2026 · 10 revisions

Listeners

A listener is a temporary event listener. It only listens for a while, then turns itself off.

Normal Skript event blocks (on damage:, on chat:, etc.) listen forever and run for every player. A skLambda listener is the opposite: it belongs to one situation (one command run, one player, one fight), and it ends on its own.

A watcher (watch … every, added in 1.3.0) is also a listener and shares everything on this page: owners, pause/resume, unregister, state checks, and /sklambda listeners. It polls a value or condition instead of reacting to an event. See Async.

The simplest listener

listen for chat:
    on trigger:
        send "%message%" to console

This starts listening for chat right now, and runs the body each time someone chats.

But this listens forever and for everyone. That's usually not what you want. The next sections show how to limit it.

Filtering events with where

Use where to make the listener only react to events that match a rule.

You can write one rule inline:

listen for damage where victim is {_p}:
    on trigger:
        send "ouch" to victim

Or you can use a where: section with multiple rules. The event must match all of them.

listen for block break:
    where:
        event-player is {_p}
        event-block is diamond ore
        event-player is sneaking
    on trigger:
        send "sneaky diamond!" to event-player

If an event doesn't match the rules, on trigger: is not run.

Stopping after some time: countdown

countdown: ends the listener after a time limit.

listen for chat where player is {_p}:
    countdown: 15 seconds
    on trigger:
        broadcast "[%{_p}%] %message%"
    on timeout:
        send "shout mode off" to {_p}

When the time runs out:

  • on timeout: runs (if you wrote one).
  • The listener turns itself off.

Stopping after N events: triggers

triggers: ends the listener after it has fired a set number of times.

listen for chat where player is {_p}:
    triggers: 3
    on trigger:
        send "you said: %message%" to {_p}
    on completion:
        send "that's enough" to {_p}

When the count is reached:

  • on completion: runs (if you wrote one).
  • The listener turns itself off.

You can use countdown: and triggers: together. Whichever happens first wins.

Scoping to an owner: owner

owner: ties the listener to something, usually a player. When that owner leaves the server, the listener stops itself. No cleanup code needed.

listen for block break:
    owner: {_p}            # stops on its own when {_p} disconnects
    where:
        event-player is {_p}
    on trigger:
        send "you broke %event-block%" to {_p}

An owner also lets you clean up one owner's listeners by hand with unregister all listeners owned by {_p}. See Stopping many listeners at once.

More than just players

As of 1.1.0, an owner can also be an entity, a chunk, or a world, and as of 1.5.0 an inventory. The listener cleans itself up automatically when that owner goes away:

Owner Stops itself when…
offline player (online players too) the player disconnects
entity it leaves the loaded world (death, despawn, or its chunk unloading)
chunk the chunk unloads
world the world unloads
inventory the last viewer closes that menu

Cleanup fires when the owner goes away, so a player who is already offline when you register won't trigger it on their own. The listener just runs until it ends some other way.

spawn a zombie at location of {_p}
set {_zombie} to last spawned entity
listen for damage:
    owner: {_zombie}        # auto-unregisters when the zombie dies or unloads
    where:
        victim is {_zombie}
    on trigger:
        send "your zombie got hit" to {_p}

Each of these fires the listener's on end: with end reason = unregistered. Also fixed in 1.1.0: teleporting a player between worlds no longer wrongly stops their listeners.

Ignoring rapid repeats: cooldown

cooldown: sets the smallest gap allowed between fires. Events that arrive during the cooldown are ignored: they don't run on trigger: and don't count toward triggers:.

listen for damage where victim is {_p}:
    cooldown: 1 second     # at most one counted hit per second
    on trigger:
        send "hit!" to {_p}

Handy for events that can fire many times in a quick burst.

A repeating timer: every

every <time>: runs a block on a repeating timer for as long as the listener is alive. Good for a live display that refreshes while the listener runs.

listen for damage where victim is {_p}:
    countdown: 30 seconds
    every 1 second:
        send action bar "shield: %remaining countdown% left" to {_p}
    on trigger:
        cancel event

The timer pauses when the listener is paused, and stops when the listener ends.

The callbacks

Inside a listener you can write these sub-sections.

Block When it runs
on register: Once, the moment the listener starts. Its partner is on end:.
on trigger: Every time the event happens and passes where.
on completion: When triggers: is reached.
on timeout: When countdown: runs out. Needs countdown:.
on pause: When the listener is paused. See Pausing and resuming.
on resume: When the listener is resumed.
on end: Always, however the listener stops. Runs after the others.

on end: is the one callback that runs no matter what: completion, timeout, cancel, unregister, or an owner leaving. It's the place for cleanup you want to happen exactly once, however things turn out.

on register: is the opposite bookend (new in 1.1.0): it runs once, the instant the listener becomes active. For a listener you save and start later, it fires on register, not when you define it. Pairing on register: (setup) with on end: (teardown) keeps both halves of a listener's lifecycle in one place.

set {watch::%{_p}%} to listener for block break where player is {_p}:
    triggers: 3
    on register:
        send "watcher armed, break 3 blocks" to {_p}
    on trigger:
        send "%remaining triggers% to go" to {_p}
    on end:
        send "watcher gone (%end reason%)" to {_p}
register {watch::%{_p}%}      # on register: fires here, not at the set above

All of them are optional, but a listener with no callbacks does nothing.

Why a listener ended: end reason

Inside on end:, end reason tells you how the listener stopped. It is one of:

Reason Means
completion triggers: was reached.
timeout countdown: ran out.
cancelled cancel listener / unregister listener ran inside on trigger:.
unregistered stopped from outside (unregister, owner left, or a bulk cleanup).
listen for block break where event-player is {_p}:
    countdown: 20 seconds
    triggers: 5
    on end:
        if end reason is completion:
            send "you broke all 5!" to {_p}
        else if end reason is timeout:
            send "out of time" to {_p}
        else:
            send "stopped early (%end reason%)" to {_p}

You can also read it from a saved listener anywhere with end reason of {listener} or {listener}'s end reason. It is not set until the listener has ended.

Inside on trigger:

You have a few extra tools that only work inside on trigger:.

cancel listener

Stop the listener right now. This does not fire on completion: or on timeout:, but on end: still runs (with end reason = cancelled). You can also write unregister listener, which means the same thing.

Useful for "first to reach the goal wins":

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}

skip trigger

Ignore this one event. Don't run the rest of on trigger:. Don't count it against triggers:. Keep listening.

Useful when where: isn't enough and you need extra checks inside the body:

listen for damage where victim is {_p}:
    triggers: 3
    on trigger:
        if damage cause is not fall:
            skip trigger      # only fall damage counts
        send "fall %3 - remaining triggers% / 3" to {_p}

remaining triggers and remaining countdown

How many fires are left, and how much time is left.

on trigger:
    send "left: %remaining triggers% hits, %remaining countdown%" to {_p}

These also work inside on completion: and on timeout:.

Saving a listener for later

So far we've used listen for ..., which starts immediately. You can also save a listener in a variable and start it whenever you want.

set {chat_log} to listener for chat:
    on trigger:
        send "[chat] %sender%: %message%" to console
        cancel event

register {chat_log}
  • set {var} to listener for ...: defines it. It does not start yet.
  • register {var} starts listening.
  • unregister {var} stops listening.

If your script uses Skript's experimental using type hints, the saved variable is remembered as holding a listener, so Skript can warn you when you use it the wrong way. See Type hints.

Stopping many listeners at once

Sometimes you want to clean up without tracking each listener in its own variable.

unregister the last created listener        # stop the most recent one
unregister all listeners owned by {_p}      # stop only the listeners owned by {_p}
unregister all listeners                    # stop EVERY active listener

unregister all listeners owned by ... only touches listeners with a matching owner: (see Scoping to an owner), so it's a safe way to clean up just one player's listeners.

Be careful with unregister all listeners: it stops every listener on the whole server, from every script, not just yours. None of these fire on completion or on timeout, but each stopped listener's on end: still runs (with end reason = unregistered).

To see what's currently running, use the /sklambda listeners command.

Listing the running listeners

You can also pull the live listeners as a real list, to loop over, count, or feed to unregister.

all active listeners            # every listener running right now, server-wide
listeners owned by {_p}         # only the ones whose owner: is {_p}

all active listeners is the same set /sklambda listeners shows. listeners owned by … matches only listeners that declared a matching owner:. A where player is {_p} filter does not make a listener "owned by" that player.

send "%size of all active listeners% listeners running" to player

loop listeners owned by {_p}:        # same effect as unregister all listeners owned by {_p}
    unregister loop-value

Added in 1.1.0.

Where a listener came from

Three expressions tell you a listener's origin, so you can do the same leak hunting /sklambda listeners does, from a script:

Expression Gives back
script of %listener% the script it was declared in, spelled the way Skript's own script spells it
creation date of %listener% when it was created
age of %listener% how long ago that was
loop all active listeners:
    if age of loop-value > 10 minutes:
        send "<yellow>stale listener from %script of loop-value%" to console
        unregister loop-value

age of counts from creation, not registration. A listener you declared with set {_x} to a listener on ... and registered later already has an age before it starts listening. (/sklambda listeners prints time since registration instead, so the two can differ for a listener that sat unregistered.)

Added in 1.4.0.

Pausing and resuming

You can pause a listener. While paused:

  • Events are ignored.
  • The countdown is frozen (it does not count down).
  • Any every timer stops ticking.
pause {shield}
resume {shield}

Other addons also define pause/resume. If one clashes, prefix skLambda to force skLambda's version: skLambda pause {shield}.

You can also pause or resume every listener tied to an owner in one line:

pause all listeners owned by {_p}     # freeze only {_p}'s listeners
resume all listeners owned by {_p}    # unfreeze them again

This is the pause/resume counterpart to unregister all listeners owned by (see Stopping many listeners at once): it only pauses them, rather than stopping them for good. Added in 1.2.0.

on pause: and on resume:

A listener can react to being paused or resumed with two callbacks. on pause: runs the moment it pauses; on resume: runs when it picks back up. Both are optional.

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}%}

Because the countdown and any every timer are held while paused, a pause for a break doesn't burn into the time limit, and the live readout naturally stops updating until you resume.

Changing a listener from the outside

You can change a saved listener while it's running.

add 1 to {shield}'s triggers           # one more hit allowed
add 10 seconds to {shield}'s countdown # 10 more seconds
add 1 second to {shield}'s cooldown    # widen the debounce gap
set countdown of {shield} to 30 seconds
set triggers of {shield} to 5
set cooldown of {shield} to 0          # turn the cooldown off

Reading a listener's owner and cooldown

You can read back the owner: and cooldown: you gave a listener.

set {_gap} to cooldown of {shield}      # the cooldown:, or 0 if it has none
set {_who} to owner of {shield}         # the owner:, or nothing if it has none

Both also read in the possessive form: {shield}'s cooldown and {shield}'s owner. cooldown of is settable too, with set / add / remove (shown above); setting it to zero switches the cooldown off. Added in 1.2.0.

Checking a listener's state

if {shield} is registered:
    ...

if {shield} is paused:
    resume {shield}

if {shield} is running:    # registered and not paused
    ...

Script options in entries

Script options: ({@name}) now expand inside a listen section, so you can keep shared values in one place and reuse them across listeners.

options:
    grace: 30 seconds
    debounce: 1 second

# ...later, in a command or event:
listen for damage where victim is {_p}:
    countdown: {@grace}
    cooldown: {@debounce}
    on trigger:
        cancel event

This works in the entry values such as countdown:, cooldown:, every, and the inline where. Before 1.2.0 these were left as raw text and failed to parse. Fixed in 1.2.0.

What goes where: quick recap

listen for <event> [where <one condition>]:
    where:
        <more conditions>
    countdown: <timestamp>
    triggers: <number>
    owner: <offline player, entity, chunk, world, inventory>
    cooldown: <timestamp>
    every <timestamp>:
        ...
    on register:
        ...
    on trigger:
        ...
    on completion:
        ...
    on timeout:
        ...
    on pause:
        ...
    on resume:
        ...
    on end:
        ...

Everything inside the listener is optional, but you usually want at least one callback.

For full working examples, see Examples.

Clone this wiki locally