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 September 4, 2026 baseline: @rive-app/canvas@2.42.0 backed by C++ runtime-v0.1.344, with source pins tracked in Runtime Compatibility Baseline.

Current Editor type surface

The current Rive Editor analyzer accepts KeyboardEvent, TextInput, FocusEvent, ReportedEvent, ViewModelChange, NoneEvent, and the three gamepad payload types below. Older *Invocation names remain relevant when reading pinned runtime source, but are not the current Editor reference names.

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(): PointerEvent?id, position, type: PointerType
isKeyboardEvent()asKeyboardEvent(): KeyboardEvent?key, shift, control, alt, meta, phase: KeyPhase
isTextInput()asTextInput(): TextInput?text
isFocus()asFocus(): FocusEvent?isFocus
isReportedEvent()asReportedEvent(): ReportedEvent?Presence/kind only in the current Editor reference
isViewModelChange()asViewModelChange(): ViewModelChange?Presence/kind only in the current Editor reference
isGamepadConnected()asGamepadConnected(): GamepadConnected?Full connected-device state
isGamepadEvent()asGamepadEvent(): GamepadEvent?Change metadata plus full device state
isGamepadDisconnected()asGamepadDisconnected(): GamepadDisconnected?deviceId
isNone()asNone(): NoneEvent?Fallback/unknown kind

For gamepad state, the current reference includes buttons, axes, mapping metadata, named standard buttons, stick vectors, triggers, and the buttonPressed, buttonValue, and axis query methods. See the Events API for the complete field map.

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