---
name: create-skill
description: Guide for creating a new autopilot skill. Covers SKILL.md format, frontmatter requirements, adding executable scripts (Python with uv, shell), argument conventions, and how skills are auto-discovered.
---

# Create a Skill

Use this guide when an operator asks you to create a new autopilot skill.

A skill is a directory under `autopilot/skills/` containing a `SKILL.md` file
and optionally one or more executable scripts.

## Step 1 — Choose a name and location

Skill names must be lowercase letters, numbers, and hyphens only, max 64 characters.
The directory name becomes the skill name.

```
autopilot/skills/<skill-name>/
├── SKILL.md          # required
└── scripts/          # optional, place scripts here or in the root
    └── my-script.py
```

Scripts can live in the skill root or in a `scripts/` subdirectory — both are scanned.

## Step 2 — Write SKILL.md

`SKILL.md` has two sections:

**YAML frontmatter** (required):
```markdown
---
name: my-skill
description: One-sentence description of what this skill does (max 1024 chars).
---
```

Both `name` and `description` are required. The `name` must match the directory name.

**Markdown body** (the instructions loaded by `load_skill`):
Write step-by-step guidance the agent should follow. Structure it clearly with
headers, code blocks, and tables. The body is only loaded when `load_skill` is
called, so keep metadata concise and put detail here.

## Step 3 — Add scripts (optional)

Scripts are auto-discovered by extension or executable bit. Supported types:

| Extension | Interpreter |
|---|---|
| `.py` | Python (shebang overrides, see below) |
| `.sh` `.bash` `.zsh` `.fish` | Corresponding shell |
| `.ps1` | PowerShell |
| `.bat` `.cmd` | cmd /c |
| no extension + executable bit | Direct execution |

### Python scripts with dependencies

Use a `#!/usr/bin/env -S uv run -q` shebang and a PEP 723 inline metadata block
so the script manages its own dependencies independently of the agent's environment:

```python
#!/usr/bin/env -S uv run -q
# /// script
# requires-python = ">=3.14"
# dependencies = ["httpx>=0.27", "boto3>=1.35"]
# ///
"""Short description of what this script does."""

import argparse

def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--region", required=True)
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args()
    # ... do work ...
    print(f"done: {args.region}")

if __name__ == "__main__":
    main()
```

Make the script executable: `chmod +x scripts/my-script.py`

### Argument conventions

All arguments are passed as named CLI flags — positional arguments are not supported.

| Python type | CLI result |
|---|---|
| `"us-east-1"` | `--region us-east-1` |
| `True` | `--dry-run` (flag only) |
| `False` / `None` | omitted |
| `["a", "b"]` | `--item a --item b` |

The agent calls the script like:
```python
await run_skill_script(skill_name="my-skill", script_path="scripts/my-script.py", args={"region": "us-east-1", "dry-run": True})
```

The return value is the combined stdout + stderr string.

## Step 4 — Create the files

Use `write_file` to create `SKILL.md` and any scripts:

```python
await write_file(
    "<auto_deploy_dir>/autopilot/skills/my-skill/SKILL.md",
    "---\nname: my-skill\ndescription: ...\n---\n\n# My Skill\n..."
)
await write_file(
    "<auto_deploy_dir>/autopilot/skills/my-skill/scripts/my-script.py",
    "#!/usr/bin/env -S uv run -q\n..."
)
```

Then make scripts executable from `run_code` using a shell capability, or note
that the operator should run `chmod +x` manually if the execute bit matters
(on macOS/Linux, `run_skill_script` falls back to the extension-based interpreter
even without the execute bit for `.py`, `.sh`, etc., so `chmod` is only required
for extension-less executables).

## Step 5 — Verify discovery

Skills are loaded at `SkillsCapability` initialization time (when the agent starts).
To verify the new skill is discoverable **without restarting**:

```python
skills = await list_skills()
print(skills)
```

If the skill doesn't appear, the agent needs to be restarted for it to be picked up
(or `auto_reload=True` would need to be set in `SkillsCapability`).

## Full example

```
autopilot/skills/query-issues/
├── SKILL.md
└── scripts/
    └── list-open-issues.py
```

`SKILL.md`:
```markdown
---
name: query-issues
description: Query GitHub issues in tidbcloud/auto-deploy by label, state, or assignee.
---

# Query Issues

Use `run_skill_script` to run `scripts/list-open-issues.py`.

Arguments:
- `label` — filter by label (optional)
- `state` — "open" or "closed" (default: "open")
```

`scripts/list-open-issues.py`:
```python
#!/usr/bin/env -S uv run -q
# /// script
# requires-python = ">=3.14"
# dependencies = ["PyGithub>=2.3"]
# ///
import argparse, os
from github import Github

def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--label", default="")
    parser.add_argument("--state", default="open")
    args = parser.parse_args()

    gh = Github(os.environ["GITHUB_TOKEN"])
    repo = gh.get_repo("tidbcloud/auto-deploy")
    issues = repo.get_issues(state=args.state, labels=[args.label] if args.label else [])
    for issue in issues:
        print(f"#{issue.number} {issue.title}")

if __name__ == "__main__":
    main()
```
