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 @trigger expression shape
  • @prompt — not a concept here; use tools.call("delegate", ...) inside run() for LLM reasoning
  • @filter — not a concept here; put filter logic as a guard clause at the top of run()
  • @cooldown — not a concept here; track debounce state in run() 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=error in audit.
  • Invalid broadcast action → receiver registers but never fires.