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.md file with YAML frontmatter (name, description, tags) followed by markdown describing what the skill does.
  • One or more .lua files. Each Lua function with a @tool annotation 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_paths are full paths from the skills root, not relative to this directory. This lets a skill share scripts with another skill without copying files.
  • The tags line 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 function is the LDoc annotation that registers this Lua function as an agent tool. CUST/OS parses these comments at load time.
  • @tool say_hello registers the tool name. It must match the function name.
  • @impact READ_ONLY means this tool runs without operator approval. The full ladder is READ_ONLY, INFORMATIONAL, PROCEDURAL, SIGNIFICANT, STRATEGIC, LETHAL. Anything at SIGNIFICANT or 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:

  1. Pick the custos.greeting skill.
  2. Call say_hello with no arguments.
  3. 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:

  1. The user message and the model's tool selection.
  2. The say_hello invocation 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 + .lua files.
  • 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