evolve
lua 5.4 · reloads on save

Lua API

These are the main calls, and each one works in the current evolve version.

91/ 91 members

start

1

A script is a .lua file in the scripts folder: in the game menu, Settings › Scripts › «Open folder» opens it. It returns a table of callbacks and re-reads on save. Lua 5.4, one shared VM for every script, callbacks run one at a time.

Ground rules
  1. 1Draw in on_draw, play in on_update. renderer.* outside the frame draws nothing, and orders.* inside on_draw leave by the wrong path.
  2. 2An entity from entities.* is good for one frame: check e:valid() first, and take the list itself from entities.* again.
  3. 3The schema walk takes the first ~10 seconds after launch. Position, team and health read instantly; armor, abilities and gamerules answer 0 until it finishes.
  4. 4While client.local_hero() is empty every team filter reads as enemy — get the hero first, target second.
  5. 5Coordinates are Source world units: the map runs about -8288..8288 on X and Y, and a point is vec(x, y, z).
aim.luareturn { … }hud.luareturn { … }farm.luareturn { … }one VM · Lua 5.4aim.on_updatehud.on_updatefarm.on_update↻ saved, reloaded
A script is a .lua file that returns a table of callbacks. All scripts share one VM and their callbacks run in turn. Save the file and it reloads.
client.local_hero()→ Entity|nil

The local player's hero. nil until you are in a match.

client.screen_size()→ number, number

Window width and height in pixels.

client.cursor_world()→ Vector|nil

The point under the cursor on the hero's z-plane.

globals.curtime()→ number

Game time in seconds. The clock for any timer.

globals.tickcount()→ number

The client's real 30 Hz tick, monotonic.

Watch outKeeps counting while game time is paused.

vec(x, y, z)→ Vector

A point or a direction. vec2, angle and color(r, g, b, a) sit next to it — components are 0..255.

log.info(msg)

A line in evolve.log. The first tool when a script goes quiet.

Example
-- scripts/hello.lua
local s = {}

function s.on_update()                  -- game logic
  local me = client.local_hero()
  if not (me and me:valid()) then return end
end

function s.on_draw()                    -- renderer.* only
  local me = client.local_hero()
  if not (me and me:valid()) then return end
  local x, y, on = renderer.world_to_screen(me:position())
  if on then
    renderer.text(font_default, x, y - 40, color(255, 255, 255, 255), "hello")
  end
end

return s

packs

2

A pack is a folder: pack.json, an entry, modules, images and data. The same folder runs dropped into scripts/, and it is what you upload to the store as a zip (Author dashboard › Versions).

Ground rules
  1. 1pack.json: version is required (like 1.2.0), entry is the file that starts it (main.lua by default), name is its name in the script list, evolve is the build it was written for.
  2. 2The folder's name is the pack's name. Modules load from it: require("my_pack.lib.util") reads lib/util.lua. In the store the folder is named after the listing's slug.
  3. 3A path starting with ./ is your pack's folder: renderer.load_image("./art/icon.png"), menu icons "./art/…", fs.read("./data/x.json"). You never need the folder's name.
  4. 4A pack may hold .lua, images .png .jpg .bmp .tga .gif (up to 4096 px a side) and data .json .txt .md .csv. The zip is up to 16 MB, up to 1500 files.
  5. 5On upload every .lua is compiled and every image and json is opened. The answer lists every problem at once, with the file and the line, and an admin reads the version after that.
my_pack/pack.jsonmain.lualib/util.luaart/icon.pngdata/x.jsonscripts/zip → store
A pack is one folder: dropped into scripts/ it runs as is, and the same folder zipped is what you upload to the store. pack.json needs a version; a ./ path points into the pack folder.
renderer.load_image(path)→ number

An image by path: "./art/x.png" from your own pack, "panorama/images/…" from the game. Returns a handle or -1; cached by path.

Watch outLoad once when the script loads, not every frame. After a script reload a changed file is picked up on its own.

renderer.load_image_bytes(bytes [, key])→ number

An image from bytes the script already holds (fs.read, http). Cached by key, or by the content without one.

Example
-- scripts/my_pack/pack.json : { "version": "1.0.0", "name": "My Pack" }
-- scripts/my_pack/main.lua
local util = require("my_pack.lib.util")         -- scripts/my_pack/lib/util.lua
local icon = renderer.load_image("./art/icon.png")
local cfg = json.decode(fs.read("./data/defaults.json") or "{}")

