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.1 -> C++ runtime-v0.1.384, the separate landing wrapper lane, and editor/GPU tiers.

GPU Canvas on Web

Web 2.42.1 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.1 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​