Skip to main content

Luau Runtime

Luau (luau-rt2026) is the low-cost Analysis runtime for scripts that read and write device and entity data. It runs on TagoIO only. You select it in the Runtime field when you create the Analysis and write the script in the same Script Editor as the other runtimes.

A Luau Analysis can work with devices and entities, list Analyses, and start other Analyses. It cannot do anything else: there is no internet access, no external libraries, no file access, and no Secrets or Analysis Token.

When to use Luau

Use Luau for scripts that only work with devices and entities, finish quickly, and run many times a day. Common examples:

  • Convert a raw reading, such as a pulse count to litres, and save the result back to the device
  • Calculate a value from recent data, such as an hourly average or a running total
  • Write a status variable to the device when a value crosses a threshold
  • Split the data a gateway sends and write each reading to the device it belongs to
  • Rename, tag, or activate devices in bulk
  • Save a computed reading as a row in an entity, or update an existing row

Luau executions do not count toward the monthly Analysis Service limit. Device and entity operations remain subject to the applicable service, resource, and rate limits. See Limits.

Use a Payload Parser to transform an incoming device payload. Use a Luau Analysis when the transformation also requires reading stored device data, writing additional values, or updating device settings.

When to use another runtime

Choose Deno, Node.js, or Python when the script needs to:

  • Call an external service or API
  • Use a library from npm or PyPI
  • Send an email, an SMS, or a notification
  • Work with dashboards, actions, files, or TagoRUN users
  • Require more than Luau's 30-second execution limit or 8 MiB memory limit
tip

Keep the Analysis that runs very often on Luau, even when one step needs a function Luau does not have. Put that step in a helper Analysis on Deno, Node.js, or Python and start it from the Luau script with Analysis.run. The frequent runs stay on the low-cost runtime, and the helper runs only when needed.

Writing a Luau Analysis

A Luau Analysis receives the same two arguments as every other Analysis:

  • context holds information about the run. context.environment contains your environment variables plus a few keys TagoIO adds about the trigger, such as _action_id, _dashboard_id, _widget_exec, and _user_id. The trigger data itself is in scope. context.analysis_id is the ID of the Analysis.
  • scope holds the trigger data: the data that fired the Action, the values a dashboard widget submitted, or the body of an API call.

Use print(...) to write to the Analysis console.

Register your code with Analysis.use. TagoIO calls the function with context and scope. Call it once, from the top level of the script. The built-in globals are read-only: a script that reassigns Devices, print, or another helper fails with a RUNTIME_ERROR.

The example below reads up to ten temperature records, averages their numeric values, writes the result to the same device as temperature_avg, and starts a helper Analysis when the average is too high.

Before running it:

  1. Choose a test device with numeric temperature readings that all use the same unit.
  2. Add a DEVICE_ID environment variable containing that device's ID, and an ALERT_ANALYSIS_ID variable containing the ID of the Analysis to start.
  3. Configure an Access Management policy that allows the Analysis to fetch the device, read its data, write data to it, and run the target Analysis.