local s = {}
function s.on_draw()
  local me = client.local_hero()
  if not (me and me:valid()) or icon < 0 then return end
  local x, y, on = renderer.world_to_screen(me:position())
  if on then renderer.image(icon, x - 16, y - 80, 32, 32) end
end
return s

events

3

Callbacks. Anything arriving from an engine thread is buffered and drained by the frame pump, so a handler runs alongside on_update and may call the whole API.

Ground rules
  1. 1Two ways to subscribe: return a { on_update = fn } table from the file, or call events.register. Both live until the script reloads.
  2. 2Event payloads are valid only inside the callback — read them now, don't stash the table for later.
  3. 3A handler that throws is detached with a traceback; diag.errors() shows it inside the client.
on_pre_updateon_updateframe_beginon_drawmenu::renderdrainpoll_keysbuffer
One turn is one frame; callbacks run in this order. An event waits in the buffer and arrives on the next drain.
events.register(name, fn)

Subscribe to a callback.

namestring
The callback name, e.g. "on_update".
fnfunction
The handler. Its arguments depend on the event.
"on_update" fn()

A logic tick, every frame. Where orders.* and all game logic belong.

"on_draw" fn()

The frame. renderer.* works here and only here.

"on_game_event" fn(name, data)

Any native event: there is no per-name subscription, everything arrives.

namestring
The engine event name, e.g. "dota_player_gained_level".
datatable
Every field of the event by name, snapshotted when it fired.

Watch outDelivered by the frame pump, at most one frame after it fired, in fire order.

"on_key" fn(vk, down)

A raw key from wndproc.

vknumber
The Windows virtual-key code.
downboolean
Press or release.

Watch outThe return value is ignored: a script cannot swallow a key from the game.

"on_projectile" fn(p)

A tracking projectile was created.

ptable
handle, source, target, ability, move_speed, is_attack, dodgeable, expire_time, max_impact_time.

Watch outThe entity fields resolve at delivery: anything that died inside that frame arrives as nil.

"on_modifier_gained" fn(npc, mod)

A modifier appeared. Its pair is on_modifier_lost(npc, name).

npcEntity
Who got it.
modtable
name, stacks, duration and the rest of the modifier fields.
"on_prepare_unit_orders" fn(o)

An order is leaving for the server. Return false to block it; o.synthetic separates a script's order from a human click.

otable
order, target, position, ability, unit, queue, issuer, synthetic.

Watch outThe only callback still run inline on the game thread. The engine waits 2 ms for the VM and, failing that, lets the order through unvetoed — count those in events.veto_timeouts().

timers.after(sec, fn)→ number

A one-shot timer, returns its id. timers.every(sec, fn) and timers.clear(id) sit next to it.

secnumber
How many seconds of game time from now.
fnfunction
What to run.
events.dropped()→ number

How many events were lost to a full queue. Should stay 0.

Example
local s = {}

function s.on_projectile(p)
  local me = client.local_hero()
  if not (me and me:valid()) then return end
  if p.target == me and p.dodgeable then
    log.info("incoming, impact in " ..
      string.format("%.2f", p.max_impact_time - globals.curtime()))
  end
end

return s

entities

4

The entity lists. The snapshot refreshes about 20 times a second and userdata is interned, so an entity works as a table key.

Ground rules
  1. 1A filter is a plain table: { team = "enemy", alive = true, around = pos, radius = 600, class = "creep" }. class matches a substring of the class name.
  2. 2team is the string "enemy" or "ally", not a number.
  3. 3About half the live neutrals on the map are a camp's pre-spawned next wave: invulnerable, and filtered out with e:is_waiting_to_spawn().
The filter is a plain table; team is the string "enemy" or "ally". The list is a snapshot of the call: a creep that walks in later shows up in the next query.
entities.all()→ Entity[]

Every entity in the client snapshot.

entities.heroes(team)→ Entity[]

Heroes, optionally from one side only.

teamstring?
"enemy", "ally", or nothing for all of them.
entities.npcs(filter)→ Entity[]

Units by filter: creeps, illusions, summons, towers.

filtertable?
team / alive / around / radius / class.
entities.in_radius(pos, radius, filter)→ Entity[]

Everything standing inside the circle.

posVector
The centre of the circle.
radiusnumber
Radius in world units.
filtertable?
The same filter npcs takes.
entities.closest(pos, filter)→ Entity|nil

