Skip to content

Manage Skills ​

In this tutorial you will create a skill, add a version holding its guidance, and publish it so the assistant can find and load it.

A skill is an identity — a name, a URL-safe slug, a description — whose guidance lives on a version. Splitting the two means the text can be revised without changing what anything else refers to.

Authoring one is a three-call sequence, and all three are required. A skill that stops after the first or second exists but is invisible to the assistant, and nothing reports an error, because nothing has gone wrong yet — the sequence is simply incomplete.

Prerequisites ​

  • A Chainabit account with a valid access token
  • Membership in the workspace the skill belongs to
  • curl available in your terminal
bash
export BASE_URL="https://api.chainabit.com/api/v1"
export TOKEN="your-access-token"
export WORKSPACE_ID="your-workspace-id"

Skills are workspace-scoped. Every endpoint below lives under the workspace, and a skill is visible to the assistant in that workspace — not to one specific agent. You do not need an agent to write a skill.


Skill Structure ​

A skill can hold many versions. The catalogue serves the most recently published one; earlier versions stay queryable by id.


Step 1: Create the Skill ​

Creates the identity. No guidance content yet.

bash
curl -s -X POST "$BASE_URL/workspaces/$WORKSPACE_ID/ai/skills" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Activity Summarizer",
    "slug": "activity-summarizer",
    "description": "Generates natural-language summaries from activity completion data.",
    "visibility": "workspace"
  }'
javascript
const res = await fetch(
  `${BASE_URL}/workspaces/${WORKSPACE_ID}/ai/skills`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      name: 'Activity Summarizer',
      slug: 'activity-summarizer',
      description:
        'Generates natural-language summaries from activity completion data.',
      visibility: 'workspace',
    }),
  },
);
const { data } = await res.json();
python
import requests

res = requests.post(
    f"{BASE}/workspaces/{WORKSPACE_ID}/ai/skills",
    headers={"Authorization": f"Bearer {TOKEN}"},
    json={
        "name": "Activity Summarizer",
        "slug": "activity-summarizer",
        "description": "Generates natural-language summaries from activity completion data.",
        "visibility": "workspace",
    },
)
data = res.json()["data"]

slug must be lowercase alphanumeric with hyphens (^[a-z0-9]+(?:-[a-z0-9]+)*$).

A new skill starts at status: "draft". Draft skills are invisible to the assistant by design — this is the state in which you are still writing.

bash
export SKILL_ID="the id returned above"

Step 2: Add a Version With the Guidance ​

The version carries the skill body as Markdown.

bash
curl -s -X POST \
  "$BASE_URL/workspaces/$WORKSPACE_ID/ai/skills/$SKILL_ID/versions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "versionTag": "1.0.0",
    "skillContent": "# Activity Summarizer\n\nSummarize completion data in two sentences..."
  }'
javascript
const res = await fetch(
  `${BASE_URL}/workspaces/${WORKSPACE_ID}/ai/skills/${SKILL_ID}/versions`,
  {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      versionTag: '1.0.0',
      skillContent:
        '# Activity Summarizer\n\nSummarize completion data in two sentences...',
    }),
  },
);
const { data } = await res.json();
python
res = requests.post(
    f"{BASE}/workspaces/{WORKSPACE_ID}/ai/skills/{SKILL_ID}/versions",
    headers={"Authorization": f"Bearer {TOKEN}"},
    json={
        "versionTag": "1.0.0",
        "skillContent": "# Activity Summarizer\n\nSummarize completion data in two sentences...",
    },
)
data = res.json()["data"]

Content is sanitized (HTML tags stripped) and capped at 8 KB; anything longer is truncated rather than rejected. versionTag must be unique per skill — reusing one returns 409 Conflict.

bash
export VERSION_ID="the version id returned above"

Step 3: Publish the Version ​

Publishing is what makes the skill reachable. It marks the version published and moves the parent skill from draft to active, in the same transaction.

