Automation Format Reference
Automations are .lua files in /sdcard/atak/custos/automations/. Each file declares one automation via comment annotations and a top-level run(ctx) function.
Automations never route to the LLM directly. run(ctx) executes deterministically as Lua in a sandboxed state. If LLM reasoning is needed, the body must explicitly call tools.call("delegate", {agent_name = "...", task = "..."}).
Anatomy
--- Human-readable title
-- @automation <unique_name>
-- @trigger <cron | duration | one-shot | broadcast-action>
-- @session MAIN | ISOLATED
-- @description <longer description>
function run(ctx)
-- Lua code invoked when the trigger fires.
-- Use tools.call("skill_name", {args}) to invoke skills.
end
Annotations
| Annotation | Required | Type | Meaning |
|---|---|---|---|
@automation |
yes | string | Unique name |
@trigger |
yes | string | Cron / duration / one-shot / broadcast action — type is auto-detected |
@session |
no | enum | MAIN (shares operator chat) or ISOLATED (default) |
@description |
no | string | Human-readable description |
Unsupported annotations (files using these are rejected at load time with a pointer to the correct shape):
@type— not used; the trigger type is auto-detected from the@triggerexpression shape@prompt— not a concept here; usetools.call("delegate", ...)insiderun()for LLM reasoning@filter— not a concept here; put filter logic as a guard clause at the top ofrun()@cooldown— not a concept here; track debounce state inrun()via the memory service if needed
The run(ctx) function
Every file must define function run(...) at module scope. The parameter name is convention matching the trigger type:
| Trigger shape | Convention | ctx shape |
|---|---|---|
| Cron / one-shot | function run(schedule) |
{ fired_at, cron } |
| Duration | function run(interval) |
{ fired_at, interval } |
| Broadcast action | function run(event) |
{ fired_at, action, cot_type, uid, lat, lon, callsign, hae, how, cot_xml, extra_* } |
fired_at is epoch milliseconds.
Trigger auto-detection
| Shape | Classified as |
|---|---|
<n><unit> (e.g. 30m, 15s, 2h) |
INTERVAL |
in <n><unit> (e.g. in 20m) |
SCHEDULE (one-shot) |
5-field cron (digits / * / / / - / , with spaces) |
SCHEDULE (cron) |
| Uppercase dotted identifier | EVENT |
Examples
Interval (deterministic):
--- Perimeter sweep
-- @automation perimeter_monitor
-- @trigger 15s
-- @session ISOLATED
function run(interval)
local pos = tools.call("get_self_position", {})
local result = tools.call("find_nearby", {
lat = pos.lat, lon = pos.lon, radius_m = 300, affiliation = "hostile",
})
if result.count > 0 then
tools.call("play_tone", { type = "alarm" })
end
end
Schedule with LLM (explicit delegate):
--- Daily SITREP at 0600
-- @automation morning_sitrep
-- @trigger 0 6 * * *
-- @session MAIN
function run(schedule)
local result = tools.call("delegate", {
agent_name = "analyst",
task = "Generate a SITREP from the current tactical picture.",
})
tools.call("speak_alert", { message = result.response })
end
The agent_name must match an agent's name field in custos.yaml exactly. The example above assumes an analyst agent has been configured.
Event with inline filter:
--- Hostile CoT alert
-- @automation hostile_alert
-- @trigger com.atakmap.android.maps.COT_RECD
-- @session ISOLATED
function run(event)
if not (event.cot_type and event.cot_type:find("a-h-")) then return end
tools.call("speak_alert", { message = "Hostile: " .. (event.callsign or "unknown") })
end
Filter and debounce patterns
- Filter — guard clause at the top of
run():if not (event.cot_type and ...) then return end - Debounce — track state in the memory service. Facts are keyed by
(category, key):local last = tonumber(memory:getFact("automation", "foo_last")) or 0 if event.fired_at - last < 60000 then return end memory:saveFact("automation", "foo_last", tostring(event.fired_at))
Hot-reload
AutomationFileWatcher picks up changes in /sdcard/atak/custos/automations/ automatically (500ms debounce). write_automation triggers an immediate reload.
Failure modes
- No
run()→ loader rejects at parse. - Uses an unsupported annotation → rejected with a pointer to the correct shape.
- Lua runtime error → caught, logged, surfaced as
status=errorin audit. - Invalid broadcast action → receiver registers but never fires.