Authoring Skills

How to create a new Luna skill — SKILL.md format, tool declarations, endpoint wiring, and contribution guidelines.

Overview

A skill is a folder under skills/ containing a singleSKILL.md file. No Python code is required for a basic skill — just a markdown file that tells the LLM how to behave when the skill is active. For skills that need a dedicated HTTP endpoint or custom logic, a Python backend module can be added.

File structure

skills/my-skill/
skills/
  my-skill/
    SKILL.md            ← required: instructions for the LLM
    handler.py          ← optional: custom Python endpoint
    SKILL_ICON.svg      ← optional: icon for the skill UI card

SKILL.md format

SKILL.md is plain markdown. It should contain three things:

  1. Heading# Skill Name — used as the display name.
  2. Trigger description — one or two sentences describing when to use this skill. Luna reads this to decide whether to invoke the skill for a given request.
  3. Workflow rules — numbered steps the model should follow when the skill is active.
skills/my-skill/SKILL.md — minimal
# My Skill

Use this skill when the user asks to do X or Y.

Workflow:

1. Do the first thing.
2. Confirm before doing anything destructive.
3. Summarise what changed.
💡
Write the trigger description to be unambiguous. If it overlaps too broadly with other skills, the LLM may invoke it when it shouldn't. Test with a few sample messages to verify the matching feels right.

Declaring tools

Tools don't need to be declared in SKILL.md — all tools registered in the ToolRegistry are available to every skill. Document the tools your skill uses in a Tools section for the LLM's benefit:

SKILL.md — Tools section example
## Tools

- `web_search(query)` — Search the web for current information.
- `web_fetch(url)` — Fetch a URL and return its readable content.
- `workspace_write(path, content)` — Save output to a workspace file.

Listing tools explicitly helps the model know which tools are relevant without scanning through all 30+ registered tools each turn.

Adding a new tool to the registry

If your skill needs a tool that doesn't exist yet, register it inbackend/services/tool_registry.py:

backend/services/tool_registry.py
{
    "name": "my_tool",
    "description": "Does something useful. Args: query (str).",
    "parameters": {
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "The query to run"}
        },
        "required": ["query"]
    }
}

Then handle it in backend/services/tool_runner/executor.py under the execute_tool_call() dispatcher.

Dedicated endpoint (optional)

Most skills only need a SKILL.md file — they run through the standard chat pipeline. If your skill benefits from a separate streaming endpoint (e.g. the coding agent at /api/coding/stream), add a FastAPI router in the backend:

backend/routers/my_skill.py
from fastapi import APIRouter, Depends
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from backend.models.database import get_db

router = APIRouter()

@router.post("/api/my-skill/stream")
async def my_skill_stream(payload: dict, db: Session = Depends(get_db)):
    async def generate():
        # your streaming logic here
        yield 'data: {"type":"token","content":"hello"}\n\n'
        yield 'data: {"type":"done"}\n\n'
    return StreamingResponse(generate(), media_type="text/event-stream")
backend/main.py — register the router
from backend.routers.my_skill import router as my_skill_router
app.include_router(my_skill_router)

Full example — price-tracker skill

skills/price-tracker/SKILL.md
# Price Tracker

Use this skill when the user asks to track, monitor, or compare prices for products,
flights, hotels, or other purchasable items.

Workflow:

1. Identify the item and any comparison criteria (store, date range, region).
2. Use `web_search` to find current pricing from multiple sources.
3. Use `web_fetch` to read individual product pages if snippets are insufficient.
4. Compare at least three sources before giving a recommendation.
5. Save a summary table to the workspace if the user wants to revisit it later.
6. Include source URLs in the response.

## Tools

- `web_search(query)` — Search for current prices.
- `web_fetch(url)` — Read a product or retailer page.
- `workspace_write(path, content)` — Save comparison table to workspace.

That's it — no Python code needed. The skill manager picks it up automatically on the next server start.

How skills are loaded

At startup, backend/services/skill_manager.py walksskills/*/SKILL.md and builds an in-memory registry. The registry is exposed to the LLM as a list_skills tool call result on the first turn of each conversation.

Skills are hot-reloaded on each server restart — no cache invalidation needed. During development, restart the backend after editing a SKILL.md.

Python — access the registry directly
from backend.services.skill_manager import list_skills, get_skill

skills = list_skills()         # all skills as dicts
skill  = get_skill("research") # a single skill by folder name
print(skill["instructions"])   # full SKILL.md content

Best practices

  • One responsibility per skill. If a skill's trigger description covers two unrelated domains, split it into two skills.
  • Include a confirmation step for destructive actions.Any tool call that sends, deletes, uploads, or overwrites should be gated behind a user confirmation in the workflow rules.
  • Document error cases. Tell the model how to handle the most common failures — missing credentials, tool errors, empty results.
  • Keep the trigger description unambiguous. It's read by the LLM to match intent. Vague descriptions lead to false positives.
  • List the tools you use. A short Tools section prevents the model from guessing or trying tools that aren't relevant.
  • Test against real messages. Run a few sample user messages through the chat API and check whether the skill is invoked when it should be — and not when it shouldn't.