ListenerAction Script Protocol
Overview
ListenerAction scripts run custom code when a State Machine listener event fires. Unlike Listeners (which are function signatures for addListener()), ListenerAction is a script type that you attach to state machine listener events.
This page is aligned to the June 4, 2026 compatibility baseline: public npm runtime line 2.37.8, with source pins tracked in Runtime Compatibility Baseline.
When to use ListenerAction scripts:
- Custom side-effects when a state machine listener triggers
- Logging, analytics, or audio playback on listener events
- Complex logic that needs listener context from the trigger event
Type Definition
export type ListenerAction<T> = {
init: ((self: T, context: Context) -> boolean)?,
performAction: (self: T, listenerContext: ListenerContext) -> (),
perform: ((self: T, pointerEvent: PointerEvent) -> ())?, -- Legacy/deprecated
} & T
Protocol Methods
| Function | Signature | Required | Description |
|---|---|---|---|
init | (self: T, context: Context): boolean | No | One-time initialization |
performAction | (self: T, listenerContext: ListenerContext): () | Yes (preferred) | Runs when the listener fires |
perform | (self: T, pointerEvent: PointerEvent): () | Legacy | Deprecated callback from older scaffolds |
performAction(self, listenerContext) — REQUIRED (Preferred)
Called when the state machine listener event fires. This is the current callback shape recommended for new scripts.
function performAction(self: MyAction, listenerContext: ListenerContext)
if listenerContext:isPointerEvent() then
local evt = listenerContext:asPointerEvent()
if evt then
print("pointer:", evt.type)
end
elseif listenerContext:isKeyboardEvent() then
local evt = listenerContext:asKeyboardEvent()
if evt then
print("keyboard phase:", evt.phase)
end
end
end
ListenerContext Event Kinds
ListenerContext is a tagged payload union. Use is... guards before as... casts.
| Guard | Accessor | Typical fields |
|---|---|---|
isPointerEvent() | asPointerEvent() | id, position, type, previousPosition, timeStamp |
isKeyboardEvent() | asKeyboardEvent() | key, shift, control, alt, meta, phase |
isTextInput() | asTextInput() | text |
isFocus() | asFocus() | isFocus |
isReportedEvent() | asReportedEvent() | delaySeconds (payload is limited at this baseline) |
isViewModelChange() | asViewModelChange() | kind/presence only (payload is limited at this baseline) |
isGamepad() | asGamepad() | deviceId, buttonMask, axis0 |
isNone() | asNone() | fallback/unknown kind |
perform(self, pointerEvent) — Legacy/Deprecated
Older examples may still use perform(self, pointerEvent). Keep it only for legacy compatibility work; prefer performAction for all new course code.
init(self, context) — Optional
Called once when the action is created. Use for setup and accessing the ViewModel.
function init(self: MyAction, context: Context): boolean
local vm = context:viewModel()
-- Setup logic
return true
end
Template
export type MyAction = {
clickCount: number,
}
function performAction(self: MyAction, listenerContext: ListenerContext)
self.clickCount += 1
print("Listener fire #" .. tostring(self.clickCount))
end
return function(): ListenerAction<MyAction>
return {
clickCount = 0,
performAction = performAction,
}
end
How to Apply
- Create a ListenerAction script in the Assets panel
- In the State Machine editor, select a Listener
- Add the script as an action on the listener
- The
performActionfunction will run each time the listener triggers
Key Differences from Node Scripts
| Feature | Node Script | ListenerAction |
|---|---|---|
| Lifecycle | init, advance, update, draw | init, performAction |
| Rendering | Has draw() and Renderer access | No rendering |
| When it runs | Every frame (advance/draw) | Only when listener fires |
| Event context | Via pointerDown/pointerMove handlers | Via performAction(listenerContext) |
Practice Exercise
Exercise 1: Click Counter Action ⭐
Premise
ListenerAction scripts are event-driven. They run only when the listener fires, making them ideal for analytics, counters, and side-effects.
By the end of this exercise, you will branch on listenerContext event kind, count pointer events, and print an ANSWER: line after 3 pointer triggers.
Starter Code
export type ClickCounterAction = {
pointerCount: number,
lastKind: string,
}
function performAction(self: ClickCounterAction, listenerContext: ListenerContext)
-- TODO 1: Detect event kind with listenerContext:is...()
-- Set self.lastKind to one of: "pointer", "keyboard", "text", "focus", "other"
-- TODO 2: If kind is pointer, increment self.pointerCount
-- TODO 3: Print a debug line with kind and count
-- print("listener kind", self.lastKind, "count", self.pointerCount)
-- TODO 4: When pointerCount reaches 3, print:
-- "ANSWER: kind=pointer,count=3"
end
return function(): ListenerAction<ClickCounterAction>
return {
performAction = performAction,
pointerCount = 0,
lastKind = "other",
}
end
Assignment
- Attach the script as a listener action in a state machine Listener
- Trigger pointer-driven listener events 3 times
- Copy the
ANSWER:line from Console
Verify Your Answer
Knowledge Check
See Also: Listeners, Script Types Overview, Events API
Next Steps
- Continue to TransitionCondition Protocol
- Verify surface status in Runtime Compatibility Baseline
- Need a refresher? Review Quick Reference