The nearest unit, excluding yourself.

posVector
Where to measure from.
filtertable?
The same filter npcs takes.
entities.by_class(name)→ Entity[]

Exact class identity plus everything derived from it — not a name guess.

namestring
The C++ class, e.g. "C_DOTA_BaseNPC_Tower".
entities.trees()→ Entity[]

Trees. entities.runes() and entities.physical_items() — items lying on the ground — sit next to it.

entities.get(index)→ Entity|nil

An entity by index; entities.from_handle(h) takes a handle.

indexnumber
The entity index.
Example
local s = {}

function s.on_draw()
  local me = client.local_hero()
  if not (me and me:valid()) then return end

  for _, h in ipairs(entities.heroes("enemy")) do
    if h:valid() and h:is_alive() and not h:is_dormant() then
      renderer.circle_3d(h:position(), h:attack_range(), color(255, 80, 80, 120))
    end
  end
end

return s

entity

5

The entity object. One type for a hero, a creep, an ability and an item — which methods mean anything depends on what it is.

Ground rules
  1. 1e:valid() comes first. An entity that is gone answers with defaults, not an error.
  2. 2An entity in fog (e:is_dormant()) is frozen at its last seen state: position, health and inventory are stale.
  3. 3Key a table by e:handle() — an index gets reused, a handle carries a serial too.
e:valid()→ boolean

The entity is still in the game.

e:position()→ Vector

World position. e:origin() is the same call.

e:health()→ number

Current health. max_health(), health_pct() (0..100), mana() and mana_pct() sit next to it.

e:is_alive()→ boolean

Alive. is_enemy(), is_ally(), is_illusion(), is_hero() and is_dormant() sit next to it.

e:distance(target)→ number

Distance in world units.

targetVector|Entity
A point or another entity.
e:ideal_speed()→ number

Exact current move speed with every modifier folded in: boots, haste, slows, auras.

Watch oute:move_speed() is the networked base stat with no modifiers. Movement wants ideal_speed.

e:effective_bat()→ number

Base attack time with modifiers folded in — what an attack timeline is built from. e:aps() and e:seconds_per_attack() sit next to it.

e:is_stunned()→ boolean

A unit state bit. is_silenced, is_rooted, is_hexed, is_disarmed, is_magic_immune and is_invulnerable read the same way.

Watch outThe bits answer yes or no and carry no clock. How long is left comes from the modifiers, not from the bit.

e:modifiers()→ table[]

Every modifier: name, class, stacks, duration, creation_time, die_time, ability, caster, parent, is_aura.

Watch outparent is the buffed unit, and for a debuff that is not the caster.

e:has_modifier(name)→ boolean

A quick check for one modifier; e:get_modifier(name) hands back its table.

namestring
The internal name, e.g. "modifier_bloodseeker_thirst".
e:handle()→ number

The full 32-bit handle: index plus serial. A safe key across frames.

e:visible_to_enemy()→ boolean

Whether the enemy sees you. Reliable for your own units.

abilities

6

Abilities and items are entities too, with their own methods: cooldown, mana, range, cast point and the KV data behind them.

Ground rules
  1. 1Cooldown, mana and cast phase replicate to the owner only. On someone else's hero an ability always looks ready — that is "we were not told", not "off cooldown".
  2. 2e:get_ability(name) hands back an invalid entity on a miss, not nil, and scans the snapshot on every call. Resolve once and keep it in a local.
  3. 3Whether a spell goes through BKB is ab:immunity_type() == 1, not a guess from the name.
Cooldown, mana and the cast phase reach only the owner: on an enemy hero ab:is_ready() is always true. Resolve an ability once with get_ability and keep it in a local.
hero:get_ability(name)→ Entity

An ability by name.

namestring
The internal name, e.g. "lion_impale".
hero:abilities()→ Entity[]

Every ability on the hero, in slot order.

Watch outScans the snapshot on every call — cache it instead of calling it inside a draw loop.

hero:get_item(slot)→ Entity

The item in a slot; hero:items() gives the whole inventory.

slotnumber
Inventory slot, counted from zero: 0..5 is what the hero carries, backpack and stash follow.
ab:cooldown()→ number

Remaining cooldown in seconds, 0 means ready.

ab:is_ready()→ boolean

Level above zero and cooldown expired.