bash
curl -s -X POST \
  "$BASE_URL/workspaces/$WORKSPACE_ID/ai/skills/$SKILL_ID/versions/$VERSION_ID/publish" \
  -H "Authorization: Bearer $TOKEN"

After this call the skill appears in the assistant's catalogue immediately. No further call is required — in particular, no PATCH to set status.


How Visibility Is Decided ​

The assistant reads skills through two queries, and both require the same two conditions at once:

  • the skill's status is active, and
  • the skill has at least one version with is_published: true

One without the other yields nothing. This is why publishing writes both in a single transaction: a published version on a draft skill, and an active skill with no published version, are equally invisible.

QueryReturnsUsed for
Workspace catalogueName, slug, description — no contentThe [Available Skills] block offered every turn
Skill by nameThe full latest published version, including contentLoading one skill on demand

The catalogue deliberately excludes skill content. Names and descriptions are cheap enough to offer every turn; bodies are not, so they are fetched only for the skill actually chosen. This is why the description matters more than it looks — it is what the assistant reads when deciding whether to load the skill at all.


Skill Lifecycle ​

StatusMeaningVisible to the assistant
draftBeing authoredNo
activePublished and in useYes, with a published version
deprecatedSuperseded, retained for historyNo
archivedRetiredNo

Publishing a version promotes draft to active. It does not move a deprecated or archived skill back — those are deliberate decisions, and republishing is not a request to undo one. To bring such a skill back, PATCH its status explicitly.


Publishing a Revision ​

Repeat steps 2 and 3 with a new versionTag. The skill stays active throughout, and the catalogue serves the most recently published version.


Skills From the Marketplace ​

Skills installed from the plugin marketplace arrive active with a published version already attached, so they are usable immediately and need none of the steps above. The sequence on this page is for skills you author yourself.


Uploading a Skill Bundle ​

The three-call sequence above authors a skill from scratch. When you already have one on disk — a SKILL.md plus the scripts and reference files it needs — POST .../skills/upload publishes the whole thing in one call, inside a single transaction. There is no partial state to guard against here: the skill, its version and its files are written together or not at all.

Files are sent as text, not as an archive. Every file type a bundle may ship is a text format, so there is nothing an archive could carry that the allowlist would not already refuse — and parsing archives server-side is a zip-slip and zip-bomb surface this API deliberately does not have. If you are starting from a .zip, unpack it client-side and send the entries.

bash
curl -s -X POST "$BASE_URL/workspaces/$WORKSPACE_ID/ai/skills/upload" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "files": [
      { "path": "SKILL.md", "content": "---\nname: PDF Builder\n---\n\nBuild a PDF..." },
      { "path": "scripts/build.py", "content": "print(1)" }
    ]
  }'

The response names what was created:

json
{
  "data": {
    "skillId": "…",
    "skillVersionId": "…",
    "name": "PDF Builder",
    "slug": "pdf-builder",
    "versionTag": "1.0.0",
    "fileCount": 2,
    "totalBytes": 184,
    "addedVersionToExisting": false
  }
}

What the Bundle Must Contain ​

RuleLimit
A SKILL.md at the bundle rootRequired
File types.md .py .json .txt .css .csv
Files per bundle64
Bytes per file512 KB
Bytes per bundle4 MB

Paths must be relative, forward-slashed, and free of .. segments. A single shared wrapper directory is stripped, so a zip of a my-skill/ folder and a zip made from inside it produce the same bundle.

These are the same ceilings and the same allowlist the plugin marketplace importer enforces — a bundle that installs from GitHub installs from your machine, and the reverse.

Name, Description and Version ​

Taken from the SKILL.md frontmatter when it declares them:

markdown
---
name: PDF Builder
description: Builds PDFs from markdown. Use when asked to produce a document.
version: 1.2.0
tags: [documents, pdf]
---

The request body's name and description override the frontmatter when present. The slug is always derived server-side from the resolved name; it is never accepted from the client.

Re-uploading an Existing Skill ​

An upload whose slug already exists returns 409 Conflict. Send "overwrite": true to publish the bundle as a new version of that skill instead — the previous version stays intact for anything already bound to it, and the parent returns to active if it had been deprecated or archived.

