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).

run(ctx) executes Lua and invokes other skills through tools.call("skill_name", {args}). Routine checks can run without model inference. To request model reasoning, the script explicitly calls tools.call("delegate", {agent_name = "...", task = "..."}).

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 scheduler's time loop. In the UI, Monitors shows ALL_CLEAR status, while Scheduled shows the 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.

Lua execution without a delegate call runs independently of the operator's chat turn, subject to the script runtime's execution limits.

See also