Skills API
Import, inspect, update, delete, and test sandbox skills
A skill is a sandboxed capability package an Agent can call: it is installed as a package from a ZIP archive or a Git repository, is owned either by a team (team_id set) or by the platform (team_id is null, i.e. a system skill), and it executes inside the code sandbox. Base path: /api/v1/skills.
Prerequisites and authentication
All endpoints require an authenticated JWT user session (Authorization: Bearer <token>); API-key authentication is not accepted.
Permission codes are the feature-level switch; team ownership is enforced separately: GET list/detail, PATCH, DELETE, and POST /test all require the caller to be a member of the owning team, and update/delete/test additionally require OWNER/ADMIN of that team. For system skills (team_id=null), update/delete/test require a superuser.
Skills can only be installed through the import flow: there is no create-from-scratch endpoint and no /files-style skill file management endpoint.
Endpoints
| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/skills | List the skills available to a team (system + team, unpaginated) | skill:read |
| POST | /api/v1/skills/import/preview-zip | Upload a ZIP, scan it, and start an import session | skill:create |
| POST | /api/v1/skills/import/preview-git | Clone a Git repository, scan it, and start an import session | skill:create |
| POST | /api/v1/skills/import/{session_id}/install | Install/update the packages selected in a session | skill:create |
| GET | /api/v1/skills/{skill_id} | Get skill details, including package contents | skill:read |
| PATCH | /api/v1/skills/{skill_id} | Update skill metadata and default config | skill:update |
| DELETE | /api/v1/skills/{skill_id} | Delete a skill and its private storage | skill:delete |
| POST | /api/v1/skills/{skill_id}/test | Execute a skill once in the sandbox with test arguments | skill:execute |
Authentication and permissions
| Permission | Purpose |
|---|---|
skill:read | List skills, read skill details |
skill:create | Preview imports (ZIP / Git) and install imports |
skill:update | Update skills |
skill:delete | Delete skills |
skill:execute | Test-execute skills |
Team access goes through check_team_access (which checks the team:read permission): a non-superuser must be a member of the target team and hold the global team:read permission; with require_admin=True (update, delete, test) they must also be an OWNER/ADMIN of it.
Skill object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Skill ID |
team_id | string (UUID) | null | Owning team; null means a system skill |
name | string | Stable name, max 100 characters, unique per team (team + name) |
display_name | string | Display name, max 100 characters |
description | string | Description |
icon | string | null | Icon emoji or URL |
category | string | Category (enum below) |
version | string | Version, default 1.0.0 |
source_type | string | Install source: zip, git, manual_text, legacy |
source_uri | string | null | Redacted source URI (Git/imported packages) |
source_ref | string | null | Git ref or resolved revision |
source_subdir | string | null | Source subdirectory scanned during import |
package_path | string | null | Package root path inside the source |
package_hash | string | null | Content hash of the installed package |
input_schema | object | JSON Schema of the arguments exposed to the model |
default_config | object | Defaults merged into an agent's per-tool config |
is_enabled | boolean | Whether the skill can be selected/executed, default true |
is_system | boolean | true when team_id is null |
import_warnings | array of string | Non-blocking import warnings |
created_by_id | string (UUID) | null | Creator ID |
created_by_name | string | null | Creator username |
created_at / updated_at | string (ISO 8601) | Creation and update timestamps |
Skill detail object
The detail endpoints add the package contents to the fields above:
| Field | Type | Description |
|---|---|---|
skill_md | string | Raw SKILL.md content |
instructions | string | Instructions parsed from SKILL.md |
frontmatter | object | SKILL.md frontmatter |
package_manifest | object | Package manifest summary |
execution_config | object | Validated execution configuration |
config_schema | object | JSON Schema of the skill configuration |
Enums
category: file, code, data, web, api, other (default other).
List skills
GET /api/v1/skillsReturns every skill available to the given team, split into a system array and a team array. Not paginated.
Query parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
team_id | string (UUID) | Yes | - | Target team; the caller must be a member (superusers excepted) |
include_system | boolean | No | true | Also return system skills (team_id=null) |
enabled | boolean | No | - | When set, only return skills whose is_enabled matches |
search | string | No | - | Case-insensitive contains match on name, display_name, description |
category | string | No | - | Filter by skill category |
curl -X GET "https://your-domain.com/api/v1/skills?team_id=b7f2c9d4-1a3e-4f5b-8c6d-9e0a1b2c3d4e&include_system=true&enabled=true&search=analysis&category=data" \
-H "Authorization: Bearer YOUR_TOKEN"200 OK:
{
"code": 0,
"data": {
"system": [
{
"id": "5c2f9a10-8b3d-4e6f-9a71-b2c3d4e5f607",
"team_id": null,
"name": "file_read",
"display_name": "File Reader",
"description": "Reads files from the workspace",
"icon": "📄",
"category": "file",
"version": "1.0.0",
"source_type": "zip",
"source_uri": "system-skills.zip",
"source_ref": null,
"source_subdir": null,
"package_path": "/data/skills/system/file_read",
"package_hash": "sha256:1f0c0b7d2a9e4c8f",
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}}},
"default_config": {},
"is_enabled": true,
"is_system": true,
"import_warnings": [],
"created_by_id": null,
"created_by_name": null,
"created_at": "2026-01-10T09:00:00Z",
"updated_at": "2026-01-10T09:00:00Z"
}
],
"team": [
{
"id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"team_id": "b7f2c9d4-1a3e-4f5b-8c6d-9e0a1b2c3d4e",
"name": "data_analysis_skill",
"display_name": "Data Analysis Skill",
"description": "Analyzes tabular CSV datasets and generates statistical charts",
"icon": null,
"category": "data",
"version": "1.0.0",
"source_type": "git",
"source_uri": "https://github.com/example/skills.git",
"source_ref": "9c1f4a7b2d8e3f5061a2b3c4d5e6f70819a2b3c4",
"source_subdir": "data_analysis",
"package_path": "/data/skills/team/data_analysis_skill",
"package_hash": "sha256:8d2e6f1a4b0c9d3e",
"input_schema": {"type": "object", "properties": {"dataset": {"type": "string"}}},
"default_config": {"max_rows": 1000},
"is_enabled": true,
"is_system": false,
"import_warnings": [],
"created_by_id": "3d1f7a92-5b6c-4f8e-9a01-2c3d4e5f6a7b",
"created_by_name": "alice",
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-01T12:00:00Z"
}
]
},
"msg": "success"
}Omitting team_id returns 422 + 1001 (FastAPI validation failure), not an empty list. The envelope is data.system / data.team with no total / page fields, and it does not use an {items: []} shape.
Errors:
| HTTP | Code | Meaning |
|---|---|---|
422 | 1001 | Required team_id missing |
404 | 4004 | team_not_found |
403 | 3000 | Missing skill:read permission |
403 | 3002 | not_team_member |
Import flow
Installing a skill is a two-step process: preview (create a short-lived import session) then install (choose packages from the session). Import sessions live for 1 hour; after that, install returns 400 + 1002 (skill_import_session_expired). The install request only needs package paths — team_id and the source come from the session.
Preview a ZIP import
POST /api/v1/skills/import/preview-zipUpload a ZIP as multipart/form-data; the server scans it for skill packages.
| Form field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | ZIP archive (.zip); other extensions are rejected |
team_id | string (UUID) | No | Team that will own the imported skills; the caller must be an OWNER/ADMIN of it. Omit to import system skills, which is superuser-only |
Limits: archive at most 50 MB, at most 500 files, expanded size at most 50 MB, and any single file at most 10 MB.
curl -X POST "https://your-domain.com/api/v1/skills/import/preview-zip" \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "team_id=b7f2c9d4-1a3e-4f5b-8c6d-9e0a1b2c3d4e" \
-F "file=@skills.zip"Preview a Git import
POST /api/v1/skills/import/preview-git| Field | Type | Required | Description |
|---|---|---|---|
team_id | string (UUID) | null | No | Same as the ZIP preview |
repo_url | string | Yes | Repository URL, 1–2000 characters; validated before cloning |
ref | string | null | No | Branch / tag / commit, max 255 characters; defaults to the repository default branch |
The clone times out after 180 seconds; a timeout or an invalid URL returns 400 + 1002 (skill_git_url_invalid).
curl -X POST "https://your-domain.com/api/v1/skills/import/preview-git" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"team_id":"b7f2c9d4-1a3e-4f5b-8c6d-9e0a1b2c3d4e","repo_url":"https://github.com/example/skills.git","ref":"main"}'Preview response
Both previews return the same shape:
{
"code": 0,
"data": {
"session_id": "f0e1d2c3-b4a5-4968-8778-99aabbccddee",
"source_type": "zip",
"source_uri": "skills.zip",
"source_ref": null,
"source_subdir": null,
"skills": [
{
"package_path": "data_analysis_skill",
"name": "data_analysis_skill",
"display_name": "Data Analysis Skill",
"description": "Analyzes tabular CSV datasets",
"version": "1.0.0",
"category": "data",
"icon": null,
"valid": true,
"errors": [],
"warnings": [],
"conflict": null,
"file_count": 4,
"package_hash": "sha256:8d2e6f1a4b0c9d3e"
}
],
"invalid": [],
"warnings": []
},
"msg": "success"
}Preview item fields
| Field | Type | Description |
|---|---|---|
package_path | string | Package path relative to the archive/repository root |
name / display_name | string | null | Skill name and display name |
description | string | Description |
version | string | Version, default 1.0.0 |
category | string | Category |
icon | string | null | Icon |
valid | boolean | false means the package cannot be installed |
errors | array of string | i18n message keys explaining why, e.g. skill_md_not_found |
warnings | array of string | Message keys such as skill_name_conflict, skill_duplicate_name_in_source |
conflict | object | null | {"type": "existing_team_skill", "skill_id": ..., "message": ...} when a same-named skill already exists in the target team |
file_count | integer | Files in the package |
package_hash | string | null | Package content hash |
Packages with valid: false appear in invalid instead of skills; errors / warnings entries are i18n message keys that the client must translate.
Install an import
POST /api/v1/skills/import/{session_id}/installPath parameters
| Parameter | Type | Description |
|---|---|---|
session_id | string (UUID) | The session ID returned by a preview route |
Request body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
items | array | No | [] | Packages to install; empty installs nothing |
items[].package_path | string | Yes | - | Must match a package_path in the preview |
items[].action | string | No | install | install, update, or skip |
items[].skill_id | string | null (UUID) | No | - | Explicit target for update; must belong to the session's team |
is_enabled | boolean | No | true | is_enabled applied to the installed/updated skills |
Package selection rules:
install: fails for that package (skill_name_exists) when the target team already has a skill with the same name.update: uses the explicitskill_idfirst; otherwise matches a team skill by name; if neither exists the whole request fails with404+4000(skill_not_found).skip: records thepackage_pathwithout touching any skill.
{
"items": [
{"package_path": "data_analysis_skill", "action": "install"},
{"package_path": "charts_skill", "action": "update", "skill_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"}
],
"is_enabled": true
}200 OK:
{
"code": 0,
"data": {
"installed": ["a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"],
"updated": [],
"skipped": ["charts_skill"],
"errors": ["other_skill: skill_name_exists"]
},
"msg": "Skills imported successfully"
}| Field | Type | Description |
|---|---|---|
installed | array of UUID | IDs of newly created skills |
updated | array of UUID | IDs of updated skills |
skipped | array of string | Skipped package_path values |
errors | array of string | Per-package errors as "<package_path>: <message_key>" |
Per-package failures do not change the HTTP status: even with a non-empty errors, the response is 200 OK + code: 0. Clients must inspect data.errors; judging success solely by HTTP status misses failures.
Get skill
GET /api/v1/skills/{skill_id}Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
team_id | string (UUID) | No | Team context. A team skill always requires membership of its owning team; for a system skill, passing team_id requires membership of that team, while omitting it performs no team check |
200 OK: data is the skill detail object.
Errors:
| HTTP | Code | Meaning |
|---|---|---|
404 | 4000 | skill_not_found |
404 | 4004 | team_not_found |
403 | 3000 | Missing skill:read permission |
403 | 3002 | not_team_member |
Update skill
PATCH /api/v1/skills/{skill_id}Only the fields present in the body are applied.
Request body
| Field | Type | Description |
|---|---|---|
display_name | string | null | Display name, 1–100 characters |
description | string | null | Description |
icon | string | null | Icon, max 100 characters |
category | string | null | Category |
is_enabled | boolean | null | Enable or disable the skill |
default_config | object | null | Default configuration |
200 OK: data is the updated skill detail object.
Errors:
| HTTP | Code | Meaning |
|---|---|---|
404 | 4000 | skill_not_found |
403 | 3000 | Missing skill:update permission, or a system skill without a superuser |
403 | 3002 / 3003 | Not a team member / not a team OWNER-ADMIN |
Delete skill
DELETE /api/v1/skills/{skill_id}Deletes the skill row and its private package storage. A skill still referenced by an agent's tools_config cannot be deleted.
200 OK: data: null.
Errors:
| HTTP | Code | Meaning |
|---|---|---|
404 | 4000 | skill_not_found |
400 | 1002 | skill_referenced_by_agent: still referenced by an agent |
403 | 3000 | Missing skill:delete permission, or a system skill without a superuser |
403 | 3002 / 3003 | Not a team member / not a team OWNER-ADMIN |
Test skill
POST /api/v1/skills/{skill_id}/testExecutes the skill once in the code sandbox. Test privileges match update: superuser for system skills, team OWNER/ADMIN for team skills.
Request body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
arguments | object | No | {} | Arguments passed to the skill |
config | object | No | {} | Configuration overrides for this run |
200 OK:
{
"code": 0,
"data": {
"success": true,
"result": {"rows": 1200, "columns": 8},
"error": null,
"stdout": "processed sales.csv\n",
"stderr": "",
"artifacts": [
{
"path": "/workspace/report.html",
"optional": false,
"description": "Generated report",
"file_type": "file",
"size": 20480,
"checksum": "sha256:...",
"content_type": "text/html",
"storage_path": "skills/artifacts/report.html",
"url": null,
"filename": "report.html"
}
],
"duration_ms": 842
},
"msg": "success"
}| Field | Type | Description |
|---|---|---|
success | boolean | Whether the run succeeded |
result | any | Skill return value |
error | string | null | Failure reason |
stdout / stderr | string | Captured standard output / error |
artifacts | array | Run artifacts (see the sandbox artifact shape in the File Uploads API) |
duration_ms | integer | null | Run duration in milliseconds |
A failed skill run does not return a non-2xx status: the response is still 200 OK + code: 0, with the failure in data.success: false and data.error. Judging success solely by HTTP status treats an execution failure as success.
Errors:
| HTTP | Code | Meaning |
|---|---|---|
404 | 4000 | skill_not_found |
403 | 3000 | Missing skill:execute permission, or a system skill without a superuser |
403 | 3002 / 3003 | Not a team member / not a team OWNER-ADMIN |
Error handling
| HTTP | Code | Trigger |
|---|---|---|
401 | 2000 / 2001 / 2002 | Missing, invalid, or expired token |
422 | 1001 | Request validation failed: missing team_id, unknown category, display_name length out of range, malformed UUID |
400 | 1002 | Invalid ZIP/Git source, archive over limits, expired session, skill_referenced_by_agent |
403 | 3000 | Missing permission code, or a system skill without a superuser (skill_system_admin_required) |
403 | 3002 / 3003 | not_team_member / team_admin_required |
404 | 4000 / 4004 | Skill not found / team not found |
400 | 1003 | Internal server error |
Per-package errors from preview and install appear only in the response's errors field and are never expressed through the HTTP status.
Related
- Skills — installing, enabling, and using skills in Agents
- Agents API — referencing a skill from an agent's
tools_config - File Uploads API — sandbox artifacts and file parsing
- Error Handling — recover by HTTP status and business error code
How is this guide?