Analysis.use(function(context, scope)
local device = Devices.get(context.environment.DEVICE_ID)

local rows = device:getData({ variables = "temperature", qty = 10 })
print("rows", #rows)

local total = 0
local count = 0

for _, point in rows do
if type(point.value) == "number" then
total += point.value
count += 1
end
end

if count == 0 then
print("No numeric temperature readings found; no average written.")
return
end

local average = total / count

device:addData({
variable = "temperature_avg",
value = average,
time = Date.format(Date.now()),
})

if average > 30 then
-- Helper Analysis on Deno, Node.js, or Python that sends the notification
Analysis.run(context.environment.ALERT_ANALYSIS_ID, {
device = device.id,
average = average,
})
end
end)

The helper Analysis receives the table passed to Analysis.run as its scope. Store device IDs, Analysis IDs, and settings as environment variables and read them from context.environment instead of writing them into the code.

Built-in helpers

Luau includes the standard language features for text, numbers, tables, and loops (string, table, math, coroutine, utf8, bit32, buffer, vector), plus these helpers:

HelperWhat it does
JsonReads and writes JSON: Json.encode(value), Json.decode(text)
DateWorks with dates and times, including time zones: now, parse, parseLocal, format, add, sub, diff, startOf, endOf, isSame, weekday, offsetMinutes, isTimezone
Base64Encodes and decodes Base64
HexEncodes and decodes hexadecimal
UuidGenerates a unique ID with Uuid.v4()

TagoIO sends and receives dates as ISO 8601 strings. Use Date.parse to read one and Date.format to write one. Date works in milliseconds, and time zones use standard names such as America/Chicago. Date.format(ms) produces 2026-09-16T14:00:00+00:00 in UTC unless you pass a time zone and a format; Date.parseLocal(text, timezone) reads wall-clock text such as 2026-09-16T09:00:00 in that time zone.

The os, io, debug, package, require, and loadstring libraries are not available. Use Date where you would use os.time or os.date.

Available functions

These are the TagoIO functions a Luau script can call. Anything not listed here is not available from Luau.

Devices.get and Devices.create return individual device objects, and Devices.list returns a list of them. Entities.get returns an individual entity object, and Entities.list returns a list of them.

Call methods on an individual object with a colon, as in device:getData() or entity:getData().

Analysis.run starts another Analysis without waiting for it to finish. A single execution can start at most 10. Analysis.list returns the Analyses the policy lets the script see, never their tokens or environment variables.

FunctionWhat it does
Devices.get(id)Fetches one device by ID
Devices.list(query?)Lists devices, with optional filters by name, tag, type, and more
Devices.create(payload)Creates a device. Requires at least a name and a type
device:getData(query?)Reads data from the device, with the same filters the device data API accepts
device:addData(item)Writes one value or a list of values to the device
device:edit(changes)Changes the device's name, description, tags, and other settings
device:params(query?)Reads the device's configuration parameters
device:setParams(list)Creates or updates configuration parameters
Entities.get(id)Fetches one entity by ID, including its schema and indexes
Entities.list(query?)Lists entities, with optional filters by name, ID, and tag
entity:getData(query?)Reads rows from the entity, with optional fields, filter, index, order, page, and amount
entity:addData(rows)Inserts one row or a list of rows
entity:editData(rows)Updates one row or a list of rows by id
Analysis.run(id, scope?)Starts another Analysis by ID. The optional scope table is passed to that Analysis as its scope
Analysis.list(query?)Lists Analyses, with optional filters by name, ID, active state, and tag

Each function accepts a fixed set of options. A misspelled or wrongly typed option fails the call before any request is sent, and the error message names the option.

local sensors = Devices.list({
filter = { tags = { { key = "kind", value = "sensor" } } },
amount = 50,
})

for _, device in sensors do
device:addData({ variable = "heartbeat", value = 1 })
end

Permissions

A Luau Analysis has no Analysis Token. Its permissions come from an Access Management policy that targets the Analysis.

Create a policy that targets the Analysis, either directly or by a tag it carries, and allow the actions the script uses. Every function requires this. Without a policy that covers a device or entity, reads and writes on it fail with a FORBIDDEN_RESOURCE error, and Devices.list, Entities.list, and Analysis.list return an empty list. Analysis.run requires the Run Analysis action on the target Analysis.

Creating devices works differently: a policy grants it by tag or for all devices, never for one specific device, because the device does not exist yet.

Limits

Standard TagoIO rate limits and resource limits apply to everything a Luau script does, the same as a direct API call. The runtime also sets these limits:

LimitValue
Run duration30 seconds
Memory per run8 MiB
Script size64 KiB
Data sent or received in one function call64 KiB
Console output per run8 KiB
Analyses started per run with Analysis.run10
Entity rows per addData or editData call100

TagoIO does not truncate results or output. If a result is too large, the call fails and returns nothing. If a script prints past the console limit, the run stops and everything printed so far stays visible.

Luau runs do not count toward the Analysis Service monthly limit. They have their own run rate limit instead.

Errors

Errors appear in the Analysis console together with anything the script printed before the failure. You can also catch an error inside the script with pcall.

ErrorMeaning
BINDING_ERRORAn option was misspelled or has the wrong type. The message names the option
OPERATION_ERRORTagoIO rejected the request, for example an unknown variable or a limit reached. The message explains why
FORBIDDEN_RESOURCEThe Access Management policy does not allow this action on this device or Analysis
RATE_LIMITToo many requests in the last minute. Wait and retry
INPUT_LIMITToo much data sent in one call. Split it across several calls
OUTPUT_LIMITToo much data returned. Request fewer records or fewer fields
RUNTIME_ERRORThe script itself failed. The message points to the line
ARTIFACT_MISMATCHThe saved script is out of date. Upload it again
note

Luau appears in the runtime list only where the platform offers it. A TagoDeploy instance running an older version does not list it until it updates.