aiagent/skill/embedded/builtin/skill-creator/SKILL.md
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/skillspermission. Ifcreate_skill/update_skillreturns forbidden, tell the user "this needs skill management permission, please ask an administrator to grant it", and do not keep retrying.
| 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.
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:
description, the crux of whether the skill gets auto-selected (see section 3).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.
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.
Generate a Redis troubleshooting report.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.
The body is the working manual for the "AI that executes this skill". A good body:
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.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".
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.
files, each item is {path, content}, e.g. [{"path":"main.py","content":"...python..."}].main.py (python3) or main.sh (bash); a single script in the directory is also auto-detected.SKILL.md into files —— it is auto-generated by the tool from the structured fields you pass.compatibility for the script-kind skill (e.g. needs sandbox; python3) to hint at dependencies./workspace (also HOME/TMPDIR) and /output; read-only /skill (its own files) and /input; the root filesystem is read-only.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./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.
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.N9E_SKILL_GATEWAY, and send/receive as line-delimited JSON:
{"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){"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).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"].ok:false (full list in n9e-api.md). Handle it gracefully.n9e-api.md, have the user confirm the real /api/n9e/... request from their browser dev tools rather than guessing.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.max_iterations (e.g. 15~30).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.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).list_teams, and pass user_group_ids in the same create_skill call. That path never interrupts, so nothing is lost.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.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.
get_skill (pass name) to read the current full definition (including frontmatter and file list).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.created_by=system) cannot be modified —— the tool will reject it and guide the user to instead create a new skill of their own.After creating a script-kind skill, if the run_skill_script tool is available (meaning this server has the sandbox enabled), you can:
run_skill_script (pass skill_name).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.
User: "Make me a skill so that from now on when I say MySQL connections are high, it troubleshoots following a fixed routine."
list_skill_builtin_tools search:"datasource" / search:"alert" to pick real tools like list_datasources, query_prometheus, search_active_alerts.mysql-connection-triageTroubleshoot 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.["list_datasources","query_prometheus","search_active_alerts"]create_skill(...) → show pending confirmation → user "confirms" → persist → report completion.