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

# Skills

> Versioned capabilities: instructions, tools and knowledge bases defined once

A skill is what an agent needs for one task: a description of when to use it, the instructions to follow, the tools it calls and the knowledge bases it reads. You define it once in a project and version it the way you version an agent.

> [!NOTE]
>
> Skills load on cascade voice calls and in text conversations: the Chat API, SMS and WhatsApp text. Speech-to-speech agents ignore them.

## Create a skill

```bash theme={null}
curl -X POST http://localhost:8090/v1/skills \
  -H "Authorization: Bearer tc_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "verify_identity",
    "description": "Use before discussing anything account-specific: balance, bills, payments.",
    "instructions": "Ask for the date of birth and the last 4 digits of the account number. Call verify_customer with both. Never reveal an account detail before it returns verified=true.",
    "tools": [{
      "name": "verify_customer",
      "description": "Verify the caller against the account",
      "parameters_schema": {
        "type": "object",
        "properties": {"dob": {"type": "string"}, "last4": {"type": "string"}},
        "required": ["dob", "last4"]
      },
      "webhook_url": "https://crm.example.com/tools/verify",
      "webhook_secret": "a-secret-of-16-or-more-characters"
    }],
    "knowledge_bases": []
  }'
```

The response is version 1, a `draft`. Creating a skill with a name the project already has makes the next version of that skill, also a draft.

