Skip to content

Scripting overview

Scripts are small programs that live inside the launcher. They can fetch things from the web, print lines into the log, post notifications, pin a status line in the header, or become new terminal commands. Write them in Lua 5.4 or JavaScript (QuickJS). Both languages get the same API.

Terminal window
script new hn # hn.lua from an RSS template, opens the editor
script new feed.js # JavaScript instead (or: script new feed js)
script new weather --cmd # a command template that takes arguments

Or tap New script in the script manager (scripts). Then:

Terminal window
script run hn # run it now
script every hn 30m # and every 30 minutes

The fastest start is the script library: script lib lists 15 ready-made scripts and script install weather installs one with sensible timers.

Lua 5.4 JavaScript (QuickJS)
Network calls block (fine, scripts never run on the UI thread) async: await http.get(…); Promise.all works
JSON json.encode / json.decode built-in JSON
Print log(…) or print(…) log(…), console.log, console.error
Arguments args (table), argline (string) args (array), argline (string)
Top level return value prints it return value prints it; top-level await works

Pick whichever you like; the bundled library has both.

A script runs:

  • on demand: script run <name>, the Run button, or Ctrl+R in the editor,
  • on an interval: script every <name> 30m (15m, 2h, 1d, a number of minutes, or off),
  • at daily times: script at <name> 08:00,18:30 (or off),
  • as a command, if it is marked @command,
  • on an event new, if it declares @on.

Intervals have Android’s 15-minute minimum. An interval and daily times can be combined. Setting a timer turns a disabled script back on; script off <name> disables it without losing the timers.

script ls shows every script with its timers, whether it is a command, notification state and the last run’s result. The script manager shows the same: new one line per script (how it runs and when it last ran, red if that failed) with its on/off switch; tap a script for Run, Edit, Export, notifications, timers and Delete.

The script’s return value is printed into the log (tables and objects as JSON). Each run gets a ── name · 22:40 ── header and the script’s own colour. When a script fails, the error line is tappable and opens the editor at the failing line.

Terminal window
lua return 6 * 7
js [1, 2, 3].map(x => x * 2)
= 2^10

lua and js evaluate a snippet and print its value. The Lua REPL tries return <code> first, so expressions work without return.

notify(title, body, ctx) only posts when notifications are on for that script:

Terminal window
script notify hn on

With notifications off, notify does nothing visible, but the script’s log lines still show the content. If Android blocks Kolahal’s notifications, a “tap to allow” line appears (once a day). See Notifications.

status(text) pins one line into the header for this script, like weather ▸ Pune: ☀️ +27°C. status(nil) (or null in JS) clears it. Only the first line is used, up to 80 characters.

Terminal window
status # list status lines
status clear weather # remove one (or: status clear all)

Tapping a status line runs its script again. header.status off hides them all.

Add @command in a comment and the script becomes a terminal command named after the script. @desc gives it a one-line description in help and in suggestions. new @usage lists its arguments, written like the usage lines in help: [a|b] is a choice, <city> a value you type. The words in it are suggested as you type (xkcd offers xkcd random), and help shows the line.

-- weather.lua
-- @command
-- @desc current weather: weather [city]
-- @usage [<city>]
local city = argline ~= "" and argline or "London"
return (http.get("https://wttr.in/" .. city .. "?format=3").body:gsub("%s+$", ""))
Terminal window
weather paris

In JavaScript use // @command and // @desc …. Arguments arrive in args and argline. A command script run from the prompt may also call launcher.run. When the same script runs on a timer, it gets no arguments, which is how most library scripts tell the two cases apart.

A command that takes no arguments runs as soon as you have typed enough of its name, like the built-in ones, when launch.instant is all. That’s a script with an empty @usage line, or one that never reads args or argline. A script that reads them but has no @usage line always waits for Enter.

new A script can also run when something happens on the device, by declaring one or more @on directives:

-- @on unlock
-- @on charger

The supported events are unlock, charger, unplug, wifi, offline, notification, boot and headset. During an event run, the script gets an event object describing what happened (at least its type). Check the in-editor docs for the exact fields of each event.

-- @on charger
-- @on unplug
local b = device.battery()
status((b.charging and "charging " or "on battery ") .. tostring(b.level) .. "%")

Scripts run in a separate :script process, so a crashing engine can’t take down the home screen.

  • 60 seconds per run, and a 64 MB memory cap. new In Lua, string.rep refuses results over 16 MB, and a run whose memory grows past 256 MB between checks is stopped (the script process is restarted).
  • At most 500 log lines per run; past that one line says the rest weren’t logged.
  • Lua: a fresh state per run. java, io, package, require, debug, dofile, loadfile and process-level os functions are removed (os.time, os.date, os.clock, os.difftime remain), and only text chunks can be loaded. The timeout and memory cap are enforced by an instruction-count hook that also covers coroutines, and pcall, xpcall and coroutine.resume can’t swallow them. A watchdog kills the script process if a C function outlives the deadline.
  • JavaScript: QuickJS with only the bindings in the API, a 64 MB memory limit and an evaluation timeout. There is no file, process or network access besides the API.
  • sleep(ms) is capped at 5000 ms per call.
  • restart scripts stops the script process if something is stuck. Scheduled scripts start it again.

new Your notifications, the clipboard, the list of your apps and your local network are only for scripts that ask, with a @uses line at the top:

-- @uses notifications, clipboard
@uses allows
notifications notifications.recent and the @on notification trigger
clipboard clipboard.get and clipboard.set
apps apps.list
lan http requests to this phone or your local network (192.168.…, router pages, a home server)

Without it, those calls stop the script with an error saying what to add. new On Android 17 and newer, reaching your local network also needs Android’s local network permission: run local-network once, or tap the line a blocked request leaves in the log. Code you type at the prompt (lua, js, =) may use everything. Scripts that come from outside (a file, a backup) arrive switched off, and script on <name> says what the script uses as you turn it on.

Links a script puts on a line (url, image, or a notification’s link) must be web links: anything else (tel:, upi:, an app’s own link) is dropped. A line’s copy must be what the line shows, part of it, or its link.

Scripts are part of the backup file, and can be imported and exported as single .lua / .js files or synced with a folder (script dir, script export, script import).