Skip to main content

Rive Scripting Quick Reference

Need the full script matrix?

See Script Capability Matrix for a comprehensive table of script inputs, callback parameters, and hard limitations by script type.

Need version-locked API status?

See Runtime Compatibility Baseline for direct Web 2.42.0 -> C++ runtime-v0.1.344, the separate landing wrapper lane, and editor/GPU tiers.

GPU Canvas on Web

Web 2.42.0 requires the experimental host option enableGPUCanvas: true when importing GPU Canvas content. It defaults to false, is fixed at RiveFile import, and is not a Luau global. LERP does not claim that RAV enables it; validate the exported file in each host.

Writing scripts outside Rive?

Use the Rive Luau VS Code extension or the Rive Luau LSP when editing .luau files in VS Code, Cursor, Windsurf, or another LSP-capable editor. It provides Rive-parity diagnostics, autocomplete, hover docs, and IntelliSense before final validation in the Rive editor.

For command-line analysis, place every analyzer option before the first path:

./bin/rive/rive-luau-analyze --formatter=plain path/to/script.luau

For an LSP client, configure ./bin/rive/rive-luau-lsp itself as the stdio command. Do not append another lsp subcommand to the wrapper.

Node Script Skeleton

export type MyNode = {
speed: Input<number>,
enabled: Input<boolean>,
path: Path,
paint: Paint,
}

function init(self: MyNode, context: Context): boolean
self.path = Path.new()
self.paint = Paint.new()
return true
end

function update(self: MyNode)
end

function advance(self: MyNode, seconds: number): boolean
return true
end

function draw(self: MyNode, renderer: Renderer)
end

return function(): Node<MyNode>
return {
init = init,
update = update,
advance = advance,
draw = draw,
speed = 1,
enabled = true,
path = late(),
paint = late(),
}
end

Canvas/GPU work inside draw

function draw(self: MyNode, renderer: Renderer)
-- Pair Canvas:beginFrame/endFrame or GPUCanvas:beginRenderPass/finish.
-- Composite canvas.image or gpuCanvas.image with renderer here.
end

Lifecycle Order

  1. init(self, context) - once on start
  2. update(self) - when inputs change (NO context param!)
  3. advance(self, seconds) - every animation frame
  4. draw(self, renderer) - records optional offscreen Canvas/GPUCanvas work and composites on repaint

Runtime Guardrails

  • Use pure Luau + Rive APIs; do not use Roblox runtime globals (game, workspace, Instance).
  • Script names are flat (no folders/slashes, no .lua/.luau extension).
  • Keep long-running integer-style values 32-bit safe (-2147483648..2147483647).

Inputs & Data Binding

Define Inputs

export type MyNode = {
size: Input<number>,
tint: Input<Color>,
model: Input<Data.Character>,
enemy: Input<Artboard<Data.Enemy>>,
}

return function(): Node<MyNode>
return {
init = init,
draw = draw,
size = 80,
tint = Color.rgb(255, 120, 80),
model = late(),
enemy = late(),
}
end

Read Input Values

local value = self.size
self.tint -- Color

ViewModel and Artboard Inputs

-- ViewModel inputs expose Property<T> fields that use .value
self.model.health.value
self.enemy:instance()

ViewModel Access (Main Artboard)

function init(self: MyNode, context: Context): boolean
local vm = context:viewModel()
local score = vm and vm:getNumber("score")
if score then
print(score.value)
end
return true
end

Global ViewModels

function init(self: MyNode, context: Context): boolean
for _, name in context:globalViewModelNames() do
local global = context:globalViewModel(name)
if global then
print("Global ViewModel:", name)
end
end
return true
end

Unknown or non-global names return nil. Context.log is a current editor function property called as context.log("message"), but it is absent from the exact target's C++ Luau Context; use print(...) for Web 2.42.0 portability.

PropertyList Patterns

local items = vm and vm:getList("items")
if items then
items:clear()
local nextItem = Data.Item.new()
items:push(nextItem)

for i = 1, items.length do
local item = items[i]
end

if items.length >= 1 then
items:removeAt(1) -- 1-based
end
end