ab:mana_cost()→ number

Cost at the current level; ab:ability_level() gives the level itself.

ab:cast_range()→ number

Cast range with every bonus folded in.

ab:cast_point()→ number

Cast point in seconds — the delay between the order and the effect.

ab:special(key, level)→ number

Any number out of the ability data, with talents and aghanim's folded in.

keystring
A key from AbilityValues, e.g. "duration" on lion_impale. The keys come from the ability data, not from a guess.
levelnumber?
The level; -1 or nothing means the current one.

Watch outOn an unleveled ability the current level answers 0 — pass the level explicitly: ab:special("duration", 0).

ab:immunity_type()→ number|nil

0 none, 1 pierces on enemies, 2 does not, 3 and 4 are the same for allies.

Watch outnil means "could not read it", which is not the same as 0.

ab:dispellable_type()→ number|nil

0 none, 1 strong dispel, 2 basic, 3 no.

it:item_charges()→ number

Item charges: bottle, wand, urn. Ability charges are ab:charges() instead.

Example
local s = {}
local hex

function s.on_update()
  local me = client.local_hero()
  if not (me and me:valid()) then return end

  hex = hex or me:get_ability("lion_voodoo")
  if not (hex and hex:valid() and hex:is_ready()) then return end
  if me:mana() < hex:mana_cost() then return end

  for _, h in ipairs(entities.heroes("enemy")) do
    if h:is_alive() and not h:is_magic_immune()
       and me:distance(h) < hex:cast_range() then
      orders.cast_target(hex, h)
      return
    end
  end
end

return s

orders

7

Real engine orders, down the same path a mouse click takes. Send them from the frame, inside on_update.

Ground rules
  1. 1Every order takes an optional trailing queue argument: orders.move_to(pos, true) queues like a shift-click.
  2. 2An order from another thread — a GC callback, a net hook, your own thread — must be buffered and leave from on_update. Calling the engine off-thread deadlocks the client on a mutex.
  3. 3An order every frame is not faster, it is spam: the server acts on the last one, and the order stream is visible in a replay.
An order reaches the game like a player's click: move_to sends the hero to a point, attack to a target. Issue orders from on_update.
orders.move_to(pos, queue)

A move order. attack_move(pos) and move_to_target(entity) sit next to it.

posVector
Where to walk.
queueboolean?
Queue instead of replacing.
orders.attack(target, queue)

Attack a target.

targetEntity
What to attack.
queueboolean?
Queue it.
orders.cast_target(ability, target, queue)

Cast at a unit.

abilityEntity
The ability or item.
targetEntity
The cast target.
queueboolean?
Queue it.
orders.cast_position(ability, pos, queue)

Cast at a position.

abilityEntity
The ability or item.
posVector
The cast point.
queueboolean?
Queue it.
orders.cast_no_target(ability, queue)

A no-target cast; cast_toggle(ability) flips a toggle.

abilityEntity
The ability or item.
queueboolean?
Queue it.
orders.use_item(item, target)

Use an item.

itemEntity
An item from the inventory.
targetEntity|Vector?
The target or point, if the item takes one.
orders.stop()

Stop; orders.hold() holds position.

orders.unit_move_to(unit, pos, queue)

An order for a controlled unit rather than your hero; unit_attack works the same.

unitEntity
Who acts: a summon, the courier, a second meepo.
posVector
Where to walk.
queueboolean?
Queue it.
orders.raw(type, opts)

The full dotaUnitOrder_t set, for anything the named calls do not cover.

typenumber
The order code from orders.types.
optstable
target, position, ability, unit, queue, issuer, show_effects.

renderer

8

An immediate d3d11 renderer. Anything drawn in on_draw lives one frame; a world primitive can instead be registered once and reprojected by C++.

Ground rules
  1. 1Colour is color(r, g, b, a), 0..255. Text and measurement take the font first; the global font_default is already there.
  2. 2world_to_screen returns three values, and the third is "the point is on screen at all". Skip it and things behind you draw mirrored.
  3. 3Pre-warm a font: metrics taken before the atlas rebuild hand back the 47px fallback, and a layout sized from them will be wrong.
game frameon_draw · renderer.*menu::render
A frame is built in layers: the game, then everything the script drew in on_draw, then the menu. renderer.* draws only in on_draw.
renderer.world_to_screen(pos)→ number, number, boolean

The projection: x, y and whether it is on screen.

