# Skill Creator

> Create/edit Nightingale AI Skills. Use when the user wants to create a new skill, codify a troubleshooting or operations workflow into a reusable skill, build a skill that can run scripts (Python/Bash), or modify/improve/optimize an existing self-built skill. Use this skill whenever the user says things like "make a skill", "save this workflow as a skill", "teach the AI a new trick", "tweak that skill of mine", "let the AI learn to troubleshoot following these steps", etc.

- Skill: `ccfos/skill-creator` (Agent Skill, multi-file: 12 files)
- Install (CLI): `npx skillmds@latest add ccfos/skill-creator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ccfos/skill-creator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: ccfos (https://skillmd.com/u/ccfos)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ccfos/skill-creator

---


# Skill: Nightingale (N9E) Skill Authoring Assistant

Help the user **create and improve their own AI skills directly in the conversation**. A skill is a single `SKILL.md`: YAML frontmatter (`name` + `description` + optional `builtin_tools` / `max_iterations`) plus a Markdown workflow body. A skill teaches the Nightingale AI a **reusable, auto-triggerable** way of working.

Your job is not to write a pile of docs for the user, but to: **clarify intent → draft → show the user → persist**, and to iterate when the user wants improvements. Persistence, permission checks, and double-confirmation are all handled by the tools; you focus on getting the skill content right.

> Permissions: creating/modifying skills requires the `/ai-config/skills` permission. If `create_skill`/`update_skill` returns forbidden, tell the user "this needs skill management permission, please ask an administrator to grant it", and do not keep retrying.

---

## 1. The two kinds of skills (decide which path first)

| Kind | What it is | Typical scenarios | How to persist |
|------|------------|-------------------|----------------|
| **Knowledge/workflow** | Only a `SKILL.md`, where the body is an **operations/troubleshooting workflow** that calls the platform's existing built-in tools as needed | "Standard troubleshooting workflow for Redis memory alerts", "create a class of alerts following these steps", "customer-service scripts" | Write the workflow in `instructions` + declare the built-in tools it uses in `builtin_tools` |
| **Script** | `SKILL.md` + `main.py` / `main.sh`, running code in an isolated sandbox to process data | "pull a disk report", "call an internal API to aggregate data", "batch-compute a metric" | Put the scripts in `files`; the runtime infers by convention (`main.py`→python, `main.sh`→bash) |

How to decide: **one question is enough** —— "Does this skill teach the AI to do something by following a workflow using existing features, or does it need to actually run a script/code to process data?" Most ops scenarios are the former (knowledge/workflow).

Script-kind notes: scripts **hold no platform token whatsoever**, run with restrictions in the sandbox as the identity of the user who started the conversation, and their output is treated as untrusted data. Scripts **can access the network by default** (via an audited proxy), and **can also call a set of read-only n9e APIs by default** (via the Skill Gateway, as the originating user's identity, restricted by their permissions) —— see section 6 for details. Only use the script kind when you truly need to "execute code"; prefer knowledge/workflow whenever a built-in tool can do the job.

---

## 2. Interview (clarify before acting, but don't interrogate)

Extract answers from the existing conversation as much as possible, fill in what's missing, and ask just 2-3 key points at a time:

1. **What should this skill do?** —— state in one sentence the task it helps the AI accomplish.
2. **When should it be used? How would the user phrase it?** —— this determines `description`, the crux of whether the skill gets auto-selected (see section 3).
3. **Knowledge/workflow or script?** (see section 1)
4. **Which Nightingale data/operations are involved?** (alerts/datasources/hosts/dashboards/events…) —— map these to `builtin_tools`, or to what the script needs to access.

Stop interviewing once you can write a decent draft; don't fire off chained follow-ups just to fill in fields.

---

## 3. Writing a good description —— the crux of triggering

Normally only `name + description` of a skill stays resident in the "available skills catalog", and the AI decides whether to use it based on the `description`. So the `description` must describe **what the user would say, in what scenario**, not just what the skill does.

- ❌ Too dry: `Generate a Redis troubleshooting report.`
- ✅ Specific and "a bit proactive": `Troubleshoot Redis performance problems and generate a report. Use when the user says Redis is slow, Redis memory is high, connections are maxed out, wants to check Redis health, or pastes Redis-related alerts.`

Key points: cover **multiple colloquial phrasings**, include trigger scenarios, push moderately ("the Nightingale AI tends to under-trigger skills, so writing the description proactively corrects for that"), but **do not exaggerate** to the point of pulling in unrelated requests.

---

## 4. Writing good instructions —— the skill body

The body is the working manual for the "AI that executes this skill". A good body:

- **Opens with a one-line role definition** ("You are an X assistant, your goal is…").
- **Clear steps/decision tree**: what to do first, how branches go, when to stop and ask the user.
- **Explains why**, rather than piling up `MUST`/`MUST NOT`. Today's models have judgment; explaining the reasoning works better than rigid rules; only re-emphasize the occasional truly critical red line.
- **Output spec**: what format of result the user should see.
- Don't write `name`/`description`/`builtin_tools` and other frontmatter into the body —— they are separate parameters of `create_skill`, and the tool will synthesize the frontmatter automatically.

When the body gets long (>500 lines) you can split it: keep the core workflow in `SKILL.md`, move details into `reference.md` etc. under `files`, and state in the body "read reference.md when you need X".

---

## 5. Picking the right builtin_tools (almost always needed for knowledge/workflow)

If the skill will have the AI call platform features (query alerts, query datasources, build dashboards…), you must declare those tools in `builtin_tools`.

**Always call `list_skill_builtin_tools` first** to get the real tool list (can be filtered with `search`, e.g. `search:"alert"`), and pick names from it. **Do not invent tool names from memory** —— `create_skill` validates them, and writing a nonexistent tool name will error out and make you fix it.

Declare only the tools the skill actually uses; don't cram them all in.

---

## 6. The scripts of a script-kind skill

- Put scripts in `files`, each item is `{path, content}`, e.g. `[{"path":"main.py","content":"...python..."}]`.
- Entry-point convention: `main.py` (python3) or `main.sh` (bash); a single script in the directory is also auto-detected.
- **Do not** put `SKILL.md` into `files` —— it is auto-generated by the tool from the structured fields you pass.
- Write `compatibility` for the script-kind skill (e.g. `needs sandbox; python3`) to hint at dependencies.

### The script's runtime environment (read before writing scripts)

- **Filesystem**: writable `/workspace` (also `HOME`/`TMPDIR`) and `/output`; read-only `/skill` (its own files) and `/input`; the root filesystem is read-only.
- **Network: open by default, but governed.** By default (server-side `Egress=open`) the script **can access the network** —— the platform automatically injects `HTTP(S)_PROXY`, and standard HTTP clients (Python `requests`/`urllib`, `curl`) can reach **public and internal** hosts **with no code changes**, with all egress going through an **audited proxy**. But two iron rules are always enforced even under open: ① n9e's own **loopback `127.0.0.0/8`** (the script cannot directly connect to the local n9e API/DB); ② **cloud metadata / link-local `169.254.0.0/16`** (to prevent stealing cloud credentials). UDP is also disabled. Administrators can tighten egress to "allowlisted hosts only" or "fully offline", so **write scripts to tolerate network failures/restrictions**, and don't assume the network is always available.
- **Platform credentials: the script holds no token.** This is a security design —— the script cannot obtain or leak any platform secret.
- **Calling n9e's own API: enabled by default (read-only, blocklist mode).** The Skill Gateway is an **HTTP passthrough proxy**: it forwards GET requests the script sends to n9e's own `/api/n9e/*`, executing them **as the identity of the user who started the conversation** (using that user's API token, which stays host-side and never enters the sandbox; if the user has no token one is created automatically). n9e's own route middleware does the usual RBAC + business-group checks for that user.
  - **Before writing any script that calls the n9e API, read `n9e-api.md`** (`read_file`, base `skill-creator`) — the index: call protocol, the response envelope + the two list shapes, the business-group scoping idiom, and the deny-list. Then read the **per-resource file under `api/`** for what you need (`api/alert-events.md`, `api/alert-rules.md`, `api/targets.md`, `api/boards.md`, `api/alert-mutes.md`, `api/alert-subscribes.md`, `api/busi-groups.md`, `api/user-groups.md`) — each has the exact endpoints, every query param, and the **full response object fields**. To query actual metric/log **data** (not config), read `api/data-query.md` (POST query endpoints — the gateway allows POST only for these) and `api/datasources.md` (`/datasource/brief`, to get a `datasource_id` first). Paths are NOT guessable by analogy — there is no `/alert-events`, no plain `/alert-rules`, no `/dashboards`; the real ones are `/alert-his-events/list`, `/busi-groups/alert-rules`, `/boards`. Take paths and field names from these files, not from memory.
  - **Usage**: get the UNIX socket path from the environment variable `N9E_SKILL_GATEWAY`, and send/receive as **line-delimited JSON**:
    - Request: `{"method":"GET","path":"/api/n9e/alert-his-events/list","query":{"hours":"24"}}\n` (**always include the `/api/n9e` prefix in `path`**; **all `query` values must be strings**)
    - Response: `{"ok":true,"status":200,"data":<n9e raw response>}` or `{"ok":false,"status":<code>,"error":"..."}`; `data` is n9e's envelope `{"dat":<actual data>,"err":""}` —— **read `data["dat"]`** (list endpoints return either `{"list":[...],"total":N}` or a bare array under `dat` — see `n9e-api.md`).
  - **A wrong path fails SILENTLY — always validate the response.** An unknown path is not a 404: n9e returns its SPA `index.html` and the gateway puts that **HTML string into `data`**. So if `data` is a string (often starting with `<!-- ... Nightingale Team`) rather than a dict, you used a nonexistent endpoint. Scripts must check `ok`, that `data` is a dict, and that `data["err"]` is empty before using `data["dat"]`.
  - **Blocklist / writes**: secret-bearing reads (datasources, notify configs, users/tokens, SSO, proxy…) and all writes/deletes (POST/PUT/DELETE) are rejected with `ok:false` (full list in `n9e-api.md`). Handle it gracefully.
  - If the endpoint you need isn't in `n9e-api.md`, have the user confirm the real `/api/n9e/...` request from their browser dev tools rather than guessing.
- **Minimal dependencies**: the default slim base **does not include pip**, so don't assume you can install third-party packages; keep Python to the standard library where possible, and use common POSIX commands in shell.

---

## 7. Persistence flow (the unified action)

1. **Draft**: **show the full skill to the user in the conversation** (name, description, kind, body highlights, the tools/scripts it will use), so the user can review it first. This step is for humans —— don't skip it.
2. **Call `create_skill`** to submit structured fields: `name` / `description` / `instructions` / optional `builtin_tools` / `files` / `max_iterations` / `compatibility` / `user_group_ids` / `private`.
   - `name` uses kebab-case (lowercase alphanumerics and hyphens), e.g. `redis-slowlog-triage`, and must not collide with a built-in skill name.
   - Give multi-step skills a sensible `max_iterations` (e.g. 15~30).
   - **Authorization (`user_group_ids` + `private`)**: every skill needs managing teams — their members may edit it. Only pass `user_group_ids` when the user actually named the teams (use `list_teams` to resolve names → ids); **otherwise leave it out** and the tool returns a team-picker form for the user to fill in — never pick a team on the user's behalf. `private` is the visibility (0 = visible to everyone, 1 = only the managing teams, default 1) and **only an admin may set it to 0**; a non-admin's skill is always private, and the picker form doesn't even show the field for them. Don't interrogate the user about these fields up front — draft the skill first and let the form ask.
   - **After the team-picker form comes back**, just call `create_skill` again with the **same `name`**. The skill's content (description / instructions / files / tools) was stashed server-side when the form popped and is restored verbatim — you do **not** need to re-paste the body or the scripts, and re-sending them changes nothing. Keep the name identical: a different name on that turn aborts the create (the tool tells you the drafted name — re-call with it).
   - If the tool reports the **stash expired / is gone**, do not retry and do not try to rewrite the body from memory: the create is off. Tell the user, re-draft the skill in full, show it to them again, and start over.
   - If the tool says it **couldn't stash the draft** (no conversation context — e.g. a CLI / A2A caller), no form is shown: ask the user which teams should manage the skill, resolve them with `list_teams`, and pass `user_group_ids` in the same `create_skill` call. That path never interrupts, so nothing is lost.
3. **Double confirmation**: the first call to `create_skill` **does not actually write to the DB**; instead it returns a "pending confirmation" prompt and ends this turn. After the user replies "confirm", the runtime automatically replays the tool with `confirmed` to actually persist it —— you **do not need** to construct `proposal_id`/`confirmed` yourself, leave it to the system.
4. **After persistence succeeds**, tell the user: the skill is created, whether it's enabled, which teams manage it, and that it can be managed under "AI Config → Skills"; if it's a script-kind skill and the sandbox is available, you can suggest running it once with `run_skill_script` to verify.

> After a skill is created it is materialized immediately, and the **next conversation turn** can see it in the skill catalog and have it triggered.

---

## 8. Improving / editing an existing skill

1. First `get_skill` (pass `name`) to read the current full definition (including frontmatter and file list).
2. Confirm with the user what to change.
3. `update_skill`: pass only the **fields to change**; unspecified ones stay as-is (the tool reads back unchanged fields from the current SKILL.md, and won't drop tool bindings). `files` are upserted by filename and don't affect files not mentioned. It likewise goes through "propose first, write only after the user confirms" double confirmation.
4. Built-in skills (`created_by=system`) cannot be modified —— the tool will reject it and guide the user to instead create a new skill of their own.

---

## 9. The "test it" loop for script-kind skills (when the sandbox is available)

After creating a script-kind skill, if the `run_skill_script` tool is available (meaning this server has the sandbox enabled), you can:

1. Run the entry-point script once with `run_skill_script` (pass `skill_name`).
2. Show the real output to the user, and check whether it matches expectations.
3. If it's wrong, discuss script changes with the user, update `main.py`/`main.sh` in `files` via `update_skill`, and run again.

Treat this as "lightweight verification", not a mandatory step; skip it when the sandbox isn't enabled, and don't refuse to create the skill just because you can't test it.

---

## 10. Principles and boundaries

- **Don't create harmful skills**: don't help build skills used for unauthorized access, data exfiltration, bypassing permissions, or hiding malicious behavior. A skill's content should match its description and must not smuggle in dangerous actions unrelated to what's declared.
- **Stay focused**: one skill solves one class of problem. A large, mixed skill is both hard to trigger and hard to maintain —— suggest splitting it.
- **Check from the user's perspective**: after writing the draft, re-read it with the eyes of "a newcomer seeing this skill for the first time" —— will the description trigger? Can the body's steps be followed?
- **Don't over-engineer**: don't turn a two-step workflow into ten steps; don't write a script for something one built-in tool can do.

---

## Full example (knowledge/workflow)

User: "Make me a skill so that from now on when I say MySQL connections are high, it troubleshoots following a fixed routine."

1. Interview to fill gaps: confirm the troubleshooting steps and which datasources/metrics to look at.
2. `list_skill_builtin_tools search:"datasource"` / `search:"alert"` to pick real tools like `list_datasources`, `query_prometheus`, `search_active_alerts`.
3. Draft and show:
   - name: `mysql-connection-triage`
   - description: `Troubleshoot the problem of excessive MySQL connections. Use when the user says MySQL connections are high/maxed out, too many connections, or reports a MySQL-connection-related alert.`
   - builtin_tools: `["list_datasources","query_prometheus","search_active_alerts"]`
   - instructions: a role + the decision tree "first look at the current connection-count metric → compare against max_connections → check whether slow queries are piling up → locate the source IP → give remediation advice" + an output spec.
4. `create_skill(...)` → show pending confirmation → user "confirms" → persist → report completion.

