Skip to Content

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 login complete.
  • 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 venv

Step 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.12

CMDOP 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: true plus result fields on success, ok: false plus reason on 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 pollutionprint("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_task from the agent.
Last updated on