Create Your First Automation
Create a repeating perimeter check, then add an event-triggered alarm and a scheduled task that calls a specialist agent.
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 |
run(ctx) executes Lua and invokes other skills through tools.call. The first examples run without a model call. The scheduled example explicitly calls tools.call("delegate", {agent_name = "...", task = "..."}) to request model reasoning.
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 to request model reasoning. It requires a named agent in custos.yaml. This step assumes you have configured an agent called analyst with a chat provider. See the agent block in the configuration reference.
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
Here, run() calls delegate and passes the returned response to the next tool. The agent_name must match an agent's name field in custos.yaml exactly.
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.