The platform discovers skills automatically at startup. Adding a skill means creating a directory under skills/, building a Docker image that implements the skill contract, and restarting the server. No core code changes required.
Every skill exposes two HTTP endpoints:
GET /schema → { "system_prompt": "...", "tools": [...] }
POST /execute → { "tool": "tool_name", "params": {...} } → { "result": ... }
/schema is called once at startup. The platform reads the tool definitions and merges them with all other loaded skills — the LLM sees the full merged list every turn. /execute is called each time the LLM decides to use one of your tools.
The skill can be written in any language that can run an HTTP server.
skills/
your_skill/
SKILL.md ← metadata the platform reads + human-readable docs
AGENT.md ← optional: skill identity and hard constraints injected at L0
.env ← secrets injected into the container at start
Dockerfile
main.py ← or whatever your skill is written in
Two fields are required:
---
name: your_skill
image: your_skill:latest
---
## Purpose
What this skill does.
## Tools
- `tool_name` — description and parameters
## Usage
Any API keys or configuration needed.name— used to scope memory (preferences.md, ChromaDB collections)image— the Docker image name passed todocker run
If present, the content is injected at position 0 in every context — before the system prompt, before memory, before history. Use it to define the skill's persona and hard constraints.
You are a weather assistant. You only answer questions about weather conditions.
You do not provide financial advice, medical advice, or legal advice.
Keep responses concise and factual.Skills without an AGENT.md contribute nothing to the identity layer.
Secrets go here — never in the host .env or committed to source control:
WEATHER_API_KEY=your_key_here
The platform injects this file into the container at start. The key is never visible to the host environment or other skills.
Return the system prompt and tool definitions in LiteLLM format (same as the OpenAI API):
from fastapi import FastAPI
app = FastAPI()
@app.get("/schema")
def schema() -> dict:
return {
"system_prompt": (
"You are a weather assistant. Use get_weather to look up "
"current conditions for any city the user asks about."
),
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'Austin, TX'"
}
},
"required": ["city"]
}
}
}]
}See skills/current_time/main.py for a minimal working example.
import os
import requests
from pydantic import BaseModel
class ExecuteRequest(BaseModel):
tool: str
params: dict
@app.post("/execute")
def execute(request: ExecuteRequest) -> dict:
if request.tool == "get_weather":
city = request.params["city"]
resp = requests.get(
"https://api.weatherapi.com/v1/current.json",
params={"key": os.environ["WEATHER_API_KEY"], "q": city}
)
return {"result": resp.json()}
return {"error": f"Unknown tool: {request.tool}"}Return {"error": "..."} for unknown tools or failed calls — the platform treats any response containing an error key as a failed tool call and will not store the result in ChromaDB.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]The platform expects the skill container to listen on port 8080.
docker build -t your_skill:latest skills/your_skill/Restart the server — SkillRegistry scans skills/*/SKILL.md, reads the name and image fields, starts POOL_SIZE containers per skill, and calls /schema on each one. Your tools appear in the LLM's tool list automatically.