Skip to main content

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.

Runtime baseline

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

FunctionSignatureRequiredDescription
init(self: T, context: Context): booleanNoOne-time initialization
performAction(self: T, listenerContext: ListenerContext): ()Yes (preferred)Runs when the listener fires
perform(self: T, pointerEvent: PointerEvent): ()LegacyDeprecated 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.

GuardAccessorTypical 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

  1. Create a ListenerAction script in the Assets panel
  2. In the State Machine editor, select a Listener
  3. Add the script as an action on the listener
  4. The performAction function will run each time the listener triggers

Key Differences from Node Scripts

FeatureNode ScriptListenerAction
Lifecycleinit, advance, update, drawinit, performAction
RenderingHas draw() and Renderer accessNo rendering
When it runsEvery frame (advance/draw)Only when listener fires
Event contextVia pointerDown/pointerMove handlersVia 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.

Goal

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

  1. Attach the script as a listener action in a state machine Listener
  2. Trigger pointer-driven listener events 3 times
  3. Copy the ANSWER: line from Console

Verify Your Answer

Verify Your Answer

Knowledge Check

Q:Which callback is required in ListenerAction scripts?
Q:What is the safest ListenerAction payload pattern?

See Also: Listeners, Script Types Overview, Events API

Next Steps