Write Your First Skill
In this lesson you will create a new skill from scratch, hot-reload it onto the device with no rebuild, and watch the agent call it in real time.
You should have completed Getting Started first.
What is a skill?
A skill is a directory under /sdcard/atak/custos/skills/ that contains:
- A
SKILL.mdfile with YAML frontmatter (name, description, tags) followed by markdown describing what the skill does. - One or more
.luafiles. Each Lua function with a@toolannotation becomes a tool the agent can call.
No APK rebuild is needed to add or change a skill. Save the file and CUST/OS picks it up immediately.
Step 1 -- Create the directory and stub files
The editor's + button makes a scratch file, not a new skill directory. The easiest way to scaffold a new skill is from chat -- ask the agent:
Create a new skill called custos.greeting that returns a personalized hello to the operator.
The built-in custos.skill_creator skill takes care of creating /sdcard/atak/custos/skills/custos.greeting/ with stub SKILL.md and .lua files, which you will overwrite in the next two steps.
If you prefer to set up the directory by hand, from a workstation:
adb shell mkdir -p /sdcard/atak/custos/skills/custos.greeting
Then use File > Open in the editor to create SKILL.md and say_hello.lua inside the new folder.
The directory name must match the group.name pattern -- group is custos, name is greeting.
Step 2 -- Write SKILL.md
Inside the new folder, create a file called SKILL.md and paste:
---
group: custos
name: greeting
description: Greet the operator by their callsign
script_paths:
- custos.greeting/say_hello.lua
tags:
- greeting
- example
---
# Greeting
A demo skill that returns a friendly hello using the operator's callsign.
The agent should call `say_hello` whenever the user says hi.
A few things to notice:
script_pathsare full paths from the skills root, not relative to this directory. This lets a skill share scripts with another skill without copying files.- The
tagsline feeds the keyword index so the skill selector ranks this skill highly when the operator says "hi" or "hello." - The markdown body is part of the skill description in the system prompt -- the LLM sees it.
Tap Save.
Step 3 -- Write the Lua tool
Create a file called say_hello.lua in the same directory and paste:
--- Greet the operator by their callsign.
-- @tool say_hello
-- @description Returns a short personalized hello using the operator's ATAK callsign.
-- @impact READ_ONLY
function say_hello(params)
local MapView = import("com.atakmap.android.maps.MapView")
local mapView = MapView:getMapView()
local self = mapView:getSelfMarker()
local callsign = self and self:getMetaString("callsign", "operator") or "operator"
return {
status = "success",
message = "Hello, " .. callsign .. ". Standing by."
}
end
Tap Save.
Walkthrough
- The comment block above
functionis the LDoc annotation that registers this Lua function as an agent tool. CUST/OS parses these comments at load time. @tool say_helloregisters the tool name. It must match the function name.@impact READ_ONLYmeans this tool runs without operator approval. The full ladder isREAD_ONLY,INFORMATIONAL,PROCEDURAL,SIGNIFICANT,STRATEGIC,LETHAL. Anything atSIGNIFICANTor higher triggers the approval gate.import(...)is a host binding that returns a Java class so you can call its static methods.self:getMetaString(...)uses colon syntax to call a Java instance method. Dot syntax (self.getMetaString) does not work in Lua when calling Java methods.- The return value is a Lua table. CUST/OS serializes it to JSON and feeds it back to the LLM as the tool result.
Step 4 -- Hot-reload
As soon as you save, CUST/OS detects the change and reloads all skills. The keyword index rebuilds and the new tools are immediately callable. No plugin restart or APK rebuild needed.
Step 5 -- Ask the agent to greet you
Switch back to the Chat panel and type:
Say hi
The agent should:
- Pick the
custos.greetingskill. - Call
say_hellowith no arguments. - Stream back something like "Hello, JOSH-01. Standing by."
If the LLM picks a different skill, your tags or description may not be specific enough. Go back and add more synonyms (hi, hello, greet, etc.) -- the hot-reload re-indexes immediately.
Step 6 -- Inspect the audit trail
Open the Audit panel. You should see entries recording:
- The user message and the model's tool selection.
- The
say_helloinvocation with its result.
The audit log is append-only and tamper-resistant.
Step 7 -- Add a parameter
Edit say_hello.lua and replace it with:
--- Greet the operator by their callsign.
-- @tool say_hello
-- @description Returns a short personalized hello using the operator's ATAK callsign.
-- @tparam string [salutation=Hello] Optional greeting word, e.g. "Howdy" or "Good morning".
-- @impact READ_ONLY
function say_hello(params)
local MapView = import("com.atakmap.android.maps.MapView")
local mapView = MapView:getMapView()
local self = mapView:getSelfMarker()
local callsign = self and self:getMetaString("callsign", "operator") or "operator"
local salutation = params.salutation or "Hello"
return {
status = "success",
message = salutation .. ", " .. callsign .. ". Standing by."
}
end
Save. Now ask the agent: "Greet me with 'good morning.'"
The agent will see the new salutation parameter, call say_hello({ salutation = "Good morning" }), and reply with "Good morning, JOSH-01. Standing by."
What you learned
- A skill is a directory with
SKILL.md+.luafiles. - LDoc annotations turn Lua functions into agent tools.
- The host gives you
import,classForName,runOnUiThread, and any services your config exposes. - Saving a file triggers hot-reload -- no APK rebuild.
- Optional parameters use the
[name=default]syntax in@tparam. - The audit log records every tool invocation.
Where to go next
- How-to: Write a skill -- cookbook patterns for common tasks (placing markers, sending CoT, calling other plugins, etc.).
- How-to: Debug a failing skill -- error reporting and the in-app editor's debug workflow.
- Create your first automation -- so the agent can act on its own.