posVector
A world point.
renderer.text(font, x, y, col, str, align, shadow)

Text on screen; renderer.measure_text(font, str) gives its width and height.

fontFont
The font; the global font_default will do.
x, ynumber
Screen coordinates.
colColor
The colour.
strstring
The text.
alignstring?
A combination of "c" centre, "r" right, "v" vertical.
shadowboolean?
A shadow behind the text.
renderer.load_font(name, size, weight, flags)→ Font

A font, cached by its parameters — calling it again is safe.

namestring
A system font name.
sizenumber
Size in pixels.
weightnumber?
Weight.
flagsnumber?
Style flags.
renderer.line(x1, y1, x2, y2, col, thick)

A line. rect, rect_outline, rounded_rect, triangle, poly and gradient sit next to it.

renderer.circle_filled(x, y, r, col, segs)

A filled screen circle; renderer.circle draws the outline.

renderer.circle_3d(center, radius, col, segs, thick)

A ring on the ground — a range indicator like the game's own.

centerVector
The centre in the world.
radiusnumber
Radius in world units.
renderer.load_image(path)→ Image

An image; renderer.image(handle, x, y, w, h, tint) draws it.

pathstring
A file on disk or a panorama resource from the game itself.
renderer.world_circle(id, center, radius, col, opts)

A world shape C++ then carries: register it on the tick, it is reprojected every frame. world_line, world_poly, world_text, world_image and world_clear sit next to it.

idstring
The shape's id: the same string replaces the previous one.
centerVector
The centre.
radiusnumber
Radius in world units.
colColor
The colour.
optstable?
ttl (0.25 s by default), thickness, segments, filled, layer.

Watch outRegistering is not drawing: call it from on_update, and it lands under whatever on_draw draws.

Example
local s = {}
local font = renderer.load_font("Verdana", 13)

function s.on_draw()
  local me = client.local_hero()
  if not (me and me:valid()) then return end

  for _, h in ipairs(entities.heroes("enemy")) do
    if h:is_alive() then
      local x, y, on = renderer.world_to_screen(h:position())
      if on then
        local w = renderer.measure_text(font, h:health() .. "")
        renderer.text(font, x - w / 2, y - 60, color(255, 90, 90, 255), h:health() .. "")
      end
    end
  end
end

return s

ui

9

The menu. Controls are declared once at file scope, survive a hot reload, and give their value through :get().

Ground rules
  1. 1The first argument is "tab.group", split on the first dot; with no dot the control lands in the group named general.
  2. 2A setting's key is "tab.group.label". Rename the label and every value saved under it is gone; for cosmetics there is c:caption().
  3. 3For combo and listbox :get() answers a zero-based index: options[c:get() + 1].
scripts.demoenabledrangemode▾lowmidhighon:get()→falsetruetruerange:get()→300600900mode:get()→012modes[mode:get() + 1]→"low""mid""high"
Declare a control once at file level and read it with :get(). A combo index starts at zero, hence options[c:get() + 1].
ui.checkbox(group, label, default)→ Control

A checkbox.

groupstring
"tab.group".
labelstring
The label, which is also part of the setting key.
defaultboolean
The value on a first run.
ui.slider_int(group, label, min, max, default, opts)→ Control

An integer slider; ui.slider_float adds a decimals argument.

min, maxnumber
The bounds.
optstable?
{ unit = " ms", min_text = "Off", max_text = "Max" }.
ui.combo(group, label, items, default_index)→ Control

A dropdown; ui.multiselect and ui.listbox work the same way.

itemsstring[]
The options.
default_indexnumber
A zero-based index.
ui.hotkey(group, label, key, mode)→ Control

A key binding. :get() answers "held right now", :key() gives the code bound to it.

keynumber
The default virtual-key code.
modestring?
"hold", "toggle", "always" or "off".
ui.button(group, label, fn)→ Control

A button. ui.color_picker, ui.textbox, ui.label and ui.separator sit next to it.

c:get()

The current value; c:set(v) writes it.

c:on_change(fn)→ Control

React to a change. Returns the control, so calls chain.

fnfunction
Called with no arguments — read the value with :get().
c:depend(other, expected, mode)→ Control

Tie a control to another one.

otherControl
The parent control.
expectednumber?
The value that keeps this one live, 1 by default.
modestring?
"grey" shows it greyed, "hide" takes it off the page.
ui.menu_open()→ boolean