The version tag is chosen server-side: the frontmatter's version when it is free, otherwise the next unused patch. A version tag is unique per skill, and you did not choose the one in the file you downloaded, so a collision is not treated as your error.

Provenance ​

An uploaded skill records metadata.source: "upload" and carries no attestation and no validators. Nothing here was fetched from a pinned repository, hash-verified against a published manifest, or signed. A validator declaration decides whether a script runs to adjudicate someone's output, and an uploader asserting one about their own bundle proves nothing — so uploads cannot declare them. Marketplace imports can, because their bundles are verified.


Reading a Skill and Its Files ​

GET .../skills/:id/detail returns the skill, its newest version's body, and the bundle's file tree in one response:

bash
curl -s "$BASE_URL/workspaces/$WORKSPACE_ID/ai/skills/$SKILL_ID/detail" \
  -H "Authorization: Bearer $TOKEN"

Unlike the catalogue read, this one does not filter to published versions: a draft is a state you are mid-way through and need to be able to open. What a person may inspect and what the assistant may load are different questions.

File content is not included — only descriptors (path, size, digest, kind). A bundle can carry dozens of files totalling megabytes and a reader opens one at a time. Fetch the one you want:

bash
curl -s -G "$BASE_URL/workspaces/$WORKSPACE_ID/ai/skills/$SKILL_ID/versions/$VERSION_ID/file" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "path=scripts/build.py"

The file is addressed by its bundle-relative path, not by a storage key, and the key is looked up from the version row. A client-supplied key would be a request to read an arbitrary object out of shared storage, authorized only by the skill id that happened to accompany it.

Responses are capped at 256 KB and set truncated: true when they stop early, so a short render is never mistaken for a short file.


Filtering the Catalogue ​

GET .../skills accepts:

ParameterValues
searchCase-insensitive substring over name, slug and description
statusdraft active deprecated archived
sourceall mine imported system
tagExact tag match
sortupdated (default) created name
limit1–100, default 20
offsetDefault 0
bash
curl -s -G "$BASE_URL/workspaces/$WORKSPACE_ID/ai/skills" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode "search=pdf" \
  --data-urlencode "source=mine" \
  --data-urlencode "sort=updated"

Each row carries an author object (name, is_you, is_system) and a latest_version. is_you is resolved server-side against your chainer id: the client knows its viewer, not the chainer a skill was authored under, and an account-level skill a colleague wrote must not read as "You" to everyone else in the account.


Endpoint Reference ​

MethodPathPurpose
GET/workspaces/:workspaceId/ai/skillsList, with search/status/source/tag/sort
GET/workspaces/:workspaceId/ai/skills/:idOne skill row
GET/workspaces/:workspaceId/ai/skills/:id/detailSkill + newest version body + file tree
GET/workspaces/:workspaceId/ai/skills/:id/versions/:versionId/file?path=One bundle file's text
POST/workspaces/:workspaceId/ai/skillsCreate the identity
POST/workspaces/:workspaceId/ai/skills/uploadPublish a whole bundle in one call
PATCH/workspaces/:workspaceId/ai/skills/:idUpdate metadata or status
DELETE/workspaces/:workspaceId/ai/skills/:idDelete
GET/workspaces/:workspaceId/ai/skills/:skillId/versionsList versions
GET/workspaces/:workspaceId/ai/skills/:skillId/versions/:versionIdOne version
POST/workspaces/:workspaceId/ai/skills/:skillId/versionsAdd a version
POST/workspaces/:workspaceId/ai/skills/:skillId/versions/:versionId/publishPublish it

Summary ​

You have:

  1. Created a skill — its name, slug and description
  2. Added a version carrying the guidance
  3. Published it, which made it active and visible to the assistant

All three steps are required when you author a skill this way. A skill left at step one or two is not broken — it is a draft, and drafts are invisible on purpose.

If you already have a bundle on disk, POST .../skills/upload does all three in one transactional call instead.

Next Steps ​

Built with purpose.