kind: Skill
What it is
kind: Skill declares a standalone entry in the workspace’s skill
registry — a SKILL.md body (markdown + front-matter) that any
agent in any crew can later bind to. It is the inverse of the nested
Skill reference under Crew.spec.agents[].skills: the nested form
says “this agent uses skill X (by slug)” and assumes the row already
exists; the standalone kind: Skill document says “ensure THIS
SKILL.md exists in the registry.”
The common pattern pairs the two: declare the Skill once at the top
level (or as a top-level document in a workspace bundle), then
reference its slug from as many agents as you like.
The kind is implemented in
internal/manifest/kinds/skill.go. The backing endpoint is the
import handler, which is a genuine upsert keyed on slug — so Create
and Update both POST the same body; the manifest only distinguishes
the two for readable dry-run output.
One body source — pick exactly one
A Skill carries itsSKILL.md body via exactly one of three
mutually-exclusive sources. Zero is a validation error (there is
nothing to import); two or more is rejected with the offending list.
| Source | What it is | Notes |
|---|---|---|
inline: | the SKILL.md body embedded verbatim | Capped at 8 KiB so the YAML stays diff-friendly. Larger bodies must use path:. |
path: | a manifest-relative path to a SKILL.md file | Resolved by the bundle loader against the manifest file’s directory (same safeJoin sandbox the nested Skill uses) before Validate runs. |
source: | an HTTPS URL to a remote SKILL.md | The import handler does the fetch itself (SSRF guard + SPDX license gate). The manifest layer only forwards the URL. |
YAML schema
Field reference
| Field | Type | Required | Notes |
|---|---|---|---|
metadata.name | string | yes | Human label on the registry card. |
metadata.slug | string | yes | Idempotency key + cross-kind reference. Must match ^[a-z0-9][a-z0-9_-]*$ (lowercase alphanumeric, underscore, hyphen). |
spec.description | string | yes | One-liner shown on the card. Required at the manifest level even when the SKILL.md front-matter also carries one — an empty description renders a blank card. |
spec.display_name | string | no | Card label override. Falls back to metadata.name server-side. |
spec.category | string | no | Free-form group (“networking”, “research”, …). |
spec.icon | string | no | lucide-react icon slug. |
spec.inline | string | one-of | The SKILL.md body embedded verbatim. Max 8 KiB. |
spec.path | string | one-of | Manifest-relative path to a SKILL.md file. |
spec.source | string | one-of | HTTPS URL the import handler fetches. |
spec.allow_unsafe_license | bool | no | Bypass the SPDX allowlist gate the importer otherwise applies. |
display_name,category, andiconare forward-compatibility metadata today: the importer reads front-matter from the body verbatim and does not yet merge these spec fields into it. They still round-trip via export, so set them now if you want them populated when the merge lands.
Examples
Inline body
Sibling file (large body)
Remote source
Cross-kind reference
Once a Skill is declared, agents reference it by slug:CLI reference
The manifest pipeline drives the same import endpoint the UI uses; no new per-kind subcommands ship with this kind.| Command | Description |
|---|---|
crewship apply --file skill.yaml | Declarative upsert from manifest (Create or Update). |
crewship apply --dir ./manifests/ | Walk a directory; apply every Skill in topo order (workspace credentials + skills run first). |
crewship export workspace | Round-trip — emits one kind: Skill document per non-BUNDLED row (see “Round-trip via export”). |
REST endpoint mapping
| Manifest field | POST body field | Notes |
|---|---|---|
metadata.slug | (front-matter name) | Idempotency key; the import handler keys the upsert on slug. |
spec.inline / spec.path | content | The resolved SKILL.md body. |
spec.source | url | Forwarded; the handler fetches + SSRF/license-gates. |
spec.allow_unsafe_license | allow_unsafe_license | Bypass the SPDX gate. |
| Verb | Path | Action |
|---|---|---|
GET | /api/v1/skills | List (for drift detection + export) |
POST | /api/v1/workspaces/{workspaceId}/skills/import | Create OR Update (upsert) |
DELETE | /api/v1/workspaces/{workspaceId}/skills/{skillId} | Delete (sync-mode only; not used by the default Plan) |
Validation rules
SkillDocument.Validate (offline — no HTTP, no filesystem) enforces:
apiVersion, when set, equalscrewship/v1.kind, when set, equalsSkill.metadata.nameis non-empty.metadata.slugis non-empty and matches^[a-z0-9][a-z0-9_-]*$.spec.descriptionis non-empty.- Exactly one of
inline/path/sourceis set (zero or more than one is an error, with the offending list named). inlinebody length ≤ 8 KiB.source, when set, parses as a URL, uses thehttpsscheme, and has a host.path, when set, must have been resolved by the bundle loader (a hand-constructed document that never went through Load is flagged).
WorkspaceContext — Skill is a leaf kind
with no FK references; the argument exists only for dispatcher
uniformity.
Apply behavior
ApplyUpsert (default)
For each declared Skill, Plan compares against the matched-by-slug remote row:- Remote missing →
ActionCreate(POST import). - Remote present and a body source is declared →
ActionUpdate(POST import). Because the list endpoint exposes no body hash, the manifest cannot tell whether the body actually changed, so any apply that declares a body re-posts it. This is deliberate: it is preferable to silently losing an edit. - Remote present, no body source, decoration matches →
ActionUnchanged(no REST call). - Remote row is
source: BUNDLED→ error. Bundled skills are server-seeded on every startup; the manifest refuses to touch them (mirrors the server-side guard). Pick a different slug.
ApplyReplace
Replace mode canDELETE the registry row then re-POST it. Be
aware bundled rows are still off-limits.
Round-trip via export
crewship export workspace calls ExportSkills, which emits one
kind: Skill document per non-BUNDLED row, sorted by slug.
Body lossiness: the list endpoint does NOT return the SKILL.md
content, so exported documents carry metadata + decoration but no
body source (no inline, path, or source). The export is
suitable for cataloguing what a workspace has, but re-applying it
as-is fails Validate (“exactly one of inline / path / source must be
set”). To get a round-trip-safe export you must re-attach a body
(materialise the SKILL.md files and rewrite the docs with path:
entries).