Skills
A skill is a named playbook an agent loads on demand. Where a tool is a single capability (one HTTP call, one DB query), a skill is a recipe — a body of instructions that teaches the agent how to do a multi-step task using whatever tools it has.
Skills are loadable graph-memory nodes — cold_memory rows with type = "skill" and meta.loadable = true (the same table Memory documents), not a separate skills table. Both built-in and custom skills are nodes; meta.is_builtin is what tells them apart. The CP /v1/skills API is a read/write proxy over that graph that the Dashboard → Skills page consumes.
There is no skill versioning, PR-flow, or CI validation suite. Older references to semver skill versions, a PR-managed skill repo, or a CI schema/lint/trigger-overlap check are stale — skills today are plain CRUD against graph nodes. There is also no
trigger,load_tools,bound_agents, oris_dangerousfield on a skill; see the object below for what actually exists.
The skill object
List row:
{
"slug": "fewer-permission-prompts",
"title": "Fewer permission prompts",
"description": "Scan transcripts and add an allowlist to settings.json.",
"category": "productivity",
"path": null,
"body_length": 4213,
"is_builtin": true,
"used_by": 2
}
Detail (GET /v1/skills/{slug}) adds the full body and the assignment fields:
{
"id": 9931,
"slug": "fewer-permission-prompts",
"title": "Fewer permission prompts",
"description": "Scan transcripts and add an allowlist to settings.json.",
"category": "productivity",
"body": "# fewer-permission-prompts\n\n1. Read the project's .claude/settings.json...",
"body_length": 4213,
"is_builtin": true,
"always_load": false,
"status": "active",
"agent": null,
"agents": [],
"scope": "shared"
}
| Field | Type | Notes |
|---|---|---|
id |
int | Detail-only. The underlying graph-node id — pass it to GET /v1/memory/entries/{id}/links for the relationships panel. |
slug |
string | URL-safe handle. |
title |
string | Display title, with any Skill: <slug> — ingestion prefix stripped. |
description |
string | One-liner shown in the picker. |
category |
string | Derived from the slug when not explicitly set. |
path |
null | Always null — skills stopped being files on disk (Skillunify S5); the field is kept for shape compatibility. |
body |
string | Detail-only. The full playbook text, unredacted for admin callers and blanked (with redacted: true) for public/portal callers — see Agents for the redaction model. |
body_length |
int | Length of body in characters. On the list endpoint this is the only signal of body size (the body itself isn't returned there). |
is_builtin |
bool | true for graph-ingested skills (read-only via this API); false for portal-created custom skills. |
used_by |
int | List-only. How many other skills reference this one via a [[slug]] depends_on wikilink. |
always_load |
bool | Detail-only. Whether the skill renders into its scoped agents' boot context on every run. |
status |
string | Detail-only. Graph-node lifecycle status (e.g. active). |
agent |
string | null | Detail-only. Legacy single-agent column; superseded by agents. |
agents |
string[] | Detail-only. The full set of agents this skill is scoped to. |
scope |
string | Detail-only. shared or agent — agent scope requires at least one entry in agents; shared scope forces agents to be empty. |
Endpoints
List skills
GET /v1/skills
Returns every loadable skill node, redacted to the caller's permissions.
const { data } = await catentio.skills.list();
skills = catentio.skills.list()
curl 'https://catent.io/api/v1/cp/v1/skills' \
-H "Cookie: catentio_session=<session cookie set at login>"
Meta: {"total", "builtin", "custom"} — same convention as Agents and Tools.
Retrieve a skill
GET /v1/skills/{slug}
Returns the full detail row. body is unredacted for admin callers only.
Create a skill
POST /v1/skills
Creates a custom (non-builtin) loadable node. Body:
{
"slug": "my-pipeline-skill",
"name": "My pipeline skill",
"description": "Drive the X pipeline end-to-end.",
"category": "pipelines",
"prompt_addendum": "1. ...",
"agents": ["fumi"],
"scope": "agent"
}
slug (1–128 chars) and name are required. prompt_addendum becomes the skill's body. category is derived from the slug when omitted. A tier cap on custom_skills applies. 409 if the slug is already taken by any loadable node, builtin or custom.
Update a skill
PATCH /v1/skills/{slug}
Partial update on a custom node only — builtin skills (meta.is_builtin = true) return 400, since they're ingested from the graph and not portal-editable. Patchable: name (re-rendered into title), description, category, prompt_addendum (becomes the new body), and the assignment fields scope/agent(s)/always_load.
Delete a skill
DELETE /v1/skills/{slug}
Removes a custom loadable node. Builtin skills return 400. Agents lose the load_skill <slug> capability immediately; a run that already loaded the skill's body carries on with what it has.
Errors
| Status | When |
|---|---|
| 400 | Attempted edit/delete of a builtin skill; missing/empty name on create or update; a field that must be a list (tools, dependencies) wasn't one. |
| 404 | Slug doesn't exist. |
| 409 | Slug collision on create. |
| 502 | CP couldn't reach the runtime. |
There is no 422 validation_error on this resource.
Events
Skill CRUD isn't currently emitted as outbox events — reserved for a later release.