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
curlavailable in your terminal
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.
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"
}'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();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.
export SKILL_ID="the id returned above"Step 2: Add a Version With the Guidance
The version carries the skill body as Markdown.
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..."
}'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();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.
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.
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
statusisactive, 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.
| Query | Returns | Used for |
|---|---|---|
| Workspace catalogue | Name, slug, description — no content | The [Available Skills] block offered every turn |
| Skill by name | The full latest published version, including content | Loading 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
| Status | Meaning | Visible to the assistant |
|---|---|---|
draft | Being authored | No |
active | Published and in use | Yes, with a published version |
deprecated | Superseded, retained for history | No |
archived | Retired | No |
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.
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:
{
"data": {
"skillId": "…",
"skillVersionId": "…",
"name": "PDF Builder",
"slug": "pdf-builder",
"versionTag": "1.0.0",
"fileCount": 2,
"totalBytes": 184,
"addedVersionToExisting": false
}
}What the Bundle Must Contain
| Rule | Limit |
|---|---|
A SKILL.md at the bundle root | Required |
| File types | .md .py .json .txt .css .csv |
| Files per bundle | 64 |
| Bytes per file | 512 KB |
| Bytes per bundle | 4 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:
---
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:
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:
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:
| Parameter | Values |
|---|---|
search | Case-insensitive substring over name, slug and description |
status | draft active deprecated archived |
source | all mine imported system |
tag | Exact tag match |
sort | updated (default) created name |
limit | 1–100, default 20 |
offset | Default 0 |
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
| Method | Path | Purpose |
|---|---|---|
| GET | /workspaces/:workspaceId/ai/skills | List, with search/status/source/tag/sort |
| GET | /workspaces/:workspaceId/ai/skills/:id | One skill row |
| GET | /workspaces/:workspaceId/ai/skills/:id/detail | Skill + newest version body + file tree |
| GET | /workspaces/:workspaceId/ai/skills/:id/versions/:versionId/file?path= | One bundle file's text |
| POST | /workspaces/:workspaceId/ai/skills | Create the identity |
| POST | /workspaces/:workspaceId/ai/skills/upload | Publish a whole bundle in one call |
| PATCH | /workspaces/:workspaceId/ai/skills/:id | Update metadata or status |
| DELETE | /workspaces/:workspaceId/ai/skills/:id | Delete |
| GET | /workspaces/:workspaceId/ai/skills/:skillId/versions | List versions |
| GET | /workspaces/:workspaceId/ai/skills/:skillId/versions/:versionId | One version |
| POST | /workspaces/:workspaceId/ai/skills/:skillId/versions | Add a version |
| POST | /workspaces/:workspaceId/ai/skills/:skillId/versions/:versionId/publish | Publish it |
Summary
You have:
- Created a skill — its name, slug and description
- Added a version carrying the guidance
- Published it, which made it
activeand 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.