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
statusvariable 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
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:
contextholds information about the run.context.environmentcontains 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 inscope.context.analysis_idis the ID of the Analysis.scopeholds 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:
- Choose a test device with numeric
temperaturereadings that all use the same unit. - Add a
DEVICE_IDenvironment variable containing that device's ID, and anALERT_ANALYSIS_IDvariable containing the ID of the Analysis to start. - 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:
| Helper | What it does |
|---|---|
Json | Reads and writes JSON: Json.encode(value), Json.decode(text) |
Date | Works with dates and times, including time zones: now, parse, parseLocal, format, add, sub, diff, startOf, endOf, isSame, weekday, offsetMinutes, isTimezone |
Base64 | Encodes and decodes Base64 |
Hex | Encodes and decodes hexadecimal |
Uuid | Generates 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.
| Function | What 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:
| Limit | Value |
|---|---|
| Run duration | 30 seconds |
| Memory per run | 8 MiB |
| Script size | 64 KiB |
| Data sent or received in one function call | 64 KiB |
| Console output per run | 8 KiB |
Analyses started per run with Analysis.run | 10 |
Entity rows per addData or editData call | 100 |
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.
| Error | Meaning |
|---|---|
BINDING_ERROR | An option was misspelled or has the wrong type. The message names the option |
OPERATION_ERROR | TagoIO rejected the request, for example an unknown variable or a limit reached. The message explains why |
FORBIDDEN_RESOURCE | The Access Management policy does not allow this action on this device or Analysis |
RATE_LIMIT | Too many requests in the last minute. Wait and retry |
INPUT_LIMIT | Too much data sent in one call. Split it across several calls |
OUTPUT_LIMIT | Too much data returned. Request fewer records or fewer fields |
RUNTIME_ERROR | The script itself failed. The message points to the line |
ARTIFACT_MISMATCH | The saved script is out of date. Upload it again |
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.