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 the current C++ WASM runtime release line (2.37.8), npm source pin (bc560112), source-level API snapshot (runtime-v0.1.106), and shader/GPU Early Access tier.

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.

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

Optional GPU/Canvas Phase

function drawCanvas(self: MyNode)
-- Canvas:beginFrame(...) or GPUCanvas:beginRenderPass(...) work
end

function draw(self: MyNode, renderer: Renderer)
-- Composite canvas.image or gpuCanvas.image 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. drawCanvas(self) - optional offscreen Canvas/GPUCanvas render phase
  5. draw(self, renderer) - on canvas repaint (keep pure, no state changes!)

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

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:isGamepad() then
local g = listenerContext:asGamepad()
if g then
print(g.deviceId, g.buttonMask, g.axis0)
end
end
end

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.

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 drawCanvas(self: ShaderNode)
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()
end

function draw(self: ShaderNode, renderer: Renderer)
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