> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cominty.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Skills

> Upload a skill with an API key and give it to an agent

A skill is a folder of instructions that an agent loads when a task matches it.
You can upload skills with an API key, the same way an admin does on the
**Skills** page of [platform.cominty.ai](https://platform.cominty.ai). Read
[Custom agents](https://docs.cominty.ai/guides/custom-agents.md#what-an-api-key-can-do) first for what an
API key can and cannot see.

Each snippet has a tab for the [Python SDK](https://docs.cominty.ai/sdk/overview.md), the
[JavaScript SDK](https://docs.cominty.ai/js-sdk/overview.md), and HTTP.

## Write a skill

A skill is a folder whose `SKILL.md` opens with YAML frontmatter, following the
[agentskills.io specification](https://agentskills.io/specification). The folder
can hold supporting files next to it.

```text theme={null}
release-notes/
  SKILL.md
  reference.md
```

```markdown theme={null}
---
name: release-notes
description: Gives the QA greeting. Use when asked for the QA greeting.
license: MIT
metadata:
  author: docs-qa
allowed-tools: Read Write
---
# QA greeter

When the user asks for the QA greeting, reply with exactly `QA-HELLO-4F2A` and nothing else.
```

The `description` is what the agent reads to decide when to load the skill. Say
what the skill does and when to use it.

## Upload it

> [tab] Python SDK
>
> ```python theme={null}
> from pathlib import Path
>
> skill = await client.skills.create(
>     Path("release-notes"),
>     display_name="Release notes",
> )
> print(skill.model_dump_json(indent=2))
> ```

> [tab] JavaScript SDK
>
> ```ts theme={null}
> import { readFile } from 'node:fs/promises'
>
> // The skill's files by path, SKILL.md at the root. The SDK zips them in memory.
> const files = {
>     'SKILL.md': await readFile('release-notes/SKILL.md'),
>     'reference.md': await readFile('release-notes/reference.md'),
> }
> let skill = await client.skills.create(files, { displayName: 'Release notes' })
> console.log(JSON.stringify(skill, null, 2))
> ```

> [tab] HTTP
>
> The request is `multipart/form-data`, and the file is a zip.
>
> ```bash theme={null}
> zip -r release-notes.zip release-notes
>
> curl -X POST "https://ds.cominty.com/skills" \
>   -H "x-cominty-token: $COMINTY_API_KEY" \
>   -F display_name="Release notes" \
>   -F scope=private \
>   -F "file=@release-notes.zip;type=application/zip"
> ```
>
> The answer is **201**.

```json theme={null}
{
  "id": "apqy_zTDwaGR",
  "created_at": "2026-11-04T11:21:54.699485Z",
  "updated_at": "2026-11-04T11:21:54.699485Z",
  "display_name": "Release notes",
  "active": true,
  "scope": "private",
  "source": "custom",
  "frontmatter": {
    "name": "release-notes",
    "description": "Gives the QA greeting. Use when asked for the QA greeting.",
    "license": "MIT",
    "compatibility": null,
    "metadata": {"author": "docs-qa"},
    "allowed_tools": ["Read", "Write"]
  }
}
```

* `id` is what an agent's `include` list takes.
* `scope` is `"private"` (the key owner only, the default) or `"organization"`.
* `source` is `"custom"` for a skill you uploaded and `"cominty"` for a built-in
  one.
* `allowed_tools` comes from the space-separated `allowed-tools` field.

### What `file` can be

The Python SDK accepts three inputs.

| Input | What happens |
| - | - |
| A folder `Path` | Zipped in memory and sent as `<folder name>.zip`. `SKILL.md` must be at its root, which the SDK checks. Entries inside the folder whose name starts with `.` are skipped. |
| A `.zip` `Path` | Sent as is. |
| `bytes` | Sent as is. It must be a zip archive. |

A plain `str` raises `InvalidParams`. Wrap a path in `Path(...)`.

The JavaScript SDK takes no path. It accepts a zip (`Blob`, `File`,
`ArrayBuffer`, or `Uint8Array`), sent as is, or an object of the skill's files
by `/`-separated path, such as `{ 'SKILL.md': '...', 'reference.md': bytes }`.
The SDK zips that object in memory. It must hold `SKILL.md` at its root, and
entries with a path part that starts with `.` are left out. A string throws
`InvalidParams`. See [`client.skills`](https://docs.cominty.ai/js-sdk/reference#client-skills).

The server accepts two zip layouts:

* `SKILL.md` at the root of the archive.
* One top-level folder that holds `SKILL.md`, and nothing else at the root.

Any other layout returns 422, and `detail` starts with `Invalid zip structure.`
A missing or invalid frontmatter also returns 422, with the reason.

## Give it to an agent

Put the skill's `id` in a `SkillsCapability`.

> [tab] Python SDK
>
> ```python theme={null}
> from cominty_sdk import AgentCapabilities, SkillsCapability
>
> agent = await client.agents.create(
>     name="Greeter",
>     capabilities=AgentCapabilities(
>         skills=SkillsCapability(activation="always", include=[skill.id]),
>     ),
> )
> run = await client.chat.start(agent_id=agent.id, message="Give me the QA greeting.")
> print((await run.result()).content)   # QA-HELLO-4F2A
> ```

> [tab] JavaScript SDK
>
> ```ts theme={null}
> const agent = await client.agents.create({
>     name: 'Greeter',
>     capabilities: {
>         skills: { activation: 'always', include: [skill.id] },
>     },
> })
> const run = await client.chat.start({ agentId: agent.id, message: 'Give me the QA greeting.' })
> console.log((await run.result()).content) // QA-HELLO-4F2A
> ```

> [tab] HTTP
>
> ```bash theme={null}
> curl -X POST "https://ds.cominty.com/agents" \
>   -H "x-cominty-token: $COMINTY_API_KEY" \
>   -H "Content-Type: application/json" \
>   -d '{
>     "name": "Greeter",
>     "capabilities": {
>       "skills": {"activation": "always", "include": ["SKILL_ID"]}
>     }
>   }'
> ```

The agent loads the skill, then follows it. An `include` id that the key cannot
read makes the creation fail with 422. For the other ways to narrow skills, see
[Capabilities](https://docs.cominty.ai/guides/capabilities.md). Skills have no per-message scope.

## Manage a skill

> [tab] Python SDK
>
> ```python theme={null}
> skills = await client.skills.list()
>
> skill = await client.skills.update(
>     skill.id, display_name="Release notes v1", active=False, scope="organization"
> )
> skill = await client.skills.update(skill.id, active=True)
>
> skill = await client.skills.upload_version(skill.id, Path("release-notes"))
> print(skill.frontmatter.description, skill.updated_at)
>
> await client.skills.delete(skill.id)
> ```

> [tab] JavaScript SDK
>
> ```ts theme={null}
> const skills = await client.skills.list()
>
> skill = await client.skills.update(skill.id, {
>     displayName: 'Release notes v1',
>     active: false,
>     scope: 'organization',
> })
> skill = await client.skills.update(skill.id, { active: true })
>
> skill = await client.skills.uploadVersion(skill.id, files)
> console.log(skill.frontmatter.description, skill.updated_at)
>
> await client.skills.delete(skill.id)
> ```

> [tab] HTTP
>
> ```bash theme={null}
> curl "https://ds.cominty.com/skills" -H "x-cominty-token: $COMINTY_API_KEY"
>
> curl -X PUT "https://ds.cominty.com/skills/$SKILL_ID" \
>   -H "x-cominty-token: $COMINTY_API_KEY" \
>   -H "Content-Type: application/json" \
>   -d '{"active": false}'
>
> curl -X POST "https://ds.cominty.com/skills/$SKILL_ID/versions" \
>   -H "x-cominty-token: $COMINTY_API_KEY" \
>   -F "file=@release-notes.zip"
>
> curl -X DELETE "https://ds.cominty.com/skills/$SKILL_ID" \
>   -H "x-cominty-token: $COMINTY_API_KEY"
> ```

* `list` returns the built-in Cominty skills (`source` is `"cominty"`, `scope` is
  null, read-only), the organization skills, and the key owner's skills. A
  private skill of another member is not there.
* `update` sends only the fields you pass: the display name, `active`, and
  `scope`. Passing none raises `InvalidParams`. With `active` set to false, the
  skill stays, and agents stop using it.
* An uploaded version becomes the default. `frontmatter` and `updated_at` then
  describe it. Versions are tagged to the second: two uploads of one skill
  within a second collide and return 500. Space them out.
* `delete` is permanent and removes every version. HTTP answers 200 with a
  `null` body.
* A built-in skill, and a skill the key owner does not own, return 404 when you
  update it, upload a version of it, or delete it.

## Limits and errors

| What happened | SDK | HTTP |
| - | - | - |
| The display name is empty or over 100 characters | `InvalidParams`, before any request | 422 over 100 characters |
| The archive is over 30 MB | `InvalidParams`, before any request | 413 |
| The folder (Python) or the files object (JavaScript) has no `SKILL.md`, or the input is a string | `InvalidParams`, before any request | - |
| The zip layout or the frontmatter is wrong | `APIError` (422) | 422 |
| The file cannot be read | `OSError` (Python) | - |
| Unknown skill, or a built-in one, on `update` or `delete` | `NotFoundError` | 404 |

Do not send a `surface` field over HTTP. The skill takes the key's surface, and
the call returns 400 `Forbidden field`.

## Next steps

> [card] [Custom agents](https://docs.cominty.ai/guides/custom-agents.md)
>
> Create the agent that uses the skill.

> [card] [Sources](https://docs.cominty.ai/guides/sources.md)
>
> Index local files and let an agent search them.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.