ReferenceAPISkills API
Skills API
The server-to-server routes that keep an environment's Skills in step with the ones your system holds: endpoints, request and response fields, going live, listing, deleting, limits, errors and what version 1 promises.
Your system lists an environment's Skills, pushes the ones it keeps, reads them back and deletes the ones it no longer has, with an org API key. No person and no AI model takes part, and nothing needs doing in the Studio after the key is made.
Before you start#
| Key | Created in Settings → API keys, in the same environment as envSlug. Choose Skills only under Access: that key lists, reads, pushes and deletes Skills and reaches nothing else. A Full access key works too |
| Header | The key in x-api-key |
| Callers | Servers only. Keep the key out of a browser |
| Base path | https://YOUR-MINDSET-HOST/api/v1/orgs/{orgSlug}/envs/{envSlug} |
Endpoints#
| Method and path | What it does | Scope |
|---|---|---|
GET /skills | Every Skill in the environment, in byte order of the handle, a page at a time | skills:read |
PUT /skills/{handle} | Makes the Skill at this handle equal the body, then applies live | skills:write |
DELETE /skills/{handle} | Deletes the Skill, with every version | skills:write |
GET /skills/{handle} | The Skill and its newest version, in full | skills:read |
GET /skills/{handle}/versions | Its versions, newest first, a page at a time | skills:read |
GET /skills/{handle}/versions/{version} | One version, in full | skills:read |
A handle is 1 to 64 lowercase letters, digits and hyphens, with no hyphen at either end and no two in a row: invoice-exceptions. Every route names the Skill by its handle.
Keep an environment in step#
A system that holds the Skills an environment should have makes three kinds of call:
GET /skills, followingnextCursorto the last page, for the handles the environment has now.PUT /skills/{handle}for each Skill the system holds, withliveas it should be. A Skill that hasn't changed answersunchangedand adds nothing, so pushing every one on every run is safe.DELETE /skills/{handle}for each handle the environment has that the system doesn't.
From a SKILL.md folder#
A Skill in the Agent Skills format is a folder: a SKILL.md that opens with frontmatter between two --- lines, and other files beside it.
| In the folder | In the push |
|---|---|
The frontmatter's name | The handle, in the path |
The frontmatter's description, license, compatibility, metadata and allowed-tools | description, license, compatibility, metadata and allowedTools |
| Everything after the line that closes the frontmatter, the blank line that usually follows it included | instructions, exactly as it is |
| Every other file, by its path from the folder | files, each path to its text |
| Nothing | name, a display name you choose, such as the first heading. Agents see it beside the description |
For example, this folder:
invoice-exceptions/
SKILL.md
references/tolerances.md
scripts/check_totals.pywith this SKILL.md:
---
name: invoice-exceptions
description: Use when an invoice does not match its purchase order.
license: Proprietary
metadata:
owner: finance-ops
---
# Invoice exceptions
Flag any line over 5% of the agreed price.is this push, to PUT /skills/invoice-exceptions. The instructions start with the blank line that follows the frontmatter, and the script is left out:
{
"name": "Invoice exceptions",
"description": "Use when an invoice does not match its purchase order.",
"license": "Proprietary",
"metadata": { "owner": "finance-ops" },
"instructions": "\n# Invoice exceptions\n\nFlag any line over 5% of the agreed price.\n",
"files": { "references/tolerances.md": "| Category | Tolerance |\n|---|---|\n| Goods | 2% |\n" },
"live": true
}Leave out files that are programs: anything under scripts/, and any file ending .py, .sh, .bash, .zsh, .js, .mjs, .cjs, .ts, .rb, .pl, .php, .ps1, .bat, .cmd or .exe. Mindset doesn't run them, and refuses a push that holds one with reason scripts_not_supported.
Push a Skill#
curl -X PUT "https://YOUR-MINDSET-HOST/api/v1/orgs/acme/envs/production/skills/invoice-exceptions" \
-H "x-api-key: $SKILLS_API_KEY" \
-H "content-type: application/json" \
-d '{
"name": "Invoice exceptions",
"description": "Use when an invoice does not match its purchase order.",
"instructions": "# Invoice exceptions\n\nCompare the invoice lines with the purchase order. Flag any line over 5% of the agreed price.",
"files": { "references/tolerances.md": "| Category | Tolerance |\n|---|---|\n| Freight | 10% |\n" },
"live": true
}'The call is declarative: send the whole Skill as it should be.
| Before the call | What happens | Status | outcome |
|---|---|---|---|
| No Skill at the handle | The Skill is created at version 1 | 201 | created |
| A Skill whose newest version differs | One version is added | 200 | updated |
| A Skill whose newest version is this document | Nothing is added | 200 | unchanged |
So a retried call never creates a second Skill or a second version. A different name renames the Skill and adds no version.
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | The display name, 1 to 255 characters. Every agent the Skill is attached to sees it beside the description, and the Studio shows it |
description | string | yes | When the Skill applies, up to 1,024 characters. An agent reads it to decide whether to load the Skill |
instructions | string | yes | The body of the Skill's SKILL.md |
files | object | no | Other text files, by path relative to the Skill's root: { "references/tolerances.md": "…" }. No scripts, no binary files, no SKILL.md |
license | string | no | Up to 500 characters |
compatibility | string | no | Up to 500 characters |
metadata | object | no | Up to 50 entries, each key up to 64 characters and each value up to 500 |
allowedTools | string | no | Kept as written. It has no effect in Mindset |
live | boolean | yes | true makes this version the one agents load. false stores it and leaves the live version as it was |
The body is strict: a field not in this table is refused, and the refusal names it. Instructions and files are stored byte for byte as sent, line endings and trailing spaces included.
Going live
live is required on every push. With true, the pushed version becomes the one agents load in the same step, and the version that was live stops being used. A Skill never has more than one live version, even when two pushes arrive at the same moment. With false, the version is stored and nothing agents load changes: a version that was live stays live, and a Skill with none stays unused. Push the same document again with true when it should go live.
Going live is refused when an agent the Skill is attached to already holds 50 published Skills, since this would make one more. An org admin attaches Skills to agents through the mindset-admin MCP server; this API doesn't.
Conditions
| Header | Effect |
|---|---|
If-None-Match: * | Create only. A 412 if a Skill already holds the handle |
If-Match: "3" | Store the push, or make the delete, only while version 3 is still the newest. A 412 if it has moved |
Every read of a Skill and every push returns the newest version as the ETag, ready for If-Match. Writes to one Skill are taken one at a time, from this API and the mindset-admin MCP server alike.
Response
| Field | What it is |
|---|---|
outcome | created, updated or unchanged |
skill | The Skill: id, handle, name, status (published while a version is live, draft when none is), activeVersion (the live version, or null), createdBy, createdAt, updatedAt |
version | The version the pushed document is |
live | Whether that version is the one agents load now |
warnings | Advice the content earned, such as instructions longer than the Agent Skills specification recommends. The push still succeeded |
Read a Skill#
GET /skills/{handle} and GET /skills/{handle}/versions/{version} return:
| Field | What it is |
|---|---|
skill | The Skill, as in a push's response |
version | The version's record: version, description, author and authorLabel (who wrote it), approvedBy, approvedByLabel and approvedAt (who last made it live, and when), isActive, totalBytes, createdAt |
document | Everything the version holds, in the shape a push takes. Its name is the Skill's name now, since a name isn't versioned |
Read document, change it, and push it back with live to make a change.
GET /skills/{handle}/versions returns versions, newest first, and nextCursor. Pass ?limit= for the page size (50 unless you ask, at most 200) and ?cursor= with the last page's nextCursor for the versions before it. nextCursor is null on the last page.
List Skills#
GET /skills returns skills, in byte order of the handle (- before digits, digits before letters), and nextCursor. Each entry is the Skill, as in a push's response, with newestVersion: the newest version's number, live or not. Pass ?limit= for the page size (50 unless you ask, at most 200; a larger number reads 200 and the answer's limitClamped says so) and ?cursor= with the last page's nextCursor for the Skills after it. nextCursor is null on the last page. A cursor is opaque: pass it back as you got it.
A Skill created or deleted while you page moves no other Skill between pages. A deleted Skill isn't listed, and neither is another environment's.
Delete a Skill#
curl -X DELETE "https://YOUR-MINDSET-HOST/api/v1/orgs/acme/envs/production/skills/invoice-exceptions" \
-H "x-api-key: $SKILLS_API_KEY"A 200 returns deleted: true and skill, the Skill as it was. It is gone with every version: agents stop loading it, and it is no longer read or listed. Its handle is free, so a later push to it creates a new Skill at version 1. A delete is recorded in the org's audit trail, naming the key, and isn't undone through this API.
When agents hold the Skill, the first delete removes nothing. It answers 409 has_dependents, with the agents in context.dependents and a context.fingerprint for that list. Show the list to whoever decides, and to delete anyway, repeat the call with ?confirmDependents= and the fingerprint. If the agents holding it have changed since, the answer is 409 dependents_changed with the current list and a new fingerprint. A Skill no agent holds is deleted on the first call.
With If-Match: "3", the Skill is deleted only while version 3 is still its newest; if a newer version has been pushed since you read it, the answer is 412 precondition_failed and nothing is deleted.
A delete where there is no Skill, a retried delete that already succeeded among them, answers 404.
Limits#
| What | Limit |
|---|---|
| One version, instructions and files together, as stored | 4 MiB (4,194,304 bytes) |
| The request body | 16 MiB (16,777,216 bytes), so a Skill at its limit fits once escaped as JSON |
| Files in one version | 10,000 |
| A file's path | 512 characters |
A Skill over a limit is refused naming the limit and its size. Nothing is stored.
Errors#
Every error body is JSON with a code, a message, and often a context. An invalid Skill is refused once, with every problem in context.problems, each with its field and reason (up to 20, and context.problemsTotal when there are more).
| What happened | Status | code |
|---|---|---|
A body that isn't JSON, misses a field, carries one this API doesn't define, or breaks a rule, including text that can't be stored as sent (a NUL character, or half of an emoji); or a limit, cursor or confirmDependents that isn't valid, or a query parameter the route doesn't define (named in the message) | 400 | validation_error |
| A handle that isn't a valid handle | 400 | invalid_handle |
No key in x-api-key | 401 | unauthorized |
| A key that is unknown, revoked or expired | 401 | invalid_api_key |
| A push or delete with a key that can read Skills but not write them | 403 | unauthorized |
| A suspended org | 403 | org_suspended |
| No such Skill or version | 404 | not_found |
| The org or environment in the path isn't the key's own, or the key holds no Skills scope | 404 | not_found |
| Another change to the same Skill, made at the same moment, got there first. Read it again and retry | 409 | version_conflict |
A delete of a Skill agents hold, without confirmDependents | 409 | has_dependents |
A delete whose confirmDependents no longer matches the agents holding the Skill | 409 | dependents_changed |
An If-None-Match or If-Match condition that didn't hold | 412 | precondition_failed |
An If-Match that names no version, or If-None-Match on a delete | 400 | validation_error |
| A body over 16 MiB | 413 | payload_too_large |
The 404s for a wrong org, environment or key look the same on purpose, so nobody holding a key can learn which orgs or environments exist.
What version 1 promises#
Within /api/v1, changes only add: new routes, new optional request fields, new response fields and new error codes. Ignore response fields you don't know, and treat an unknown code by its status. Nothing here is removed, renamed or narrowed within v1. A change that would break a caller ships as /api/v2 beside /api/v1, which keeps answering through a declared deprecation window and says so in Deprecation and Sunset headers.
Every push and every delete is recorded in the org's audit trail, naming the key, and every call is counted against the key that made it.
The OpenAPI 3.1 document for these routes, generated from the code that serves them, is customer-v1.json.