| Field | |
| - | - |
| `name` | Lowercase letters, digits and underscores, starting with a letter or `_` (the shape of a tool name), at most 64 characters. Shared by every version of the skill, so it cannot be edited |
| `description` | When to use the skill, 1-1024 characters. The model chooses a skill by it |
| `instructions` | What to do once the skill is in use, 1-128,000 characters |
| `tools` | [Custom webhook tools](https://docs.turncall.io/guides/tools.md), in the agent's tool schema and under the agent's rules: a `webhook_url` (https outside development), an optional `webhook_secret` to sign each request. Names are unique within the skill, and a built-in's name (`end_call`, `transfer_call`, ...) is refused |
| `knowledge_bases` | Knowledge bases of the project, with the fields of an [agent's link](https://docs.turncall.io/guides/knowledge-base.md): `knowledge_base_id`, `mode`, `top_k`, `similarity_threshold`, `priority`, `tool_description` |

A refusal names what it refused: a tool by its place and name (`tools[1] 'get_invoices' webhook_url: ...`), a knowledge base outside the project by its place and id. What each mode does on a call is in [A skill's knowledge bases](#a-skills-knowledge-bases).

A tool's `webhook_secret` is encrypted at rest and every read returns it as `"***"`, as an agent's is.

## Lifecycle

Skills follow [agent versioning](https://docs.turncall.io/guides/agent-versioning.md):

| Request | |
| - | - |
| `PUT /v1/skills/{skill_id}` | Edit a draft. Send only the fields to change: `description`, `instructions`, `tools`, `knowledge_bases`. A published or archived version is a `409` |
| `POST /v1/skills/{skill_id}/publish` | Make a draft the skill's live version. The previously published version of the same name is archived |
| `POST /v1/skills/{skill_id}/rollback` | Make an archived version live again. The current live version is archived |

A published version never changes; to change a live skill, create a new version, edit it, and publish it.

> [!WARNING]
>
> Publishing or rolling back a skill changes every agent that follows its live version, on each agent's next call, without republishing those agents. [Who uses a skill](#who-uses-a-skill) lists them before you publish; [pin a version](#pin-a-version) to hold an agent back. See ADR-0030.

## List and read

```bash theme={null}
curl "http://localhost:8090/v1/skills?name=verify_identity" \
  -H "Authorization: Bearer tc_xxx"
```

The list is newest first in the paginated envelope (`total`, `page`, `limit` up to 200). `name` lists one skill's versions, and `state` (`draft`, `published` or `archived`) filters by state: `?state=published` lists the live version of every skill. `GET /v1/skills/{skill_id}` reads one version.

Skills belong to their project: another project's skill is a `404` on every route.

## Attach skills to an agent

List the skills an agent may use by name in its config:

```json theme={null}
{
  "system_prompt": "You are Maya, the support agent. Be brief and warm.",
  "skills": [{"skill": "verify_identity"}, {"skill": "explain_bill"}]
}
```

Each name resolves to the skill's live published version once, when a call starts, so a publish mid-call does not change a running call, and the next call runs whatever is live then.

### Pin a version

An entry may pin one version of the skill by its id, the `id` a skill read returns:

```json theme={null}
"skills": [
  {"skill": "verify_identity"},
  {"skill": "payment_promise", "version_id": "4a73a31b-d797-4cfc-bf6c-bca5895c928b"}
]
```

A pinned entry runs exactly that version on every call, whatever is published or rolled back later. It still runs once a newer publish archives it; a draft cannot be pinned, since it can still change. The pin is an id rather than a version number because a rollback makes an older number live again, and an id always names the same version. To move a pinned agent to a newer version, change the pin in the agent's config.

Saving the agent (`POST /v1/agents`, `PUT /v1/agents/{agent_id}`) checks its skills against the version each resolves to now, and refuses with a `422` that names both sides:

| Refused | Message |
| - | - |
| A name the project has no skill of | `skills[0] 'nope' is not a skill in this project` |
| A name with only drafts: a call runs published versions only | `skills[1] 'draft_only' has no published version in this project; publish one first` |
| A skill tool named like one of the agent's own tools | `skills[1] 'explain_bill' tool 'get_invoices' collides with the agent's own tool 'get_invoices'` |
| Two listed skills with a tool of the same name | `skills[1] 'payment_promise' tool 'get_invoices' collides with skills[0] 'explain_bill' tool 'get_invoices'` |
| A name listed twice, pinned or not | `skills[1] 'explain_bill' is listed twice (first as skills[0])` |
| A pin naming no version in the project: an id that does not exist, or another project's | `skills[0] 'verify_identity' version_id '<id>' is not a version of a skill in this project` |
| A pin naming another skill's version | `skills[0] 'verify_identity' version_id '<id>' is a version of 'explain_bill', not of 'verify_identity'` |
| A pin naming a draft | `skills[0] 'verify_identity' version_id '<id>' is a draft (v3); pin a published or archived version` |

For a pinned entry the tools checked are the pinned version's, not the live one's. `details` carries the entry (`field`, `skill`; for a pin, `field` is `skills[i].version_id` and `version_id` is set) and, for a collision, `tool` and `collides_with`. `load_skill` is offered automatically, so listing it in the agent's own `tools` is refused too.

A skill version published after the agent was saved is not checked against it: a tool of that version whose name the agent already uses is skipped on the call, as is a skill an [inline call-init agent](https://docs.turncall.io/guides/call-init.md) names that has no published version, or pins a version that is not one of that skill's in the project.

## Who uses a skill

```bash theme={null}
curl http://localhost:8090/v1/skills/verify_identity/agents \
  -H "Authorization: Bearer tc_xxx"
```

```json theme={null}
[
  {"agent_id": "...", "project_id": "...", "agent_name": "collections", "agent_version": 3,
   "agent_state": "published", "skill": "verify_identity", "resolved_skill_id": "...",
   "resolved_version": 1, "pinned": true},
  {"agent_id": "...", "project_id": "...", "agent_name": "support", "agent_version": 7,
   "agent_state": "draft", "skill": "verify_identity", "resolved_skill_id": "...",
   "resolved_version": 2, "pinned": false}
]
```

Every draft and published agent version of the project that lists the skill — and, for a [skill library](#a-skill-library), of every project linked to it — by agent name and version, with its `project_id`, the skill version it resolves to now and `pinned`: for a pinned entry its pinned version, for any other the live published one, or `null` when none is published. This is who a publish or a rollback reaches: every entry with `pinned: false`. An archived agent version, a deleted agent's included, is not listed. A name the project has no version of is a `404`. A project linked to a library lists none of its own skills' agents: its agents read the library's.

## Delete a skill

`DELETE /v1/skills/{skill_id}` deletes one version. While any agent version listed above names the skill, it is a `409` whose `details.agents` names them (`agent_id`, `project_id`, `name`, `version`, `state`), a linked project's agents included, whatever the version deleted, pinned or not: an agent following the live version can be moved to any version of the skill by a publish or rollback, and a pinned one runs its version on every call. Remove the skill from their `skills` first. A deleted version is gone from every read; the calls that ran it keep their record of it.

## Load a skill on a call

Until a skill is loaded the model sees only its name and description, in a section added to the system prompt, and a built-in tool, `load_skill`:

```text theme={null}
You are Maya, the support agent. Be brief and warm.

## Skills you can load
Load a skill with load_skill when the conversation needs it. Its instructions and tools are added when you load it.
- verify_identity: Use before discussing anything account-specific: balance, bills, payments.
- explain_bill: Use when the caller asks why a bill changed.
```

None of the skills' tools are offered yet. When the conversation needs a skill the model calls `load_skill` with `{"name": "verify_identity"}`, and the skill's instructions join the prompt as their own section, its tools join the tool list, and the model answers with them:

```text theme={null}
## Skill: verify_identity (v2)
Ask for the date of birth and the last 4 digits of the account number. ...
```

The conversation so far stays in the context: the caller does not repeat themselves. A load costs one more model response, once per skill per call.

A skill's instructions and its tools' descriptions render the call's [template variables](https://docs.turncall.io/guides/call-init.md#response-fields) (`{{name}}`), as the agent's own prompt does. Its webhook tools run like the agent's own, recorded in [tool invocations](https://docs.turncall.io/guides/tools.md).

When two tools share a name, the first of built-in, the agent's own tools, loaded skills' tools in load order, and MCP tools wins, in what the model is offered and in what runs. A skill tool with the name of one of the agent's tools is skipped.

An agent with no skills is given no section and no `load_skill`. Skills are not loaded on speech-to-speech agents: an S2S agent that lists skills carries a `skills_ignored_on_s2s` entry in its read's `warnings`, and its calls run without them, logging why.

## A skill's knowledge bases

A skill's `knowledge_bases` take effect when it is loaded, by their `mode`:

| Mode | Once the skill is loaded |
| - | - |
| `prompt` | The documents' full text joins the skill's own section, after its instructions |
| `tool` | `query_knowledge` searches them. An agent with its own tool-mode knowledge base already offers it, and the same tool then searches the agent's and every loaded skill's. An agent without one is offered it by the load, with the hint to search before saying no |
| `auto` | Not applied: per-turn retrieval is built with the call, before any skill is loaded. Accepted at save, and every read of the skill carries a `knowledge_mode_unsupported` entry in its `warnings` naming the knowledge bases (`knowledge_base_ids`). `auto` is the default, so name `tool` or `prompt` |

```json theme={null}
"warnings": [
  {
    "code": "knowledge_mode_unsupported",
    "message": "A knowledge base attached to a skill in 'auto' mode is not applied: ...",
    "modes": ["auto"],
    "knowledge_base_ids": ["6f1c..."]
  }
]
```

Before the load neither is in the prompt or searchable through the skill. A skill unloaded to make room (below) takes its documents out of the prompt and its knowledge bases out of the search. The documents are read when the call starts, with the skill, so a load reads nothing; a document added mid-call is in the next call. With several tool-mode knowledge bases, `top_k` and `similarity_threshold` come from the first searched: the agent's own, then the skills' in load order. A tool of the agent's, a skill's or an MCP server's named `query_knowledge` keeps the name, and the skills' knowledge bases are then not searchable.

## How many skills a call holds

`max_loaded_skills` in the agent config caps how many skills a call holds loaded at once: 3 by default, from 1 to 10.

```json theme={null}
{
  "skills": [{"skill": "verify_identity"}, {"skill": "explain_bill"}, {"skill": "payment_promise"}, {"skill": "plan_change"}],
  "max_loaded_skills": 3
}
```

Loading a skill past the cap unloads the one loaded longest ago, in the same switch: its section leaves the prompt and its tools leave the tool list as the new skill's arrive, and the `load_skill` result tells the model which skill went. An unloaded skill stays in the list of skills the model can load, so it can load it again.

Loading a skill that is already loaded changes nothing: the result says it is already loaded, and the prompt and the tools stay as they are.

## Skills across a handoff

When a call is handed to another agent, with the `handoff_to_agent` tool or `POST /v1/calls/{id}/handoff`, the call takes that agent's skills: each resolved as at a call's start (its pinned or live published version in the call's project, rendered with the call's template variables), none loaded. The model is given the target agent's list and `load_skill` when it has skills, and neither when it has none. Skills the first agent loaded are not carried over, even when the target lists them too, and the target's own `max_loaded_skills` applies from then on.

## Skills in a text conversation

A text conversation — the [Chat API](https://docs.turncall.io/guides/sms-chat.md), an SMS or a WhatsApp text — holds skills the way a call does, across its messages. The prompt lists the agent's skills and the reply's tool loop offers `load_skill`. A load in one round of the loop gives the next round the skill's section and tools, so the reply that loaded `explain_bill` can call `get_invoices` before it goes out:

```text theme={null}
POST /v1/chat {"agent_id": "...", "message": "Why is my bill higher?"}
  → load_skill("explain_bill") → get_invoices() → "Your bill went up by ..."
POST /v1/chat {"agent_id": "...", "session_id": "...", "message": "When is it due?"}
  → explain_bill is still loaded: its section and get_invoices are there from the first round
```

* **Resolved once per session.** A session's first message resolves the agent's skills (pinned or live published versions, in the project) and the session keeps those versions: a skill published mid-conversation reaches the next session, not this one. Instructions and tool descriptions render the session's [template variables](https://docs.turncall.io/guides/sms-chat.md) as they stand on each message.
* **Loaded for the session.** What a message loads stays loaded for the session's later messages. A new session starts with nothing loaded.
* **The same rules as a call.** `max_loaded_skills` unloads the skill loaded longest ago, a skill already loaded loads as nothing, an unknown name is answered with the skills that can be loaded, and tools take the same precedence. A loaded skill's `prompt`-mode documents join its section and its `tool`-mode knowledge bases are searched through `query_knowledge`.
* **Two messages at once.** Two messages of one session answered at the same time keep both of their loads, within `max_loaded_skills`.

A text load is recorded as a `load_skill` row in the session's tool invocations (`GET /v1/chat/sessions/{id}/tool-invocations`), like any text tool. A text session has no event log, so there are no `skill.loaded` events.

## Call-init inline agents

An [inline agent](https://docs.turncall.io/guides/call-init.md#returning-an-inline-agent) returned by call-init has no stored row, so nothing checks its `skills` when it is saved. They resolve against the call's project like a stored agent's: the skills the project has can be loaded on the call, and a name the project does not have (or has no published version of) is logged as a warning and skipped. The call starts either way.

```json theme={null}
{"agent": {"system_prompt": "...", "skills": [{"skill": "verify_identity"}, {"skill": "vip_retention"}]}}
```

## Skills in evals

An [eval run](https://docs.turncall.io/guides/evals.md) resolves its target's skills once, in the run's project, before its first iteration, and every iteration runs those versions. An `inline` target can name skills too. The run's `resolved_config` records them under `resolved_skills`, and a skill that did not resolve is a `skills_skipped` warning on the run.

`load_skill` is never mocked, because it changes the agent under test rather than reaching anything outside the eval. A `function_call` expectation on it checks that the agent loads the skill when it should:

```json theme={null}
{"turns": [{"user": "Why did my bill go up?",
  "expect": [{"event": "function_call",
              "calls": [{"name": "load_skill", "args": {"name": "explain_bill"}}]}]}],
 "tool_mocks": {"get_invoices": {"last": 52, "previous": 40}}}
```

The skill's own tools are mocked like any tool, and a call to one with no mock is refused under `mock_only`. Tag a skill's scenarios (say `skill:explain_bill`) and run them against each agent [`GET /v1/skills/{name}/agents`](#who-uses-a-skill) lists.

## What a call records

The versions a call resolved are saved on the call when it starts, so the call stays explainable after a skill is published again. A handoff adds the target agent's versions. Each load is a `skill.loaded` event on the call's event log (`GET /v1/calls/{id}/events`), and each unload to stay within `max_loaded_skills` a `skill.unloaded` event with reason `limit`, recorded before the load that made room. Both are also delivered to [webhook subscribers](https://docs.turncall.io/guides/server-events.md#webhook-events):

```json theme={null}
{"event": "skill.unloaded", "payload": {"skill": "verify_identity", "version": 2, "reason": "limit"}}
{"event": "skill.loaded", "payload": {"skill": "plan_change", "version": 1, "reason": "load_skill"}}
```

The call read (`GET /v1/calls/{id}` and the list) and `call.ended` carry `skills`: every skill version the call resolved, in the agent's order (after a handoff, the target agent's next; a version both agents resolved is listed once), with whether the agent pinned it and whether the call loaded it at any point. A skill the call never loaded is listed too, and a skill unloaded later still reads `loaded: true`: its `skill.unloaded` event says when it went. A call whose agent has no skills has an empty list.

```json theme={null}
"skills": [
  {"skill": "verify_identity", "version": 2, "pinned": false, "loaded": true},
  {"skill": "explain_bill", "version": 1, "pinned": true, "loaded": false}
]
```

An eval run records neither (its own record is `resolved_config`, above). Skill names, versions and reasons are identifiers, never [redacted](https://docs.turncall.io/guides/redaction.md). `skill.loaded` and `skill.unloaded` are in the events kind of [retention](https://docs.turncall.io/guides/retention.md); once a call's events expire, `loaded` is `null`, and the versions stay.

## A skill library

A project can read its skills from another project, its **skill library**, so agents in several projects share one set of skills. Only the platform credential links a project to a library or unlinks it:

```bash theme={null}
curl -X PUT http://localhost:8090/v1/projects/{project_id}/skill-library \
  -H "X-Platform-Key: $PLATFORM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"library_project_id": "<library project id>"}'
```

`{"library_project_id": null}` unlinks it; the field is required, so an empty body is a `422` rather than an unlink. The response is the project, which shows the link as `skill_library` on every read:

```json theme={null}
"skill_library": {"library_project_id": "<library project id>"}
```

An API key, admin included, is refused (`401`): a link lets one project read another's skills, so only the platform, which created both, may make one. The link is refused with a `422` for the project itself, a project that does not exist or is deleted, a library that is itself linked, and a project other projects already read from: a library is one level deep. Each change is an [audit log](https://docs.turncall.io/guides/audit-log.md) entry on the linked project, `action: "skill_library_set"`, by `actor: "platform"`.

A linked project's skills come **only** from its library. Its own skills are not consulted, never as a fallback: one name means one skill for every agent that reads from the library. So, in a linked project:

* **Saving an agent** checks its `skills` against the library: a name the library has no published version of, a tool collision with the library version's tools, and a pin on a version that is not one of the library's are the `422`s [above](#attach-skills-to-an-agent), saying "in this project's skill library".
* **A call** resolves the agent's skills in the library: a stored agent, a [call-init inline agent](#call-init-inline-agents) and a handoff's target alike. A name the library does not hold, or a pin outside it, is a warning and a skipped skill; the call starts either way.
* **A text conversation** ([below](#skills-in-a-text-conversation)) and an **[eval run](#skills-in-evals)** resolve the agent's skills in the library too: a session loads the library's skills and keeps them across messages, and a run records the library's versions in `resolved_config.resolved_skills`.
* **A skill's knowledge bases** belong to the project that holds the skill: a library skill's `knowledge_bases` are the library's, and its `prompt`-mode documents and `tool`-mode search reach the linked project's calls and conversations once it is loaded.
* **A library that is deleted** holds no skills: a call skips them with a warning, and a save that lists one is a `422`. A library that is purged unlinks its projects, which then read their own skills again.

A call's record is the same as for a project's own skills: `skills` on the call read names the library's versions.

## Audit

Every create, edit, publish, rollback and delete is an [audit log](https://docs.turncall.io/guides/audit-log.md) entry with `resource_type: "skill"`. A publish or rollback also writes an `archived` entry for the version it took out of service.


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