Create Your First Automation
So far you've spoken to CUST/OS and watched it call tools. In this lesson you'll teach it to act on its own — firing on a schedule, at a fixed interval, or when an ATAK event arrives.
You should have completed Getting Started and Write Your First Skill first.
What is an automation?
An automation is a .lua file under /sdcard/atak/custos/automations/ with comment annotations describing when to fire and a top-level function run(ctx) that executes when the trigger fires. There are three trigger types:
| Tab | Trigger shape | Examples |
|---|---|---|
| Scheduled | cron, one-shot | 0 6 * * *, in 20m |
| Monitors | duration | 15s, 30m, 2h |
| Events | broadcast action | com.atakmap.android.maps.COT_RECD |
The key rule: automations never call the LLM implicitly. run(ctx) executes deterministically as Lua, invoking other skills through tools.call. If reasoning is needed, the body must explicitly call tools.call("delegate", {agent_name = "...", task = "..."}). This keeps on-device models from being hammered every tick.
Step 1 — Ask the agent to write an automation
The custos.automation skill has one tool — write_automation — and the agent picks the trigger type based on the expression you describe.
In the chat panel:
Create an automation called perimeter_monitor that every 15 seconds checks
for hostile contacts within 300m of my position and plays an alarm if any
are found.
The agent writes /sdcard/atak/custos/automations/perimeter_monitor.lua:
--- 300m hostile 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
The run(interval) parameter name matches the trigger type. When the 15s interval fires, this Lua code runs in a sandbox, calls three skills in sequence, and exits. No LLM involved.
Step 2 — Open the Automation panel
Tap the Automation icon in the NavBar. Three tabs:
- Scheduled — cron and one-shot triggers
- Monitors — duration-based intervals
- Events — broadcast-driven
Your perimeter_monitor is on the Monitors tab. Each row shows the next fire time, enable/disable toggle, edit, and delete.
Step 3 — Build an event-driven alarm
Ask:
Create an event automation called hostile_alert that fires when ATAK receives
a CoT event of type a-h-* and speaks the callsign via TTS.
The agent writes:
--- Alert on hostile CoT
-- @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
Things to notice:
- The
@triggeris the broadcast intent action string. eventis a table of parsed broadcast fields —cot_type,uid,lat,lon,callsign, etc.- Filtering is a guard clause at the top of
run()— no separate annotation. @session ISOLATEDkeeps automation output out of the operator's active chat.
Step 4 — An LLM-backed schedule
Use delegate when you genuinely need reasoning. Delegation requires a named agent in custos.yaml — the default config ships with an author agent intended for skill authoring, so this step assumes you have configured your own agent (for example, analyst backed by your chat provider). See How-to: Add a cloud provider and the agent block in the configuration reference for the shape.
With an analyst agent configured, ask:
Create a morning_sitrep automation that runs at 0600 daily, asks the analyst
agent to produce a SITREP, and speaks it aloud.
The result:
--- Daily 0600 SITREP
-- @automation morning_sitrep
-- @trigger 0 6 * * *
-- @session MAIN
function run(schedule)
local result = tools.call("delegate", {
agent_name = "analyst",
task = "Generate a comprehensive SITREP from the current tactical picture.",
})
tools.call("speak_alert", { message = result.response })
end
This is the canonical pattern: run() calls delegate, captures the response, and does something with it. The agent_name must match an agent's name field in custos.yaml exactly — there is no implicit fallback to the chat provider.
Step 5 — Edit, disable, delete
From the Automation panel:
- Toggle — flips enabled/disabled.
- Edit — opens the file in the in-app editor.
- Delete — removes the file.
What you learned
- Automations are
.luafiles with@automation,@trigger,@session,@descriptionannotations and arun(ctx)function. - Trigger type is auto-detected — no
@typeannotation. run(ctx)executes deterministically; LLM access is only through explicittools.call("delegate", ...).- Filter logic is a guard clause inside
run(); no@filterannotation. - The agent writes automations for you via
write_automation.
Where to go next
- How-to: Write an automation — more templates and patterns.
- How-to: Configure hooks — control which tools the agent can call.