Skills
A skill is a versioned playbook an agent loads on demand. Skills live as cold_memory graph nodes (type='skill', meta.loadable=true) — built-in ones are ingested from the runtime's SKILL.md files, and custom skills are DB-backed and creatable via the control plane (POST /v1/skills, PATCH /v1/skills/{slug}, DELETE /v1/skills/{slug}). This SDK only wraps the read side.
For the runtime skill contract, see API → Skills.
Auth. Every route in this namespace requires the control plane's
x-catentio-sessionheader, which this SDK never sends. NeitherapiKeymode nor Bearer-session mode authenticates here. See SDKs → Auth: what actually works.
Namespace
catentio.skills — two methods:
catentio.skills.list()
catentio.skills.get(slug)
create/update/delete exist on the control plane but aren't wrapped by this SDK — call the REST API directly for custom skills.
Methods
skills.list
Signature. catentio.skills.list(token?): Promise<{ data: SkillListRow[]; meta: { total: number; builtin: number; custom: number } }>
import { CatentioSaasClient } from '@forjio/catentio-saas-node';
const catentio = new CatentioSaasClient({ session });
const { data: skills } = await catentio.skills.list();
for (const s of skills) {
console.log(s.slug, s.title, s.category, s.is_builtin, s.used_by);
}
Real list-row shape — note there is no body here, only its length:
interface SkillListRow {
slug: string;
title: string;
description: string;
category: string;
path: null; // dead field, always null (kept for shape stability)
body_length: number;
is_builtin: boolean;
used_by: number; // how many other skills declare a `[[slug]]` depends_on link
}
There is no displayName, version, triggers, tools, body, createdAt, or updatedAt field on this row — none of those exist.
skills.get
Signature. catentio.skills.get(slug, token?): Promise<SkillDetail>
const skill = await catentio.skills.get('fewer-permission-prompts');
console.log(skill.body); // full markdown body (parsed, frontmatter stripped)
Real detail shape:
interface SkillDetail {
id: number; // the underlying cold_memory node id
slug: string;
title: string;
description: string;
category: string;
body: string;
body_length: number;
is_builtin: boolean;
always_load: boolean;
status: string;
agent: string | null;
agents: string[];
scope: string | null;
}
There is no version, triggers object, or tools array — skills don't carry a semver, a structured trigger rule, or a declared tool list on this endpoint.
Common patterns
Render the skill catalogue in your own UI
const { data: skills } = await catentio.skills.list();
const cards = skills.map(s => ({
title: s.title,
blurb: s.description,
badge: s.is_builtin ? 'built-in' : 'custom',
}));
Pre-flight: confirm the runtime has the skill you expect
async function requireSkill(slug: string) {
try {
await catentio.skills.get(slug);
} catch (err) {
if (err instanceof CatentioError && err.status === 404) {
throw new Error(`Skill ${slug} is not deployed on this runtime.`);
}
throw err;
}
}
await requireSkill('schedule');
Find unused custom skills
const { data: skills } = await catentio.skills.list();
const unused = skills.filter(s => !s.is_builtin && s.used_by === 0);
Errors
The real error body is FastAPI's {"detail": ...}. Because the wire response never has an error key, the SDK doesn't parse out a structured code/message — you get CatentioError with a generic HTTP_ERROR code and the HTTP reason phrase as the message.
| Status | Cause |
|---|---|
| 401 | Missing/invalid session — this SDK never sends x-catentio-session. |
| 404 | get() on a slug with no loadable skill node. |
Next
- Tools — the capability registry skills reference.
- Agents — the principal that loads skills.
- API → Skills — HTTP-level reference, including custom-skill create/update/delete.