The Automation Model

CUST/OS lets automations run on their own — on a schedule, at a fixed interval, or in response to an ATAK broadcast event.

What an automation is

An automation is a .lua file in /sdcard/atak/custos/automations/. It declares:

  • When to fire — @trigger (cron, duration, one-shot, or broadcast action; type is auto-detected from shape).
  • What to do — a top-level function run(ctx) that executes deterministically when the trigger fires.
  • Where to run it — @session MAIN (shares the operator's chat) or @session ISOLATED (hidden background context, the default).

Automations never route to the LLM implicitly. run(ctx) is Lua — it invokes other skills through tools.call("skill_name", {args}). If the task genuinely requires LLM reasoning, the body must explicitly call tools.call("delegate", {agent_name = "...", task = "..."}). This is the only path from an automation to an LLM.

This design means an interval automation firing every 15 seconds costs milliseconds, not a full LLM ReAct loop. Small on-device models aren't hammered by schedules; they only run when the author explicitly delegates.

Three trigger types

Trigger type is classified automatically from the expression shape:

Type Shapes UI tab
schedule cron (0 7 * * *), one-shot (in 20m) Scheduled
interval duration (30s, 15m, 2h) Monitors
event broadcast action (com.atakmap.android.maps.COT_RECD) Events

Schedule and interval share the same time loop in the scheduler. The UI split is about operator intent — Monitors shows ALL_CLEAR status, Scheduled shows next-fire time.

Schedule

Time-based automation. Fires on a cron expression or once after a delay.

--- Daily morning sitrep
-- @automation morning_sitrep
-- @trigger 0 7 * * *
-- @session MAIN

function run(schedule)
    local result = tools.call("delegate", {
        agent_name = "analyst",
        task = "Generate a morning SITREP.",
    })
    tools.call("speak_alert", { message = result.response })
end

agent_name must match an agent configured in custos.yaml — the example above assumes an analyst agent has been added.

Interval (Monitors)

Simple repeating duration — intended for "check state, report if anomalous" patterns.

--- 300m hostile sweep every 15s
-- @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

Event

Broadcast-driven. The run(event) body receives a table of parsed broadcast fields and decides what to do.

--- Hostile contact alarm
-- @automation hostile_alarm
-- @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

Filtering is a guard clause — no separate annotation.

The ctx parameter

run() takes one argument. The parameter name is convention (Lua doesn't enforce names) but matching the trigger type makes the signature self-documenting:

  • function run(schedule){ fired_at, cron }
  • function run(interval){ fired_at, interval }
  • function run(event){ fired_at, action, cot_type, uid, lat, lon, callsign, hae, how, cot_xml, extra_* }

Sessions: MAIN vs ISOLATED

Session Behavior
MAIN Any delegate response flows into the operator's active chat
ISOLATED (default) Stays in a hidden background context

For purely deterministic automations (no delegate), session affects audit tagging only.

How automations are created

Ask the agent:

  1. "Create an automation that checks for hostiles every 15 seconds and plays an alarm if found."
  2. The agent picks custos.automation and calls write_automation with an auto-detected trigger type.
  3. The tool writes the file, validates Lua syntax, and triggers a reload.
  4. The new automation appears in the Automation panel and is armed.

Automations do not preempt operator chat

Only one delegate call executes at a time. If an automation's run() calls delegate while the operator is mid-chat, the delegation serializes behind the operator's turn.

Deterministic run() bodies (no delegate) don't wait on the operator — they execute immediately.

See also