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/
SKILL.md ← required: instructions for the LLM
handler.py ← optional: custom Python endpoint
SKILL_ICON.svg ← optional: icon for the skill UI cardSKILL.md format
SKILL.md is plain markdown. It should contain three things:
- Heading —
# Skill Name— used as the display name. - 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.
- Workflow rules — numbered steps the model should follow when the skill is active.
# 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.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:
## 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:
{
"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:
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")from backend.routers.my_skill import router as my_skill_router
app.include_router(my_skill_router)Full example — price-tracker skill
# 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.
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 contentBest 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.