ListenerContext Safe Payload Branching

function performAction(self: MyAction, listenerContext: ListenerContext)
if listenerContext:isGamepadEvent() then
local g = listenerContext:asGamepadEvent()
if g then
print(g.changeKind, g.changeIndex, g.changeValue)
end
elseif listenerContext:isGamepadConnected() then
local g = listenerContext:asGamepadConnected()
if g then print(g.deviceId, g.mapping, g.leftStick) end
elseif listenerContext:isGamepadDisconnected() then
local g = listenerContext:asGamepadDisconnected()
if g then print(g.deviceId) end
end
end

Current Editor event names are KeyboardEvent, TextInput, FocusEvent, ReportedEvent, ViewModelChange, NoneEvent, GamepadConnected, GamepadEvent, and GamepadDisconnected. Older *Invocation names belong to pinned runtime-source compatibility, not new Editor-authored examples.

FileFormat editor extension

local format: TextFileFormat = {
name = "Notes",
extensions = { "note" },
}

return function(): TextFileFormat
return format
end

This is an editor protocol, not an artboard runtime script. Claimed text files export as Blob assets; runtime scripts use context:blob(name). See File Formats for analysis, view, and EditorContext callbacks.

Promise / async / await

local run = async(function()
local ok, decoded = await(context:decodeImage(self.imageBlob))
if ok then
print(decoded.width, decoded.height)
else
print("decode failed:", decoded)
end
end)

run:catch(function(err) print(err) end)

Drawing Essentials

Path

local path = Path.new()
path:moveTo(Vector.xy(0, 0))
path:lineTo(Vector.xy(100, 0))
path:lineTo(Vector.xy(100, 100))
path:close()

If a path uses a fill, ensure the path is closed and wound clockwise (y points down) — counter-clockwise contours render invisible in the current renderer.

Paint

local fill = Paint.with({ style = "fill", color = Color.rgb(80, 170, 255) })
local stroke = Paint.with({ style = "stroke", thickness = 4, color = Color.rgb(30, 60, 120) })

Renderer

renderer:drawPath(path, fill)
renderer:save()
renderer:transform(Mat2D.withTranslation(50, 0))
renderer:drawPath(path, stroke)
renderer:restore()

Mat4 Quick Use

local model = Mat4.fromTranslation(0, 0, -4) * Mat4.fromRotationY(math.rad(20))
local x, y, z, w = model:transformVec4(1, 0, 0, 1)

GPU Shader Quick Use

function init(self: ShaderNode, context: Context): boolean
self.gpuCanvas = context:gpuCanvas({ width = 512, height = 512 })
self.imageSampler = ImageSampler("clamp", "clamp", "bilinear")

local shader = context:shader("my_effect")
if not shader then
print("Missing shader asset")
return true
end

self.pipeline = GPUPipeline.new({
vertex = { module = shader, entryPoint = "vsMain" },
fragment = { module = shader, entryPoint = "fsMain" },
vertexLayout = self.vertexLayout,
colorTargets = {{ format = self.gpuCanvas.format }},
})

return true
end

function draw(self: ShaderNode, renderer: Renderer)
if not self.pipeline or self.gpuCanvas.width == 0 then return end
local pass = self.gpuCanvas:beginRenderPass({
color = {{ loadOp = "clear", storeOp = "store", clearColor = { 0, 0, 0, 0 } }},
})
pass:setPipeline(self.pipeline)
pass:setVertexBuffer(0, self.vertexBuffer)
pass:draw(self.vertexCount)
pass:finish()
if self.gpuCanvas.width > 0 then
renderer:drawImage(self.gpuCanvas.image, self.imageSampler, "srcOver", 1)
end
end

Luau Essentials Used in Rive

Tables (Arrays + Dictionaries)

local list = {1, 2, 3}
local dict = {x = 10, y = 20}
for i, v in ipairs(list) do end
for k, v in pairs(dict) do end

Functions and Methods

local function clamp(value: number, min: number, max: number): number
return math.max(min, math.min(max, value))
end

function MyType:methodName()
end

String Interpolation

print(`Score: {score}`)

Next Steps