Lua API reference
This page is generated from the server's function registry. Each function lists the contexts it is available in: ui_event (form events), report (report expressions) and job (scheduled and on-demand jobs).
artifacts
Files a job run produces, downloadable from the run's history.
Available in: job.
artifacts.save
artifacts.save(name, content, {mime}?) → {name, size}
Saves a file for this run, within the workspace's artifact quota.
| Parameter | Type | Description |
|---|---|---|
name |
string | File name: letters, digits, spaces, '.', '_' or '-' (at most 128 characters). Saving the same name again replaces it. |
content |
string | File content (at most 16 MiB). |
options (optional) |
table | mime: the media type (default from the file extension). |
Errors:
BAD_REQUEST: invalid nameQUOTA_EXCEEDED: artifact storage full or too many artifacts in this run
artifacts.save("summary.csv", csv.encode(rows, {columns = {"id", "total"}}))
crypto
Pure cryptographic hashing, HMAC signatures, binary encoding and secure random generators. Evaluated in-memory with zero network or DNS lookups.
Available in: ui_event, report, job.
crypto.sha256
crypto.sha256(data) → string
Calculates the SHA-256 hash of a string.
| Parameter | Type | Description |
|---|---|---|
data |
string | Data to hash. |
Returns: hex-encoded SHA-256 digest (64 characters)
local hash = crypto.sha256("hello world")
crypto.sha512
crypto.sha512(data) → string
Calculates the SHA-512 hash of a string.
| Parameter | Type | Description |
|---|---|---|
data |
string | Data to hash. |
Returns: hex-encoded SHA-512 digest (128 characters)
local hash = crypto.sha512("hello world")
crypto.hmac_sha256
crypto.hmac_sha256(key, message) → string
Calculates an HMAC-SHA256 signature.
| Parameter | Type | Description |
|---|---|---|
key |
string | Secret key. |
message |
string | Message payload. |
Returns: hex-encoded HMAC-SHA256 signature
local sig = crypto.hmac_sha256("secret-key", "payload")
crypto.hmac_sha512
crypto.hmac_sha512(key, message) → string
Calculates an HMAC-SHA512 signature.
| Parameter | Type | Description |
|---|---|---|
key |
string | Secret key. |
message |
string | Message payload. |
Returns: hex-encoded HMAC-SHA512 signature
local sig = crypto.hmac_sha512("secret-key", "payload")
crypto.base64_encode
crypto.base64_encode(data) → string
Encodes a string using standard Base64 with padding.
| Parameter | Type | Description |
|---|---|---|
data |
string | Data to encode. |
Returns: standard Base64-encoded string (RFC 4648 §4)
local b64 = crypto.base64_encode("credentials:secret")
crypto.base64_decode
crypto.base64_decode(data) → string|nil, err
Decodes a standard Base64 string.
| Parameter | Type | Description |
|---|---|---|
data |
string | Base64-encoded string. |
Returns: decoded string (or nil, error)
local plain, err = crypto.base64_decode("Y3JlZGVudGlhbHM6c2VjcmV0")
crypto.base64url_encode
crypto.base64url_encode(data) → string
Encodes a string using unpadded URL-safe Base64.
| Parameter | Type | Description |
|---|---|---|
data |
string | Data to encode. |
Returns: unpadded URL-safe Base64-encoded string (RFC 4648 §5)
local enc = crypto.base64url_encode('{"alg":"none"}')
crypto.base64url_decode
crypto.base64url_decode(data) → string|nil, err
Decodes an unpadded or padded URL-safe Base64 string.
| Parameter | Type | Description |
|---|---|---|
data |
string | URL-safe Base64-encoded string. |
Returns: decoded string (or nil, error)
local plain, err = crypto.base64url_decode("eyJhbGciOiJub25lIn0")
crypto.hex_encode
crypto.hex_encode(data) → string
Encodes bytes to a hexadecimal string.
| Parameter | Type | Description |
|---|---|---|
data |
string | Raw bytes or string. |
Returns: lowercase hexadecimal string
local h = crypto.hex_encode("abc")
crypto.hex_decode
crypto.hex_decode(data) → string|nil, err
Decodes a hexadecimal string to raw bytes.
| Parameter | Type | Description |
|---|---|---|
data |
string | Hexadecimal string. |
Returns: decoded string (or nil, error)
local raw, err = crypto.hex_decode("616263")
crypto.random_hex
crypto.random_hex(n) → string
Generates cryptographically secure random bytes as a hex string.
| Parameter | Type | Description |
|---|---|---|
n |
number | Number of random bytes (at least 1, at most 1024). |
Returns: hexadecimal string of length 2n
local token = crypto.random_hex(16)
crypto.uuid
crypto.uuid() → string
Generates a cryptographically random RFC 4122 v4 UUID.
Returns: canonical lowercase RFC 4122 v4 UUID string
local id = crypto.uuid()
csv
CSV parsing and writing (RFC 4180 quoting). All cell values are strings when parsing; a leading UTF-8 byte-order mark is ignored. Rows may have differing lengths. Unknown option names raise an error.
Available in: ui_event, report, job.
csv.parse
csv.parse(s, opts?)
Parses all of s into an array of rows. On malformed CSV returns nil and a message of the form "line N: message".
| Parameter | Type | Description |
|---|---|---|
s |
string | CSV text. |
opts (optional) |
table | {header = true, delimiter = ",", trim = false}. With header = true each row is a table keyed by the header names (empty names become column_N, duplicates get _2, _3 suffixes; short rows are padded with empty strings, extra cells are named column_N). With header = false rows are arrays of strings. trim = true strips surrounding whitespace from every cell. delimiter must be a single character. |
Returns: table|nil, string|nil
Errors:
- invalid option
local rows, err = csv.parse("name,qty\nbolt,3\n")
return rows[1].name -- "bolt"
csv.rows
csv.rows(s, opts?)
Returns an iterator yielding one row per call, for use in a generic for loop. Malformed CSV raises an error when the bad line is reached.
| Parameter | Type | Description |
|---|---|---|
s |
string | CSV text. |
opts (optional) |
table | {header = true, delimiter = ",", trim = false}. With header = true each row is a table keyed by the header names (empty names become column_N, duplicates get _2, _3 suffixes; short rows are padded with empty strings, extra cells are named column_N). With header = false rows are arrays of strings. trim = true strips surrounding whitespace from every cell. delimiter must be a single character. |
Returns: function
Errors:
- invalid option
- malformed CSV while iterating (message "line N: ...")
for row in csv.rows(text) do
total = total + tonumber(row.qty)
end
csv.encode
csv.encode(rows, opts?)
Encodes rows as CSV text with LF line endings and standard quoting.
| Parameter | Type | Description |
|---|---|---|
rows |
table | Array of rows. Each row is an array of values, or a table keyed by column name (then opts.columns is required). |
opts (optional) |
table | {columns = {"a", "b"}, delimiter = ",", header = true}. columns selects and orders keyed-row cells and supplies the header line. For array rows a header line is written only when columns is given. Cells may be strings, numbers, booleans, decimals, nil or db.NULL (empty). |
Returns: string
Errors:
- columns missing for keyed rows
- unsupported cell value
- result exceeds the maximum string size (SCRIPT_LIMIT_EXCEEDED)
return csv.encode({{sku = "A1", qty = 2}}, {columns = {"sku", "qty"}}) -- "sku,qty\nA1,2\n"
data
Tabular dataset manipulation: grouping, multi-metric aggregation, pivot tables, sorting and transformations on row arrays.
Available in: ui_event, report, job.
data.group_by
data.group_by(rows, key) → table
Groups an array of rows by a key field.
| Parameter | Type | Description |
|---|---|---|
rows |
table | Array of row tables. |
key |
string | Field name to group by. |
Returns: table mapping each distinct group key value to an array of matching rows
local byRegion = data.group_by(customers, "Region")
data.aggregate
data.aggregate(rows, spec) → rows
Performs grouped rollups with multiple metric aggregations in a single pass.
| Parameter | Type | Description |
|---|---|---|
rows |
table | Array of row tables. |
spec |
table | {by?: string|array, metrics: table}: metrics maps output column name to agg function ("count", "sum", "avg", "min", "max", "median", "first", "last") or {field, fn}. |
Returns: array of aggregated row tables
local summary = data.aggregate(orders, {
by = "Region",
metrics = {
order_count = "count",
total_sales = {field = "Total", fn = "sum"},
avg_sales = {field = "Total", fn = "avg"},
}
})
data.summarize
data.summarize(rows, spec) → rows
Alias for data.aggregate.
| Parameter | Type | Description |
|---|---|---|
rows |
table | Array of row tables. |
spec |
table | Aggregation specification (alias for data.aggregate). |
Returns: array of aggregated row tables
local summary = data.summarize(orders, {by = "Category", metrics = {revenue = "sum"}})
data.pivot
data.pivot(rows, spec) → rows
Generates a two-dimensional pivot table from tabular rows.
| Parameter | Type | Description |
|---|---|---|
rows |
table | Array of row tables. |
spec |
table | {row: string, col: string, value: string, agg?: string}. |
Returns: array of pivot table row tables (with row key, column keys, and total)
local pt = data.pivot(sales, {row = "Region", col = "Quarter", value = "Amount", agg = "sum"})
data.sort_by
data.sort_by(rows, spec [, desc]) → rows
Sorts an array of row tables by one or multiple fields.
| Parameter | Type | Description |
|---|---|---|
rows |
table | Array of row tables. |
spec |
string|table | Field name or array of field names (prefix with "-" for descending). |
desc (optional) |
boolean | True to sort descending (when spec is a single field name). |
Returns: new array containing sorted rows
local sorted = data.sort_by(products, {"Category", "-Price"})
data.pluck
data.pluck(rows, field) → array
Extracts a single column from an array of row tables.
| Parameter | Type | Description |
|---|---|---|
rows |
table | Array of row tables. |
field |
string | Field name to extract. |
Returns: 1-indexed array of values
local ids = data.pluck(users, "id")
data.index_by
data.index_by(rows, key) → table
Indexes an array of rows by a unique key field.
| Parameter | Type | Description |
|---|---|---|
rows |
table | Array of row tables. |
key |
string | Field name whose values serve as keys. |
Returns: table mapping key values to their corresponding row tables
local userMap = data.index_by(users, "id")
data.distinct
data.distinct(rows [, field]) → array
Returns unique elements or distinct values of a field.
| Parameter | Type | Description |
|---|---|---|
rows |
table | Array of row tables or array of scalar values. |
field (optional) |
string | Field name to extract distinct values of. |
Returns: array of distinct values or deduplicated rows
local cities = data.distinct(customers, "City")
data.frequencies
data.frequencies(rows [, field]) → array
Computes frequency counts and proportions.
| Parameter | Type | Description |
|---|---|---|
rows |
table | Array of row tables or scalar values. |
field (optional) |
string | Field name to count frequencies for. |
Returns: array of {value, count, ratio} sorted descending by count
local statusCounts = data.frequencies(orders, "Status")
data.top_n
data.top_n(rows, n, field [, desc]) → rows
Returns the top n rows ranked by a field.
| Parameter | Type | Description |
|---|---|---|
rows |
table | Array of row tables. |
n |
number | Maximum number of rows to return. |
field |
string | Field name to rank by. |
desc (optional) |
boolean | True for highest first (default true). |
Returns: array of up to n row tables
local top5 = data.top_n(customers, 5, "TotalSpent")
data.filter
data.filter(rows, predicate) → rows
Filters rows using a predicate function or match table.
| Parameter | Type | Description |
|---|---|---|
rows |
table | Array of row tables. |
predicate |
function|table | Function returning boolean or table of matching key-values. |
Returns: filtered array of row tables
local active = data.filter(users, {Active = true})
data.slice
data.slice(rows, start, count) → rows
Extracts a slice of rows.
| Parameter | Type | Description |
|---|---|---|
rows |
table | Array of row tables. |
start |
number | 1-indexed starting position. |
count |
number | Number of rows to take. |
Returns: sub-array of row tables
local page1 = data.slice(rows, 1, 10)
datetime
Dates and times. A timestamp is an ISO 8601 UTC string like "2026-03-09T05:30:00.000Z"; a date is "2026-01-31". Timestamps and dates are plain strings, so they sort and compare correctly with < and ==. Calendar arithmetic and local-time parsing use the workspace time zone unless a tz argument is given. Invalid tz names raise an error.
Available in: ui_event, report, job.
datetime.now
datetime.now()
Returns the current time as an ISO UTC timestamp (millisecond precision).
Returns: string
local started = datetime.now() -- "2026-03-09T05:30:00.123Z"
datetime.today
datetime.today(tz?)
Returns today's date (YYYY-MM-DD) in the time zone.
| Parameter | Type | Description |
|---|---|---|
tz (optional) |
string | IANA time zone name such as "America/New_York". Defaults to the workspace time zone. |
Returns: string
Errors:
- unknown time zone
local d = datetime.today("Europe/Paris")
datetime.parse
datetime.parse(s, tz?)
Parses a date/time into an ISO UTC timestamp. On failure returns nil and an error message instead of raising.
| Parameter | Type | Description |
|---|---|---|
s |
string | RFC 3339 (any offset), "YYYY-MM-DD" (midnight in tz), "YYYY-MM-DD HH:MM" or "YYYY-MM-DD HH:MM:SS" (local time in tz). |
tz (optional) |
string | IANA time zone name such as "America/New_York". Defaults to the workspace time zone. |
Returns: string|nil, string|nil
Errors:
- unknown time zone
- s is not a string
local ts, err = datetime.parse("2026-03-08 21:30", "America/New_York")
if not ts then error(err) end
datetime.format
datetime.format(ts, layout, tz?)
Formats a time in the time zone. %Z is the zone abbreviation and %z the numeric offset such as +0100.
| Parameter | Type | Description |
|---|---|---|
ts |
string | A timestamp or date (any form accepted by datetime.parse). |
layout |
string | strftime-style layout. Supported: %Y %m %d %H %M %S %y %b %B %a %A %p %I %j %Z %z %%. |
tz (optional) |
string | IANA time zone name such as "America/New_York". Defaults to the workspace time zone. |
Returns: string
Errors:
- ts cannot be parsed
- unknown directive in layout
- unknown time zone
- result exceeds the maximum string size (SCRIPT_LIMIT_EXCEEDED)
return datetime.format("2026-03-09T05:30:00.000Z", "%d %b %Y %I:%M %p", "America/New_York") -- "09 Mar 2026 01:30 AM"
datetime.add
datetime.add(ts, spec, tz?)
Adds calendar and clock amounts. years, months, days and weeks use calendar arithmetic in the time zone so local clock time is kept across daylight-saving changes; adding months clamps to the end of shorter months (Jan 31 + 1 month = Feb 28). hours, minutes and seconds are exact elapsed time. When ts is a date and only years/months/days/weeks are given the result is a date, otherwise a timestamp.
| Parameter | Type | Description |
|---|---|---|
ts |
string | A timestamp or date. |
spec |
table | Any of years, months, days, weeks (whole numbers, may be negative) and hours, minutes, seconds (fractions allowed). |
tz (optional) |
string | IANA time zone name such as "America/New_York". Defaults to the workspace time zone. |
Returns: string
Errors:
- ts cannot be parsed
- unknown key in spec
- non-integer years/months/days/weeks
- result out of range
- unknown time zone
datetime.add("2026-03-08T06:30:00.000Z", {days = 1}, "America/New_York") -- "2026-03-09T05:30:00.000Z"
datetime.diff
datetime.diff(a, b, unit, tz?)
Returns b - a in the unit. seconds to weeks are exact elapsed time (a day is 24 hours) and may be fractional. months and years count whole calendar months/years in the time zone, truncated toward zero.
| Parameter | Type | Description |
|---|---|---|
a |
string | Start timestamp or date. |
b |
string | End timestamp or date. |
unit |
string | One of "seconds", "minutes", "hours", "days", "weeks", "months", "years". |
tz (optional) |
string | IANA time zone name such as "America/New_York". Defaults to the workspace time zone. |
Returns: number
Errors:
- a or b cannot be parsed
- unknown unit
- unknown time zone
datetime.diff("2026-01-01", "2026-03-15", "days") -- 73
db
Read and change this workspace's tables. db.select, db.update and db.delete need no SQL, so a script that uses them runs the same on SQLite and PostgreSQL; their writes are validated and recorded in row history. db.query and db.exec take SQL in the workspace database's own dialect: reads run on a snapshot, writes need edit rights and are not recorded in row history (use records or db.update for tracked edits), and system tables, schema changes and database settings are not allowed. On PostgreSQL, SQL text cannot run inside db.transaction.
Available in: ui_event, report, job.
NULL(userdata): SQL NULL. Lua tables cannot hold nil, so empty field values are db.NULL.
db.query
db.query(sql, params?) → rows
Runs a read-only query and returns up to 10,000 rows.
| Parameter | Type | Description |
|---|---|---|
sql |
string | One read-only statement; use :name placeholders. |
params (optional) |
table | Values for :name placeholders. |
Returns: array of row tables keyed by column name (SQL NULL is db.NULL)
Errors:
- SQL_NOT_ALLOWED
- BAD_REQUEST
local rows = db.query("SELECT Name FROM Customers WHERE City = :city", {city = "Paris"})
db.query_one
db.query_one(sql, params?) → row|nil
Like db.query but returns only the first row.
Returns: first row or nil
local c = db.query_one("SELECT * FROM Customers WHERE id = :id", {id = 1})
db.query_value
db.query_value(sql, params?) → value|nil
Returns a single value.
Returns: first column of the first row, or nil when there are no rows
local n = db.query_value("SELECT count(*) FROM Orders")
db.iter
db.iter(sql, params?) → iterator
Iterates over a large result in pages of 1,000 rows (jobs only).
Available in: job.
Returns: iterator yielding row tables
for row in db.iter("SELECT * FROM Orders") do total = total + row.Qty end
db.exec
db.exec(sql, params?) → {changes, last_id}
Runs one INSERT, UPDATE or DELETE on a workspace table. Not available in reports.
Available in: ui_event, job.
Returns: table with changes and last_id
Errors:
- SQL_NOT_ALLOWED
- VALIDATION_FAILED
- LINKED_RECORDS
db.exec("UPDATE Orders SET Status = 'Shipped' WHERE id = :id", {id = 7})
db.transaction
db.transaction(fn) → results of fn
Runs fn inside one write transaction (all-or-nothing). Raising an error inside fn rolls back. The block has its own time budget (250 ms in form events, 2 s in jobs).
Available in: ui_event, job.
db.transaction(function() db.exec("UPDATE Stock SET Qty = Qty - 1 WHERE id = :id", {id = 3}) records.insert("Moves", {Item = 3}) end)
db.select
db.select{from, where?, order?, limit?, offset?} → rows
Reads rows without SQL, so the script runs the same on every database. where maps fields to a value (db.NULL: empty), a list (any of its values) or operators: eq, ne, lt, le, gt, ge, contains, starts, ends, in, between, is_null; _or takes a list of where tables. order lists fields, "-" first for descending. limit is 100 unless given (at most 500).
Returns: array of row tables, as records.get returns them
Errors:
- BAD_REQUEST
local rows = db.select{from = "Orders", where = {Status = "Open", Total = {gt = 100}}, order = {"-Ordered"}, limit = 50}
db.update
db.update{table, set, where} → {changes}
Updates every matching row as records.update would: validated, versioned and recorded in row history, all or nothing. where is required (where = {} means every row); at most 10,000 rows. Not available in reports.
Available in: ui_event, job.
Returns: table with changes
Errors:
- VALIDATION_FAILED
- CONCURRENT_MODIFICATION
- BAD_REQUEST
db.update{table = "Orders", set = {Status = "Shipped"}, where = {id = 42}}
db.delete
db.delete{table, where} → {changes}
Deletes every matching row as records.delete would, all or nothing. where is required; at most 10,000 rows. Not available in reports.
Available in: ui_event, job.
Returns: table with changes
Errors:
- LINKED_RECORDS
- BAD_REQUEST
db.delete{table = "Drafts", where = {Status = "Abandoned"}}
db.lookup
db.lookup(table, field, where?, params?) → value|db.NULL
Access DLookup: the field's value in the first matching row, or db.NULL.
local email = db.lookup("Customers", "Email", "id = :id", {id = record.Customer})
db.count
db.count(table, field?, where?, params?) → number
Access DCount: number of matching rows (non-empty values of field, or all rows when field is "*" or omitted).
local open = db.count("Orders", "*", "Status = 'Open'")
db.sum
db.sum(table, field, where?, params?) → number|db.NULL
Access DSum.
local total = db.sum("OrderLines", "Qty", "OrderId = :id", {id = record.id})
db.avg
db.avg(table, field, where?, params?) → number|db.NULL
Access DAvg.
db.avg("Products", "Price")
db.min
db.min(table, field, where?, params?) → value|db.NULL
Access DMin.
db.min("Orders", "OrderDate")
db.max
db.max(table, field, where?, params?) → value|db.NULL
Access DMax.
db.max("Orders", "OrderDate")
db.is_null
db.is_null(v) → boolean
True for nil and db.NULL.
if db.is_null(record.ShippedAt) then … end
db.from
db.from(table) → builder
Starts a dialect-neutral query builder on table.
local rows = db.from("Orders"):where("Status", "Open"):order("-Ordered"):limit(10):all()
db.scalar
db.scalar(sql, params?) → value|nil
Runs a read query and returns a single value.
Returns: first column of the first row, or nil when there are no rows
local n = db.scalar("SELECT count(*) FROM Orders")
db.pluck
db.pluck(sql, params?) → array
Runs a read query and returns a 1-indexed list of the first column across all rows.
Returns: array of values from the first column across all rows
local ids = db.pluck("SELECT id FROM Orders WHERE Status = 'Open'")
decimal
Exact base-10 arithmetic. Currency is stored as an integer scaled by 10^4 and appears in scripts as decimal values, so decimal.new("0.1") + decimal.new("0.2") == decimal.new("0.3") is true.
Decimals support + - * / % unary minus, == < <= > >= and .. (concatenation). Arithmetic operands may be decimals, numbers (converted through their shortest exact decimal representation) or numeric strings. Lua 5.1 caveats: == is only evaluated when both sides are decimals, so decimal == number is always false; < <= > >= only work between two decimals and comparing a decimal with a number raises an error. Convert first: d == decimal.new(1), d < decimal.new("2.5"). Division keeps 16 decimal places and raises on division by zero; % follows Lua (the result has the sign of the divisor). Values are limited to about 4,900 digits and an exponent of 1000.
Decimal values have the methods d:round(places), d:tostring() and d:tonumber().
Available in: ui_event, report, job.
decimal.new
decimal.new(value)
Creates a decimal. Prefer strings for exact values; a number is converted through its shortest round-trip representation, so decimal.new(0.1) is exactly 0.1.
| Parameter | Type | Description |
|---|---|---|
value |
string|number|decimal | A numeric string such as "12.50" or "1e3", a finite number, or another decimal. |
Returns: decimal
Errors:
- value is not a number, numeric string or decimal
- the string is not a valid decimal or is too large
local price = decimal.new("19.99")
return price * 3 -- 59.97
decimal.is_decimal
decimal.is_decimal(value)
Returns true when value is a decimal.
| Parameter | Type | Description |
|---|---|---|
value |
any | Any value. |
Returns: boolean
return decimal.is_decimal(decimal.new(1)) -- true
event
The form event being handled.
Available in: ui_event.
name(string): Event name: load, current, before_insert, before_update, after_update, before_delete, after_delete, close, click, change, confirm.control(string|nil): Name of the control that raised a control event.old_value(any): Previous value for change/before_update control events.new_value(any): New value for change/before_update control events.args(table): Arguments the form was opened with (open_args).answer(boolean|nil): The user's answer when re-invoked after ui.confirm.
fetch
Makes an HTTP request from a job. Only hosts and ports on the platform's egress allowlist, which platform administrators manage for all workspaces, are reachable (https unless the platform also allows http); an empty allowlist permits nothing; private and internal addresses are always refused, including after redirects (at most 5).
Available in: job.
fetch
fetch(url, {method, headers, query, params, body, json, form, bearer, basic_auth, timeout_ms, max_bytes}?) → response
Sends the request and reads the whole response.
| Parameter | Type | Description |
|---|---|---|
url |
string | Absolute https URL. |
options (optional) |
table | method (default GET); headers (string → string); query/params (table of query parameters); body (string); json (value encoded as application/json); form (table encoded as application/x-www-form-urlencoded); bearer (string token); basic_auth ({username, password}); timeout_ms (default 10000, at most 30000); max_bytes (default 2 MiB, at most 10 MiB). |
Returns: {status, ok, headers, body, json(), text(), header(name)}: ok is true for 2xx; header names are lower-case; response:json() decodes the body; response:text() returns body string; response:header(name) returns a header value case-insensitively.
Errors:
EGRESS_DENIED: host, port or address not allowedEGRESS_RESPONSE_TOO_LARGE: body larger than max_bytesEGRESS_FAILED: network error or timeoutRATE_LIMITED: too many requests from this workspace
local r = fetch("https://api.example.com/rates", {params = {base = "USD"}})
if r.ok then log.info("rates", {count = #r:json().items}) end
fetch.get
fetch.get(url, options?) → response
Convenience helper for fetch(url, {method = "GET", ...}).
| Parameter | Type | Description |
|---|---|---|
url |
string | Absolute https URL. |
options (optional) |
table | Request options (headers, query/params, bearer, etc.). |
Returns: response table
local r = fetch.get("https://api.example.com/users", {params = {active = "true"}})
fetch.post
fetch.post(url, options?) → response
Convenience helper for fetch(url, {method = "POST", ...}).
| Parameter | Type | Description |
|---|---|---|
url |
string | Absolute https URL. |
options (optional) |
table | Request options (body, json, form, headers, etc.). |
Returns: response table
local r = fetch.post("https://api.example.com/items", {json = {name = "Widget", price = 19.99}})
fetch.put
fetch.put(url, options?) → response
Convenience helper for fetch(url, {method = "PUT", ...}).
| Parameter | Type | Description |
|---|---|---|
url |
string | Absolute https URL. |
options (optional) |
table | Request options (body, json, headers, etc.). |
Returns: response table
local r = fetch.put("https://api.example.com/items/12", {json = {name = "Updated Widget"}})
fetch.patch
fetch.patch(url, options?) → response
Convenience helper for fetch(url, {method = "PATCH", ...}).
| Parameter | Type | Description |
|---|---|---|
url |
string | Absolute https URL. |
options (optional) |
table | Request options (body, json, headers, etc.). |
Returns: response table
local r = fetch.patch("https://api.example.com/items/12", {json = {price = 24.99}})
fetch.delete
fetch.delete(url, options?) → response
Convenience helper for fetch(url, {method = "DELETE", ...}).
| Parameter | Type | Description |
|---|---|---|
url |
string | Absolute https URL. |
options (optional) |
table | Request options (headers, query/params, etc.). |
Returns: response table
local r = fetch.delete("https://api.example.com/items/12")
files
File attachments. A file is identified by its id, the value stored in an attachments field. Content is read and written as a Lua string; reads count against the run's memory budget.
Available in: ui_event, report, job.
files.info
files.info(id) → info|nil
Describes a file without reading it.
| Parameter | Type | Description |
|---|---|---|
id |
string | A file id, as stored in an attachments field. |
Returns: {id, name, size, content_type, sha256, width, height, state, created_at} (width and height only for images; state is "pending" or "attached"), or nil when no such file exists in this workspace.
local f = files.info(record.Photos[1])
if f then log.info(f.name, {size = f.size}) end
files.read
files.read(id) → content|nil
Reads a whole file. Use it for small text or data files; images and documents are meant to be shown to people, not parsed in Lua.
| Parameter | Type | Description |
|---|---|---|
id |
string | A file id. |
Returns: The file's bytes as a string, or nil when no such file exists.
Errors:
BAD_REQUEST: the file is larger than this context allows (1 MiB in form events and reports, 10 MiB in jobs)SCRIPT_LIMIT_EXCEEDED: the run has read more than its memory budget
local text = files.read(id)
local rows = csv.parse(text)
files.create
files.create(name, content, content_type?) → info
Stores a new file.
Available in: job.
| Parameter | Type | Description |
|---|---|---|
name |
string | The file name people will see; any directory part is dropped. |
content |
string | The bytes (at most the server's file size limit, 25 MiB by default). |
content_type (optional) |
string | Accepted for readability and ignored: the type is detected from the content. |
Returns: The new file's info table. The file is pending until a row refers to it: put info.id into an attachments field within 24 hours.
Errors:
QUOTA_EXCEEDED: the workspace's file storage is fullBAD_REQUEST: empty or too largeRATE_LIMITED: too many uploads at once
local f = files.create("totals.csv", csv.encode(rows, {columns = {"id", "total"}}))
records.update("Reports", report_id, {Attachment = {f.id}})
geo
Locations and areas. Use these with geo_point and geo_area fields: records.get returns their values as geometry tables and records.insert and records.update accept them.
A point is a table {type = "Point", coordinates = {longitude, latitude}} and an area is a GeoJSON Polygon or MultiPolygon table. Everywhere a point is expected you can also pass {lat = .., lon = ..}, the text "lat, lon", or a GeoJSON Feature; everywhere an area is expected, a Feature or GeoJSON text. Coordinates are rounded to 7 decimal places. Areas that cross the 180° meridian are not supported.
Distances are in metres along the Earth's surface (haversine) and areas in square metres (spherical approximation).
Available in: ui_event, report, job.
geo.point
geo.point(lat, lon, alt?)
Builds a point. Note the argument order: latitude first, as people write coordinates; the stored coordinates are [longitude, latitude] as GeoJSON requires.
| Parameter | Type | Description |
|---|---|---|
lat |
number | Latitude in degrees, -90 to 90. |
lon |
number | Longitude in degrees, -180 to 180. |
alt (optional) |
number | Altitude in metres, kept as the third coordinate. |
Returns: table
Errors:
- lat or lon is not a number
- lat is outside -90 to 90 or lon is outside -180 to 180
local p = geo.point(51.50135, -0.14189)
return p.coordinates[1] -- -0.14189 (the longitude)
geo.lat
geo.lat(point)
Returns a point's latitude.
| Parameter | Type | Description |
|---|---|---|
point |
table|string | A point. |
Returns: number
Errors:
- point is not a valid point
return geo.lat("51.5, -0.14") -- 51.5
geo.lon
geo.lon(point)
Returns a point's longitude.
| Parameter | Type | Description |
|---|---|---|
point |
table|string | A point. |
Returns: number
Errors:
- point is not a valid point
return geo.lon("51.5, -0.14") -- -0.14
geo.distance
geo.distance(a, b)
The great-circle distance between two points, in metres. It is symmetric: distance(a, b) equals distance(b, a).
| Parameter | Type | Description |
|---|---|---|
a |
table|string | A point. |
b |
table|string | A point. |
Returns: number
Errors:
- a or b is not a valid point
return geo.distance(geo.point(51.5074, -0.1278), geo.point(48.8566, 2.3522)) -- about 343,500
geo.within
geo.within(point, area)
Tests whether a point is inside an area. Holes are outside, and a point exactly on the boundary counts as inside.
| Parameter | Type | Description |
|---|---|---|
point |
table|string | A point. |
area |
table|string | A Polygon or MultiPolygon. |
Returns: boolean
Errors:
- point is not a valid point
- area is not a valid area
local area = records.get("Zones", 1).Fence
return geo.within(geo.point(5, 5), area)
geo.area
geo.area(area)
The area in square metres (holes are subtracted).
| Parameter | Type | Description |
|---|---|---|
area |
table|string | A Polygon or MultiPolygon. |
Returns: number
Errors:
- area is not a valid area
return geo.area(records.get("Zones", 1).Fence) / 10000 -- hectares
geo.centroid
geo.centroid(area)
The area-weighted centre of an area, as a point. For a concave area it can lie outside the area.
| Parameter | Type | Description |
|---|---|---|
area |
table|string | A Polygon or MultiPolygon. |
Returns: table
Errors:
- area is not a valid area
return geo.centroid(records.get("Zones", 1).Fence)
geo.bbox
geo.bbox(area)
The bounding box as a list {minLon, minLat, maxLon, maxLat}.
| Parameter | Type | Description |
|---|---|---|
area |
table|string | A Polygon or MultiPolygon. |
Returns: table
Errors:
- area is not a valid area
local b = geo.bbox(records.get("Zones", 1).Fence)
return b[3] - b[1] -- width in degrees
job
The running job.
Available in: job.
id(string): Job id.name(string): Job name.run_id(string): This run's id.args(table): The job's args, with any enqueue args merged over them.
jobs
Starts background jobs.
Available in: ui_event, job.
jobs.enqueue
jobs.enqueue(name, args?) → run_id
Queues a run of a job. From a form, the user must be an editor or above and the job's args must contain allow_enqueue = true.
| Parameter | Type | Description |
|---|---|---|
name |
string | Job name. |
args (optional) |
table | Arguments merged over the job's own args (string keys; JSON-compatible values). |
Returns: The new run's id. The run starts as soon as a worker is free, following the job's overlap policy.
Errors:
NOT_FOUND: no job with that nameFORBIDDEN: the job does not allow enqueue from formsWORKSPACE_BUSY: the job is already running (overlap policy)
local run = jobs.enqueue("Send invoices", {customer = record.id})
json
JSON encoding and decoding.
Encoding rules: tables with keys exactly 1..n (n >= 1) or marked with json.array become arrays; an empty unmarked table becomes {}; other tables become objects with sorted keys (keys must be strings or numbers). json.null and db.NULL become null; decimals become JSON strings. Integral numbers within +/-2^53 are written without a decimal point; NaN and infinity raise. Nesting deeper than 100 levels and cycles raise.
Decoding maps objects to tables, arrays to tables marked as arrays (so they re-encode as arrays even when empty), null to json.null, and integers beyond +/-2^53 to strings to avoid precision loss.
json.array and json.object mark a table by giving it a private metatable. Calling setmetatable on a marked table replaces the metatable and drops the mark.
Available in: ui_event, report, job.
null(userdata): The JSON null value. Compare with ==; it is distinct from nil and db.NULL but both encode as null.
json.encode
json.encode(value, opts?)
Encodes a Lua value as JSON text.
| Parameter | Type | Description |
|---|---|---|
value |
any | The value to encode. |
opts (optional) |
table | {pretty = boolean}: pretty = true indents with two spaces. |
Returns: string
Errors:
- unsupported value type (function, thread, non-null userdata)
- NaN or infinity
- table keys that are not strings or numbers
- nesting deeper than 100 or cyclic tables
- result exceeds the maximum string size (SCRIPT_LIMIT_EXCEEDED)
return json.encode({name = "Ada", tags = json.array({})}) -- {"name":"Ada","tags":[]}
json.decode
json.decode(s)
Parses JSON text. On invalid JSON returns nil and an error message instead of raising. A literal null decodes to json.null (not nil).
| Parameter | Type | Description |
|---|---|---|
s |
string | JSON text. |
Returns: any|nil, string|nil
local v, err = json.decode('{"a":[1,2,null]}')
if not v then error(err) end
json.array
json.array(t?)
Marks t (and returns it) so it always encodes as a JSON array, even when empty. Keys other than 1..n make encoding fail.
| Parameter | Type | Description |
|---|---|---|
t (optional) |
table | Table to mark; a new empty table when omitted. |
Returns: table
Errors:
- t has a protected metatable
local rows = json.array({})
json.object
json.object(t?)
Marks t (and returns it) so it always encodes as a JSON object, including an empty table or one with integer keys. Overrides json.array.
| Parameter | Type | Description |
|---|---|---|
t (optional) |
table | Table to mark; a new empty table when omitted. |
Returns: table
Errors:
- t has a protected metatable
return json.encode(json.object({})) -- {}
log
Writes to the run log (the script console, and the job run log for jobs). Messages longer than 8 KiB are truncated with an ellipsis; fields are limited to about 8 KiB when serialized. The most recent 256 KiB of log output is kept per run.
Available in: ui_event, report, job.
log.debug
log.debug(msg, fields?)
Writes a debug entry to the run log.
| Parameter | Type | Description |
|---|---|---|
msg |
any | Message; converted with tostring. |
fields (optional) |
table | Structured context: a table with string keys. Values may be strings, numbers, booleans, decimals, json.null or nested tables. |
Errors:
- fields is not a table with string keys
- fields are too large
log.debug("imported rows", {count = 12, source = "upload"})
log.info
log.info(msg, fields?)
Writes a info entry to the run log.
| Parameter | Type | Description |
|---|---|---|
msg |
any | Message; converted with tostring. |
fields (optional) |
table | Structured context: a table with string keys. Values may be strings, numbers, booleans, decimals, json.null or nested tables. |
Errors:
- fields is not a table with string keys
- fields are too large
log.info("imported rows", {count = 12, source = "upload"})
log.warn
log.warn(msg, fields?)
Writes a warn entry to the run log.
| Parameter | Type | Description |
|---|---|---|
msg |
any | Message; converted with tostring. |
fields (optional) |
table | Structured context: a table with string keys. Values may be strings, numbers, booleans, decimals, json.null or nested tables. |
Errors:
- fields is not a table with string keys
- fields are too large
log.warn("imported rows", {count = 12, source = "upload"})
log.error
log.error(msg, fields?)
Writes a error entry to the run log.
| Parameter | Type | Description |
|---|---|---|
msg |
any | Message; converted with tostring. |
fields (optional) |
table | Structured context: a table with string keys. Values may be strings, numbers, booleans, decimals, json.null or nested tables. |
Errors:
- fields is not a table with string keys
- fields are too large
log.error("imported rows", {count = 12, source = "upload"})
notify
Show messages to the user.
Available in: ui_event.
notify.toast
notify.toast(text, {level?})
A brief message; level is "info" (default), "success", "warning" or "error".
notify.toast("Order saved", {level = "success"})
notify.alert
notify.alert(text, {title?})
A message the user must dismiss.
notify.alert("Stock is low", {title = "Heads up"})
record
The form's current record buffer. Read and assign fields by name (record.Qty = 5); assignments are sent back to the form and saved with the record.
Available in: ui_event.
id(number|nil): Row id (nil for a new record).version(number|nil): Row _version the form loaded.is_new(boolean): True for a record not yet saved.is_dirty(boolean): True when the user changed fields.
record.get
record.get(field) → value
Reads a field (db.NULL when empty).
local q = record.get("Qty")
record.set
record.set(field, value)
Assigns a field.
record.set("Status", "Approved")
record.changed_fields
record.changed_fields() → fields
Returns a list of fields assigned during this event.
local fields = record.changed_fields()
record.reset
record.reset(field?)
Discards changes to a field or the entire record buffer.
record.reset("Status")
record.to_table
record.to_table() → table
Returns a snapshot of all current record values as a plain Lua table.
local snapshot = record.to_table()
records
Read and change rows field by field, with the same validation, concurrency checks and history as the datasheet. Fields are named by their field (physical) names.
Available in: ui_event, report, job.
records.get
records.get(table, id) → row|nil
Reads one row by id.
Returns: row table (id, _version and every field; currency as decimal, yes/no as boolean, empty as db.NULL) or nil
local c = records.get("Customers", record.Customer)
records.find_one
records.find_one(table, where) → row|nil
Finds the first row matching the where filter.
Returns: first matching row table or nil
local u = records.find_one("Customers", {Email = "alice@example.com"})
records.count
records.count(table, where?) → number
Counts rows matching the where filter (or all rows if where is omitted).
Returns: integer count of matching rows
local n = records.count("Orders", {Status = "Open"})
records.exists
records.exists(table, where?) → boolean
Reports whether any rows match the where filter.
Returns: true if at least one matching row exists
if records.exists("Customers", {City = "Paris"}) then … end
records.insert
records.insert(table, values) → id
Creates a row and returns its id.
Available in: ui_event, job.
Errors:
- REQUIRED_FIELD
- VALIDATION_FAILED
- DUPLICATE_VALUE
- LINKED_RECORDS
local id = records.insert("Notes", {Text = "Called", Customer = record.id})
records.insert_many
records.insert_many(table, rows) → ids
Creates multiple rows inside a transaction and returns an array of their ids.
Available in: ui_event, job.
Errors:
- REQUIRED_FIELD
- VALIDATION_FAILED
- DUPLICATE_VALUE
- LINKED_RECORDS
local ids = records.insert_many("Items", {{Name = "A"}, {Name = "B"}})
records.update
records.update(table, id, values, {version?}) → version
Changes fields of a row. With version, fails if the row changed since that version; without it, applies to the current version.
Available in: ui_event, job.
Errors:
- CONCURRENT_MODIFICATION
- VALIDATION_FAILED
- NOT_FOUND
records.update("Orders", id, {Status = "Shipped"})
records.update_many
records.update_many(table, where, values) → {changes}
Updates every row matching where with the given field values.
Available in: ui_event, job.
Errors:
- VALIDATION_FAILED
- BAD_REQUEST
records.update_many("Orders", {Status = "Open"}, {Status = "Processing"})
records.upsert
records.upsert(table, match, values) → id
Finds a row matching match and updates it with values, or inserts a new row merging match and values.
Available in: ui_event, job.
Errors:
- VALIDATION_FAILED
- REQUIRED_FIELD
records.upsert("Counters", {Key = "views"}, {Count = 1})
records.delete
records.delete(table, id, {version?})
Deletes a row.
Available in: ui_event, job.
Errors:
- CONCURRENT_MODIFICATION
- LINKED_RECORDS
- NOT_FOUND
records.delete("Drafts", id)
reports
Runs reports from a job and saves the result as a run artifact.
Available in: job.
reports.generate
reports.generate(report, params?, options?) → {name, size, rows, format}
Runs the report as the job (with an editor's data rights) and saves a rendering as an artifact of this run, within the artifact quota. csv holds the detail rows with one leading column per grouping level; json mirrors the groups as a tree with their header and footer values; html is a self-contained table. Maps and images are not part of generated files. Saving the same name again replaces the file.
| Parameter | Type | Description |
|---|---|---|
report |
string | The report's id or name. |
params (optional) |
table | Values for the parameters of the report's saved-query source. |
options (optional) |
table | format: "csv" (default), "json" or "html". name: the artifact file name (default: the report's name with the format's extension). |
Returns: The saved artifact's name and size, the number of detail rows and the format.
Errors:
NOT_FOUND: no such reportBAD_REQUEST: unknown format, invalid name or a missing report parameterQUOTA_EXCEEDED: artifact storage full or too many artifacts in this run
local out = reports.generate("Monthly sales", {month = "2026-09"}, {format = "csv", name = "sales-2026-09.csv"})
staging
Scratch tables for bulk loads. staging.create returns a handle with these methods; the staging tables are private to the run and deleted when it ends (at most 8 per run, 1,000,000 rows and 256 MiB each).
handle:insert(row): adds one row, given as a table keyed by column name or as an array in column order. Values are text, numbers, booleans or db.NULL; missing columns are empty.
handle:query(sql, params?) → rows: runs one SELECT against the table, which is named data (columns are the names given to create, plus n, the insertion number). Use ? placeholders with an array or :name placeholders with a table. Writes, PRAGMA, ATTACH and any other table are refused (SQL_NOT_ALLOWED). Returns an array of row tables keyed by column name; SQL NULL is db.NULL. A result of more than 100,000 rows or 32 MiB is an error.
handle:count() → number: the number of rows inserted.
handle:merge_into(table_name, {mode, key, chunk, all_or_nothing}?) → {inserted, updated, skipped, error_count, errors}: copies every row into the table. Staging column names must match the table's field names (display or physical name, any case). mode is "append" (default), "upsert" (key = the name of a field with a unique index; existing rows with the same key are updated) or "replace" (delete all rows, then insert; one transaction up to 50,000 rows). Rows are converted exactly like row API values (text "12.50" for currency, ISO dates, yes/no words) and written in chunks of chunk rows (default 2,000, at most 10,000) so interactive saves are served between chunks; rows the database rejects are skipped and listed in errors (the first 100, each {row, column, code, message}). With all_or_nothing = true the whole merge is one transaction that rolls back on the first bad row and raises an error. merge_into cannot run inside db.transaction.
Available in: job.
staging.create
staging.create({columns = {...}}) → handle
Creates an empty staging table for this run.
| Parameter | Type | Description |
|---|---|---|
options |
table | columns: array of column names (letters, digits, underscores; at most 256; n is reserved). |
Returns: A staging handle with the methods insert, query, count and merge_into (see the module description).
Errors:
BAD_REQUEST: invalid or repeated column namesQUOTA_EXCEEDED: too many staging tables in this run
local st = staging.create({columns = {"sku", "qty"}})
st:insert({sku = "A-1", qty = 5})
st:insert({"B-2", 7})
local rows = st:query("SELECT sku FROM data WHERE qty > :min", {min = 5})
local res = st:merge_into("Stock", {mode = "upsert", key = "sku"})
log.info("merged", {inserted = res.inserted, updated = res.updated})
stats
Descriptive statistics, linear regression, time-series moving averages and distribution analysis. All functions accept either an array of numbers or (rows, field_name).
Available in: ui_event, report, job.
stats.sum
stats.sum(xs [, field]) → number|nil
Calculates the sum of numbers.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: sum of elements, or nil if empty
local total = stats.sum({10, 20, 30})
stats.mean
stats.mean(xs [, field]) → number|nil
Calculates the arithmetic mean.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: arithmetic mean (average), or nil if empty
local avg = stats.mean(orders, "Total")
stats.median
stats.median(xs [, field]) → number|nil
Calculates the median (50th percentile).
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: median value, or nil if empty
local med = stats.median({1, 3, 3, 6, 7, 8, 9})
stats.mode
stats.mode(xs [, field]) → number|nil
Finds the mode (most common value).
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: most frequent value, or nil if empty
local m = stats.mode({1, 2, 2, 3})
stats.min
stats.min(xs [, field]) → number|nil
Finds the minimum value.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: minimum value, or nil if empty
local low = stats.min(rates)
stats.max
stats.max(xs [, field]) → number|nil
Finds the maximum value.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: maximum value, or nil if empty
local high = stats.max(rates)
stats.range
stats.range(xs [, field]) → number|nil
Calculates the range (max - min).
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: difference between max and min, or nil if empty
local span = stats.range({10, 2, 8})
stats.variance
stats.variance(xs [, field]) → number|nil
Calculates the unbiased sample variance.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: sample variance (n-1 denominator), or nil if count < 2
local v = stats.variance({2, 4, 4, 4, 5, 5, 7, 9})
stats.pvariance
stats.pvariance(xs [, field]) → number|nil
Calculates the population variance.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: population variance (n denominator), or nil if empty
local pv = stats.pvariance({2, 4, 4, 4, 5, 5, 7, 9})
stats.stdev
stats.stdev(xs [, field]) → number|nil
Calculates the sample standard deviation.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: sample standard deviation, or nil if count < 2
local s = stats.stdev(sales, "Amount")
stats.pstdev
stats.pstdev(xs [, field]) → number|nil
Calculates the population standard deviation.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: population standard deviation, or nil if empty
local ps = stats.pstdev(populationScores)
stats.percentile
stats.percentile(xs, p [, field]) → number|nil
Calculates the p-th percentile using linear interpolation.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
p |
number | Percentile (0 to 100, or 0.0 to 1.0). |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: interpolated percentile value, or nil if empty
local p95 = stats.percentile(latencies, 95)
stats.quantiles
stats.quantiles(xs, probs [, field]) → table
Calculates multiple percentiles in a single pass.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
probs |
table | Array of probabilities (e.g. {0.25, 0.5, 0.75}). |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: array of quantile values
local q = stats.quantiles(scores, {0.25, 0.5, 0.75})
stats.iqr
stats.iqr(xs [, field]) → number|nil
Calculates the interquartile range.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: interquartile range (Q75 - Q25), or nil if empty
local spread = stats.iqr(prices)
stats.summary
stats.summary(xs [, field]) → table|nil
Returns a comprehensive summary table of descriptive statistics.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: {count, sum, mean, stdev, min, q25, median, q75, max, iqr} or nil if empty
local s = stats.summary(orders, "Total")
log.info("stats", {avg = s.mean, median = s.median, iqr = s.iqr})
stats.zscores
stats.zscores(xs [, field]) → table
Calculates the z-score for each value.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: array of standardized z-scores ((x - mean) / stdev)
local z = stats.zscores({10, 20, 30, 40, 50})
stats.correl
stats.correl(xs, ys) → number|nil
Calculates the Pearson product-moment correlation coefficient.
| Parameter | Type | Description |
|---|---|---|
xs |
table | First array of numbers. |
ys |
table | Second array of numbers. |
Returns: Pearson correlation coefficient r (-1.0 to 1.0), or nil if undefined
local r = stats.correl(adSpend, revenue)
stats.covariance
stats.covariance(xs, ys) → number|nil
Calculates the sample covariance between two variables.
| Parameter | Type | Description |
|---|---|---|
xs |
table | First array of numbers. |
ys |
table | Second array of numbers. |
Returns: sample covariance, or nil if count < 2
local cov = stats.covariance(xs, ys)
stats.linreg
stats.linreg(xs, ys) → table|nil
Calculates ordinary least-squares linear regression (y = slope * x + intercept).
| Parameter | Type | Description |
|---|---|---|
xs |
table | Independent variable values (x). |
ys |
table | Dependent variable values (y). |
Returns: {slope, intercept, r, r2} or nil if insufficient points
local fit = stats.linreg(x, y)
local forecast = fit.slope * nextX + fit.intercept
stats.moving_avg
stats.moving_avg(xs, window) → table
Calculates the simple moving average over a sliding window.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers. |
window |
number | Window size in elements. |
Returns: array of simple moving averages of length (#xs - window + 1)
local sma = stats.moving_avg(dailyRevenue, 7)
stats.ema
stats.ema(xs, alpha) → table
Calculates the exponential moving average (EMA).
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers. |
alpha |
number | Smoothing factor between 0.0 and 1.0. |
Returns: array of exponential moving averages of same length as xs
local ema = stats.ema(prices, 0.2)
stats.cumsum
stats.cumsum(xs) → table
Calculates the cumulative sum.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers. |
Returns: array of cumulative sums
local running = stats.cumsum({1, 2, 3, 4}) -- {1, 3, 6, 10}
stats.diffs
stats.diffs(xs) → table
Calculates discrete differences between consecutive elements.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers. |
Returns: array of differences x[i] - x[i-1]
local d = stats.diffs({10, 15, 12}) -- {5, -3}
stats.pct_change
stats.pct_change(xs) → table
Calculates percentage changes between consecutive elements.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers. |
Returns: array of percentage changes (x[i] - x[i-1]) / x[i-1]
local p = stats.pct_change({100, 110, 99}) -- {0.1, -0.1}
stats.histogram
stats.histogram(xs, bins [, field]) → table
Computes equal-width frequency bins across the dataset's range.
| Parameter | Type | Description |
|---|---|---|
xs |
table | Array of numbers or array of row tables. |
bins |
number | Number of equal-width bins (at least 1, at most 10000). |
field (optional) |
string | Field name if xs is an array of row tables. |
Returns: array of bin tables: { {min, max, count, ratio}, ... }
local bins = stats.histogram(ages, 5)
ui
Change the form: control properties, focus, navigation and confirmations.
Available in: ui_event.
ui.control
ui.control(name) → props
A snapshot of a control's properties (visible, enabled, locked, caption, value…).
if ui.control("Approve").visible then … end
ui.set
ui.set(name, props)
Changes control properties: visible, enabled, locked, required (booleans), caption, tooltip, error (text), tone ("default"|"warning"|"danger"|"success"), value (unbound controls).
ui.set("Approve", {visible = record.Total > 5000})
ui.focus
ui.focus(name)
Moves focus to a control.
ui.focus("Qty")
ui.goto_record
ui.goto_record(target)
Moves to a record: an id, or "next", "prev", "first", "last", "new".
ui.goto_record("new")
ui.open_form
ui.open_form(name, {where?, args?})
Opens another form, optionally filtered with a filter-model table and passing args.
ui.open_form("Customer", {args = {id = record.Customer}})
ui.open_report
ui.open_report(name, {params?})
Opens a report.
ui.open_report("Invoice", {params = {order_id = record.id}})
ui.open_url
ui.open_url(url, target?)
Opens a URL in the browser: an http, https or mailto link, or a path on this server (/...). target is a window name such as _blank (default) or _self.
ui.open_url("https://example.com/docs", "_blank")
ui.close
ui.close()
Closes the form.
ui.close()
ui.requery
ui.requery(control?)
Reloads the form's records, or one subform/combo box.
ui.requery("Lines")
ui.refresh
ui.refresh()
Alias for ui.requery(); reloads the form's record source.
ui.refresh()
ui.cancel
ui.cancel(message)
Cancels the pending action (save, insert, delete) in before_* events and shows message.
ui.cancel("Orders over 5,000 need an approver.")
ui.confirm
ui.confirm(message, continuation)
Asks the user to confirm. When answered, the form calls the handler function named continuation with event.answer set.
ui.confirm("Delete all lines?", "on_delete_lines")
ui.set_error
ui.set_error(control, message)
Sets an inline validation error message on a control.
ui.set_error("Email", "Invalid email address format.")
ui.clear_error
ui.clear_error(control)
Clears any inline validation error on a control.
ui.clear_error("Email")
ui.clear_errors
ui.clear_errors()
Clears inline validation errors from all controls on the form.
ui.clear_errors()
ui.set_visible
ui.set_visible(control, visible)
Shows or hides a control.
ui.set_visible("ShippingAddress", record.NeedsDelivery)
ui.set_enabled
ui.set_enabled(control, enabled)
Enables or disables a control.
ui.set_enabled("SubmitBtn", record.is_dirty)
ui.set_read_only
ui.set_read_only(control, read_only)
Locks or unlocks a control for editing.
ui.set_read_only("UnitPrice", not user.is_admin)
ui.set_locked
ui.set_locked(control, locked)
Locks or unlocks a control (alias for set_read_only).
ui.set_locked("Qty", true)
ui.set_required
ui.set_required(control, required)
Marks a control as required or optional.
ui.set_required("Reason", record.Status == "Rejected")
ui.set_label
ui.set_label(control, text)
Sets the caption or label text of a control.
ui.set_label("SaveBtn", "Update Record")
ui.set_caption
ui.set_caption(control, text)
Sets the caption text of a control (alias for set_label).
ui.set_caption("Title", "Edit Mode")
ui.set_value
ui.set_value(control, value)
Sets the value of an unbound control.
ui.set_value("SearchBox", "")
url
Pure URL parsing, building and percent-encoding helpers. Evaluated entirely in-memory with zero network or DNS lookups.
Available in: ui_event, report, job.
url.parse
url.parse(str) → table|nil, err
Parses a URL string into its components.
| Parameter | Type | Description |
|---|---|---|
str |
string | URL string to parse. |
Returns: table with scheme, host, hostname, port, path, query, raw_query, fragment (or nil, err)
local u = url.parse("https://api.example.com:8443/v1/items?tag=new#section")
log.info("parsed", {host = u.hostname, port = u.port, tag = u.query.tag})
url.build
url.build(components) → string
Constructs a URL string from component parts.
| Parameter | Type | Description |
|---|---|---|
components |
table | Table with scheme, host (or hostname, port), path, query or raw_query, fragment. |
Returns: reconstructed URL string
local s = url.build({scheme = "https", host = "api.example.com", path = "/v1/search", query = {q = "widgets"}})
url.encode
url.encode(str) → string
Percent-encodes a string according to RFC 3986.
| Parameter | Type | Description |
|---|---|---|
str |
string | Text to percent-encode for query/path. |
Returns: percent-encoded string
local enc = url.encode("hello world & goodbye")
url.decode
url.decode(str) → string|nil, err
Decodes a percent-encoded string.
| Parameter | Type | Description |
|---|---|---|
str |
string | Percent-encoded string to decode. |
Returns: decoded string (or nil, error on malformed escapes)
local dec = url.decode("hello%20world")
url.query_encode
url.query_encode(params) → string
Encodes a table of parameters into a URL query string, sorted stably by key.
| Parameter | Type | Description |
|---|---|---|
params |
table | Table mapping query parameter keys to scalar values or arrays. |
Returns: encoded query string (without leading '?')
local qs = url.query_encode({page = 1, sort = "desc", tags = {"alpha", "beta"}})
url.query_decode
url.query_decode(str) → table
Decodes a URL query string into a Lua table.
| Parameter | Type | Description |
|---|---|---|
str |
string | Query string with or without leading '?'. |
Returns: table of query parameters (lists for multi-value keys)
local params = url.query_decode("page=1&sort=desc")
workspace
Information about the workspace and the user the script runs as.
Available in: ui_event, report, job.
timezone(string): The workspace's IANA time zone.user(table): {id, email, display_name, role} of the user (jobs run as the job principal).