Writing your first skill
This guide takes you from empty directory to a working CMDOP skill that the agent can invoke. We will build a skill that fetches a URL and returns its title, but the structure is identical for any skill.
Prerequisites
- CMDOP installed and
cmdop logincomplete. - Python 3.10+ available on PATH (CMDOP auto-creates a venv on first run).
- A test repo or scratch directory.
The shape of a skill
A skill is one directory with three files:
my-skill/
├── skill.md # manifest + system prompt
├── run.py # entry point
└── requirements.txt # auto-installed in venvStep 1 — skill.md
The manifest combines YAML frontmatter (metadata) with a free-form system prompt for the agent:
---
name: page-title
description: Fetch a URL and return its <title>.
version: 0.1.0
keywords:
- http
- scraping
schedule: null
---
You are a tiny skill that fetches a URL and returns its HTML <title>.
Input: a single URL.
Output: { ok: true, url, title } on success, { ok: false, reason } on failure.
Do not follow more than 5 redirects. Do not download bodies larger than 1 MiB.Reserved frontmatter fields: name, description, version, keywords, schedule. The agent reads the prompt as system context for the sub-session that runs the skill.
Step 2 — requirements.txt
httpx>=0.25
beautifulsoup4>=4.12CMDOP creates a venv per skill on first run and installs these. Subsequent runs reuse the venv.
Step 3 — run.py
The entry point reads CLI args, does the work, and prints exactly one JSON object:
#!/usr/bin/env python3
"""page-title skill entry point."""
import argparse
import json
import sys
import httpx
from bs4 import BeautifulSoup
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("url", help="URL to fetch")
args = parser.parse_args()
try:
with httpx.Client(follow_redirects=True, max_redirects=5) as client:
response = client.get(args.url, timeout=10.0)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
title = (soup.title.string or "").strip() if soup.title else ""
json.dump({"ok": True, "url": args.url, "title": title}, sys.stdout)
return 0
except Exception as exc:
json.dump({"ok": False, "reason": str(exc)}, sys.stdout)
return 1
if __name__ == "__main__":
sys.exit(main())Key contract:
- Print one JSON object on stdout.
ok: trueplus result fields on success,ok: falseplusreasonon failure.- Exit 0 on success, 1 on failure.
- Logs / debug output go to stderr, not stdout — the agent parses stdout as JSON.
Step 4 — drop into .cmdop/skills/
Place the directory at .cmdop/skills/page-title/ in your repo. CMDOP auto-discovers local skills.
Step 5 — verify
cmdop skills list
# page-title local 0.1.0 just now
cmdop skills run page-title -- https://example.com
# {"ok": true, "url": "https://example.com", "title": "Example Domain"}Step 6 — invoke from chat
In cmdop chat, ask the agent something that maps to your skill:
Use the page-title skill on https://example.com .
The agent calls skill_run(name="page-title", prompt="...") and surfaces the result.
Common pitfalls
- Stdout pollution —
print("debug")breaks the JSON parser. Send debug logs to stderr. - Skill name collisions — your skill name must not match a built-in tool (
execute_command,read_file, etc.). Pick something specific. - Hard timeouts — long-running skills can be killed. Stream progress via stderr, return only the final JSON on stdout.
- Heavy deps in
requirements.txt— first-run venv setup adds latency. Pin minimal versions.
Test stdin / stdout strictly. The agent treats anything but valid JSON on stdout as a parse error and surfaces ok: false.
Going further
- Add a schedule (
schedule: "*/15 * * * *") to declare it as a recurring task. - Promote to global so teammates can use it (see Publishing skills).
- Layer multiple skills with
delegate_taskfrom the agent.