Whether the menu is open — what you gate your own overlay on.

Example
local enabled = ui.checkbox("Draw.ESP", "Enemy circles", true)
local radius = ui.slider_int("Draw.ESP", "Radius", 100, 900, 400, { unit = " u" })

radius:depend(enabled)

local s = {}

function s.on_draw()
  if not enabled:get() then return end
  for _, h in ipairs(entities.heroes("enemy")) do
    if h:is_alive() then
      renderer.circle_3d(h:position(), radius:get(), color(255, 80, 80, 120))
    end
  end
end

return s

ml

10

In-process ONNX, synchronous or on a worker thread. A model is an ordinary file next to the script; the core knows nothing about it.

Ground rules
  1. 1onnxruntime.dll has to sit next to evolve.dll. ml.available() says whether it does, ml.gpu_available() whether DirectML is there.
  2. 2net:run is synchronous and costs 0.2-0.5 ms. Anything heavier goes to net:submit and comes back from net:poll a frame later.
  3. 3history hands back raw world values. Normalising them is the script's job, not the buffer's.
history.tensor(e, 6)[6, 11] · 20 Hznet:run(t)0.2-0.5 msnet:submit(t)poll · +1 frame
net:run is synchronous and costs 0.2-0.5 ms; a heavier model goes to net:submit and comes back from net:poll on the next frame. history hands back raw world values, the script normalises them.
ml.load(path, provider)→ Model|nil

A model, cached by path and provider. nil on failure, with the reason in ml.last_error().

pathstring
The .onnx file.
providerstring?
"cpu" by default, "gpu" or "auto".
net:run(input)→ Tensor|nil

A synchronous run.

inputTensor|Tensor[]
One tensor, or a list matching the input count.
net:submit(input)

An async run on a worker thread; net:poll() takes the result, net:busy() says it is still running.

ml.tensor(rows, cols)→ Tensor

A zeroed tensor. Indices are 1-based: t:set(r, c, v), t:get(r, c), t:table().

rowsnumber
Row count.
colsnumber?
Column count; without it you get a vector.
history.tensor(entity, frames)→ Tensor|nil

A [frames, 11] motion buffer, zero-padded at the front. The channels are listed in history.CH.

entityEntity
Who to read.
framesnumber
How many frames of history, oldest first.
history.set_rate(hz)

The rate of the shared buffer. history.track_all(true) widens it from heroes to every unit, which costs noticeably more.

hznumber
The sampling rate, 20 by default.

data

11

Files, settings, the log and JSON. What a script needs to survive a client restart.

Ground rules
  1. 1Keep your own files in fs.datadir() and find a package's siblings from fs.scriptdir(). Don't recover a path from the chunk name in an error: Lua truncates it at 60 characters.
  2. 2For anything written all game long there is fs.append. A read-modify-write loop through fs.write is quadratic in the file size and loses everything if it dies mid-write.
clientfs.append(path, s)fs.read(path)→ 3 lines↻ restartdata/run.log{"t":72,"lh":1}{"t":95,"lh":2}{"t":131,"lh":3}
Anything written all game long goes through fs.append into fs.datadir(): the lines outlive a client restart. A read-modify-write loop through fs.write is quadratic in the file size and loses everything if it dies mid-write.
log.info(msg)

A line in evolve.log; log.warn and log.error sit next to it.

fs.read(name)→ string|nil

Read a whole file; fs.exists and fs.listdir sit next to it.

namestring
The file path.
fs.append(name, str)

Append, creating the parent directory. How to keep a log across a match.

fs.datadir()→ string

The client's data/ directory, already created. fs.scriptdir() gives the absolute path to scripts/.

config.write_int(key, value)

The script's typed store: read_int, read_float, read_bool, read_string and their write pairs, plus config.has and config.delete.

keystring
Your own key, unrelated to the menu.
valuenumber
The value.
config.get(path)

A menu control's value by path; config.set(path, v) writes it.

pathstring
"tab.group.label" — the same key config.export() uses.

Watch outIt raises when the path names nothing, deliberately: otherwise a typo in the path silently looks like a setting that is off.

json.encode(value)→ string

A table to a string; json.decode(str) goes back.

diag.errors()→ table[]

Script errors, folded to one row per distinct failure: event, script, msg, traceback, count. diag.profile(), with the time each handler costs, sits next to it.