---
title: "Skills"
---

> Documentation Index
> Fetch the complete documentation index at: https://truememory-docs.mvstg.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Skills

Skills are reusable prompt templates stored in Truememory — short, named instructions like "summarize this thread" or "translate to formal English" that an app can pull up by key and run on demand. Truememory stores the template; the calling app decides when to invoke it and how to render it (a slash command, a button in the UI, a quick action menu, etc.).

If your app already builds prompt presets on its own backend, skills move that storage into Truememory so the same set is available everywhere the user goes — BlinkLife, Vibrantly, and any future app on the platform — without each app maintaining its own copy.

These endpoints manage **app-facing skill records in the server database**. They are separate from the repository's `.agents/skills` or prompt-instruction folders, which are development-time instructions for LLM agents working on this codebase.

All skill endpoints require authentication. User-scoped endpoints accept either:

- a user Bearer token from [Truememory Auth](/authentication/overview/)
- the calling app's service token plus `X-On-Behalf-Of: <user_id>` when your backend is proxying the request for a user

Admin endpoints require the service token (see [Admin endpoints](#admin-endpoints) below).

## Integration flow

Use skills in two steps:

1. Store or fetch the skill template through `/skills`.
2. Invoke your agent by sending the skill's `prompt` through `/v1/chat/completions`, usually in `skill_instructions` or as part of your caller-composed prompt.

For example, a slash-command menu can load `GET /skills`, render each skill by `name`, `icon`, and `argument_hint`, then fetch the selected row by `key` before sending its `prompt` into the chat request.

```bash
# Backend proxying for a user
GET https://your-tie-host/skills?is_active=true
Authorization: Bearer $AUTH_SECRET
X-On-Behalf-Of: 11111111-2222-3333-4444-555555555555
```

```bash
# Later, invoke the selected template
POST https://your-tie-host/v1/chat/completions
Authorization: Bearer $AUTH_SECRET
X-On-Behalf-Of: 11111111-2222-3333-4444-555555555555
Content-Type: application/json

{
  "model": "anthropic/claude-sonnet-4-5",
  "messages": [
    { "role": "user", "content": "Summarize this thread for my product standup." }
  ],
  "skill_instructions": "Summarize the conversation so far in 3 short bullet points.",
  "metadata": {
    "active_skill": "summarize-thread"
  }
}
```

Truememory does not automatically inject a skill's `prompt` into chat completion requests. Your app decides which skill applies, reads the stored `prompt`, and passes it via `skill_instructions`. There is one automatic connection: passing the skill's `id` as `skill_id` on a chat request retrieves every [knowledge pack](/ai-gateway/knowledge-packs/#retrieval) tagged with that skill.

## What a skill actually is

A skill is a row of caller-defined data. Truememory does not interpret the `prompt` text, the `allowed_tools` list, the `category`, or the `metadata` blob — it stores and returns them. Your app decides what to do with each field when it invokes the skill.

This matters because:

- A skill named `summarize` does not automatically run a summarization — your app reads the skill's `prompt` and sends it through `/v1/chat/completions` (or wherever it routes prompts).
- `allowed_tools` is metadata, not enforcement. Truememory will not block tool calls based on this list; the caller is responsible for honoring it at invocation time.
- `icon` and `argument_hint` exist to help your UI render the skill — Truememory never reads them. `category` is the one exception: Truememory accepts it as an exact-match filter on `GET /skills?category=…`.
- `metadata` is a caller-owned JSON object. Use it for integration hints your app understands, not for Truememory control flags. Anything Truememory acts on is a real field instead — see `use_case` and `detection_patterns` below, which were promoted out of `metadata` for exactly this reason.

## Trigger fields

Two fields describe *when* a skill applies, as opposed to what it does. Both are optional — a skill without them is still invokable by key, it just never gets selected automatically.

| Field | Type | Purpose |
|---|---|---|
| `use_case` | string | When the skill applies, in natural language: `"Triggers when a user shares a photo of food, a menu, or ingredients."` |
| `detection_patterns` | string[] | Example utterances that should invoke it: `["what are the macros in this", "can I eat these berries"]` |

Write `use_case` to describe the *trigger*, not the behaviour — "Triggers when the user asks for a recap" beats "Summarizes conversations."

`detection_patterns` are matched **semantically, not literally**. List distinct phrasings rather than keyword variants: "can I eat these berries" also covers "are these mushrooms safe", so a separate entry for every synonym is wasted effort. Up to 32 entries, 500 characters each.

Both were previously written into `metadata`. That was the right place while Truememory only stored them; once selection reads them they are control flags, which `metadata` explicitly is not for. Existing values were copied into the new columns by migration 037, and reads still fall back to `metadata` so a client that has not switched keeps working. Send them as real fields going forward.

Keys that remain caller-owned — `platform_skill_type`, `icon_data`, `model_tier` — should stay in `metadata`. Truememory does not act on them.

### Automatic selection

Send `skill_selection: "auto"` on a chat completion and Truememory matches the user's message against the trigger data of every skill visible to that caller and app, injecting the closest matches:

```json
{
  "model": "anthropic/claude-sonnet-4-5",
  "messages": [{ "role": "user", "content": "what are the macros in this" }],
  "skill_selection": "auto"
}
```

Defaults to `"off"`, so nothing changes until you ask for it. It combines with `skills`: explicitly named keys always apply, and matches are appended after them without duplicating one you already named.

Only skills you could already list are candidates, and only those carrying trigger data — a skill with no `use_case` or `detection_patterns` is never selected automatically, though it stays invokable by key. At most 3 skills are selected per turn.

Matching is **semantic**, so paraphrases work: "how many calories in this" selects a skill whose patterns are "what are the macros in this" and "scan this nutrition label", sharing no words with either. Messages carrying little information ("hi", "thanks") skip matching entirely.

Selection is best-effort. If matching cannot run, the turn proceeds with no automatically selected skills rather than failing — unlike a key you named in `skills`, which 404s when it cannot be resolved.

**Think of it like:** a shared snippet library. The library stores the snippet text; the editor that opens it decides how to expand and run it.

**Where that metaphor breaks down:** unlike most snippet libraries, skills have ownership scoping (presets vs. custom) and a fork relationship built in.

## Presets and custom skills

Every skill is one of two kinds:

| Kind | `user_id` | `is_preset` | Who can see it | Who can mutate it |
|---|---|---|---|---|
| **Preset** | `null` | `true` | Every authenticated user | Admins only (service token) |
| **Custom** | the owner | `false` | Only the owner | Only the owner |

When a user owns a custom skill whose `key` matches a preset's `key`, the user's version wins on `GET /skills/by-key/{key}`. This is intentional — it lets users override a preset without losing the preset for everyone else.

## Key naming

`key` is the stable identifier your app uses to find the skill (typically the slash command name, like `/summarize`). It must:

- Be 2–64 characters
- Use only lowercase letters, digits, hyphens, or underscores
- Start and end with a letter or digit

Two different users can each own a skill with the same key (for example, both have a `summarize`). A single user cannot have two skills with the same key, and presets share one global keyspace.

## App scoping

Every skill carries an `app_ids` array that controls which apps can see it. One skill can serve several apps, or every app on the platform.

| `app_ids` value | Who sees the skill |
|---|---|
| `null` (or omitted) | Every app — the skill is global |
| `["vibrantly", "blinklife"]` | Only those apps, plus nothing else |

Pass `app_ids` on create or update:

```bash
POST https://your-tie-host/skills
Authorization: Bearer $TOKEN
Content-Type: application/json

{
  "key": "summarize-thread",
  "name": "Summarize Thread",
  "prompt": "Summarize the conversation so far in 3 short bullet points.",
  "app_ids": ["vibrantly", "blinklife"]
}
```

Send `"app_ids": null` to make the skill global. An empty list is treated the same as `null`.

**Legacy `app_id`.** The earlier scalar `app_id` field still works on create and update. Truememory folds a single `app_id` into a one-element `app_ids` list, so existing single-app callers need no changes. Responses always return `app_ids`.

**Reading by app.** The `app_id` query parameter on `GET /skills` names the requesting app, not the skill. It is still a single value:

```bash
GET https://your-tie-host/skills?app_id=vibrantly
Authorization: Bearer $TOKEN
```

A user-facing read returns skills assigned to that app **plus** global skills (`app_ids` of `null`). Admin reads (`GET /admin/skills?app_id=…`) match the app exactly and do not include global skills. Omit the parameter to read across every app.

This `app_ids` array is one of two ways to scope a read. The other is the admin-managed app/category taxonomy, reached with the `X-Use-Taxonomy` header — see [Listing through the app/category taxonomy](#listing-through-the-appcategory-taxonomy).

## List skills

Returns presets plus the authenticated user's custom skills, paginated. Results are ordered presets first, then by `sort_order` ascending, then by `name`.

```bash
GET https://your-tie-host/skills?category=writing&is_active=true&search=summary&limit=50&offset=0
Authorization: Bearer $TOKEN
```

| Query param | Type | Default | Description |
|---|---|---|---|
| `app_id` | string | — | Requesting app. Returns skills assigned to it plus global skills. Omit to read across every app. See [App scoping](#app-scoping) |
| `category` | string | — | Exact match on the `category` field |
| `is_active` | bool | — | Filter by active flag. Inactive skills are still listed when this is omitted |
| `search` | string | — | Case-insensitive substring match on `name` or `description` |
| `limit` | int | 50 | 1–200 |
| `offset` | int | 0 | — |

For backend-to-backend integration, use the service token and pass the user subject explicitly:

```bash
GET https://your-tie-host/skills?is_active=true
Authorization: Bearer $AUTH_SECRET
X-On-Behalf-Of: 11111111-2222-3333-4444-555555555555
```

**Response:**

```json
{
  "skills": [
    {
      "id": "0bc0...",
      "user_id": null,
      "app_ids": null,
      "key": "summarize",
      "name": "Summarize Thread",
      "description": "Summarize the current conversation in three bullets.",
      "prompt": "Summarize the conversation so far in 3 short bullet points.",
      "category": "productivity",
      "icon": "sparkles",
      "argument_hint": null,
      "allowed_tools": [],
      "metadata": {},
      "is_preset": true,
      "is_active": true,
      "sort_order": 0,
      "forked_from_skill_id": null,
      "created_at": "2026-01-01T00:00:00Z",
      "updated_at": "2026-01-01T00:00:00Z"
    }
  ],
  "total": 1
}
```

`total` is the count of all matching rows before `limit`/`offset` — use it to paginate.

### Listing through the app/category taxonomy

Alongside the `app_ids` array and the free-text `category` field, Truememory keeps an admin-managed **app/category taxonomy**: apps and categories are first-class records — each app has a unique, URL-safe **slug** — and skills are grouped into categories that are grouped under apps.

To list skills through that taxonomy, send the `X-Use-Taxonomy: true` header on the same `GET /skills` endpoint. The header does not add any new endpoint — it only changes how two existing query parameters are interpreted:

```bash
GET https://your-tie-host/skills?app_id=vibrantly&category=6f1c8b2a-...&is_active=true
Authorization: Bearer $TOKEN
X-Use-Taxonomy: true
```

| Parameter | Without the header (default) | With `X-Use-Taxonomy: true` |
|---|---|---|
| `app_id` | The `app_ids` scope value (see [App scoping](#app-scoping)) | An **app slug**, resolved through the taxonomy |
| `category` | Exact match on the free-text `category` field | A **category id** (UUID) from the taxonomy |

`is_active`, `search`, `limit`, `offset`, the ordering, and the response shape are all unchanged.

Two behaviours to note in taxonomy mode:

- **Unknown slug 404s.** If `app_id` is a slug no app owns, the request returns `404 APP_NOT_FOUND` — a typo fails loudly instead of returning an empty list. A valid app that simply has no linked skills returns an empty `skills` array with `total: 0`.
- **No global skills.** Unlike the default read, taxonomy mode returns only the skills the taxonomy links to that app; it does **not** add global (`app_ids: null`) skills.

## Get a skill

Two ways to read a single skill: by id, or by key.

```bash
GET https://your-tie-host/skills/{skill_id}
Authorization: Bearer $TOKEN
```

Returns 404 if the skill does not exist or is owned by another user.

```bash
GET https://your-tie-host/skills/by-key/{key}
Authorization: Bearer $TOKEN
```

When a user owns a skill with the same `key` as a preset, the user's version is returned (override semantics).

## Create a custom skill

```bash
POST https://your-tie-host/skills
Authorization: Bearer $TOKEN
Content-Type: application/json

{
  "key": "summarize-thread",
  "name": "Summarize Thread",
  "description": "Summarize the current conversation in three bullets.",
  "prompt": "Summarize the conversation so far in 3 short bullet points.",
  "category": "productivity",
  "app_ids": ["vibrantly"],
  "icon": "sparkles",
  "argument_hint": "[topic]",
  "allowed_tools": ["web_search"],
  "metadata": {},
  "is_active": true,
  "sort_order": 0,
  "forked_from_skill_id": null
}
```

Returns the created skill with HTTP `201 Created`. Only `key`, `name`, and `prompt` are required.

| Field | Type | Notes |
|---|---|---|
| `key` | string | See [Key naming](#key-naming) |
| `app_ids` | string[] \| null | Apps that can see the skill. `null` means every app. See [App scoping](#app-scoping). The legacy scalar `app_id` is also accepted |
| `name` | string | 1–255 chars |
| `description` | string \| null | Optional, no length cap |
| `prompt` | string | The template body, sent to the LLM by the caller |
| `category` | string \| null | ≤ 100 chars |
| `icon` | string \| null | ≤ 255 chars. Interpreted by the caller — any identifier or URL |
| `argument_hint` | string \| null | ≤ 255 chars. Hint shown to the user about expected arguments (e.g. `[topic]`) |
| `allowed_tools` | string[] | Caller-defined list; Truememory stores but does not enforce |
| `metadata` | object | Free-form JSON owned by the caller |
| `is_active` | bool | Default `true`. Inactive skills are still listed |
| `sort_order` | int | Display order; lower sorts earlier |
| `forked_from_skill_id` | string \| null | Optional — see [Forking a preset](#forking-a-preset) |

Omitted optional fields use server defaults: `description`, `category`, `app_ids`, `icon`, `argument_hint`, and `forked_from_skill_id` become `null` (a `null` `app_ids` means every app); `allowed_tools` becomes `[]`; `metadata` becomes `{}`; `is_active` becomes `true`; `sort_order` becomes `0`.

**Errors:**

- `409 SKILL_KEY_TAKEN` — the user already owns a skill with this key
- `400 SKILL_FORK_INVALID` — `forked_from_skill_id` is set but does not point at an existing preset
- `422` validation error — the request body does not match the schema, such as an invalid `key`, empty `prompt`, or `null` for a required update field

## Forking a preset

Setting `forked_from_skill_id` on create copies the relationship — the new skill is a user-owned snapshot of the preset, with a back-pointer for analytics or UI hints. Only **preset** skills are forkable; passing the id of another user's custom skill, or any non-preset row, is rejected with `400 SKILL_FORK_INVALID`.

Fork is metadata, not a live link. Edits to the preset do not propagate to forks.

## Update a custom skill

```bash
PUT https://your-tie-host/skills/{skill_id}
Authorization: Bearer $TOKEN
Content-Type: application/json

{
  "name": "Summarize (concise)",
  "prompt": "Summarize the conversation in 2 short bullets, max 12 words each."
}
```

All fields are optional — send only what you want to change. Returns 404 if the skill does not exist, is not owned by the user, or is a preset.

**Nullable vs. NOT NULL:** `description`, `category`, `app_ids`, `icon`, `argument_hint`, and `forked_from_skill_id` accept explicit `null` to clear the value (a `null` `app_ids` makes the skill global). The remaining fields are NOT NULL — sending `null` is rejected; omit the field to leave it unchanged.

## Delete a custom skill

```bash
DELETE https://your-tie-host/skills/{skill_id}
Authorization: Bearer $TOKEN
```

**Response:**

```json
{ "status": "deleted", "skill_id": "0bc0..." }
```

Returns 404 if the skill does not exist, is not owned by the user, or is a preset.

## Admin endpoints

A second route block under `/admin/skills/*` exposes the same CRUD without ownership or preset scoping. It lets a service create presets, list skills across all users, and mutate or delete any row regardless of `is_preset`.

These endpoints are gated by the Truememory service token (`AUTH_SECRET`), not a user Bearer token. The trust model is: the calling app validates that the human acting through it is an admin; Truememory trusts the service token. End-user tokens cannot reach these endpoints.

`X-On-Behalf-Of` is not needed on admin calls. Admin routes are authorized by the service token, and they do not use a subject user unless you explicitly pass `user_id` in an admin request body or query string.

```bash
POST https://your-tie-host/admin/skills
Authorization: Bearer $AUTH_SECRET
Content-Type: application/json

{
  "key": "summarize-thread",
  "name": "Summarize Thread",
  "prompt": "Summarize the conversation so far in 3 short bullet points.",
  "category": "productivity",
  "icon": "sparkles"
}
```

Omit `user_id` or set it to `null` to create a preset visible to every authenticated user. Set `user_id` to create a custom row owned by that user.

Admin create and update accept `app_ids` exactly like the user endpoints (see [App scoping](#app-scoping)), so an admin can scope a preset to specific apps or share it with all of them. The legacy scalar `app_id` is accepted here too.

| Endpoint | Behavior |
|---|---|
| `GET /admin/skills` | List across all users. Filters: `user_id`, `app_id` (exact match, no global skills), `is_preset`, `category`, `is_active`, `search`, `limit`, `offset` |
| `POST /admin/skills` | Create. `user_id` null → preset; `user_id` set → row owned by that user |
| `GET /admin/skills/{skill_id}` | Get any skill |
| `PUT /admin/skills/{skill_id}` | Update any skill, including presets |
| `DELETE /admin/skills/{skill_id}` | Delete any skill, including presets |

Fork validation (`SKILL_FORK_INVALID`) still runs on admin paths — it is a data invariant, not a permission check.

## Common pitfalls

- **Confusing API skills with LLM instruction files.** The `/skills` endpoints manage rows in the Truememory database for apps to use at runtime. They do not read or write `.agents/skills`, `.claude/skills`, or other repository-local instruction folders.
- **Treating `allowed_tools` as enforcement.** Truememory does not block tool calls based on this list. If your app needs to restrict which tools a skill can use, enforce it in the caller before sending the prompt.
- **Trying to update or delete a preset via the user endpoints.** Returns 404, not 403 — presets are invisible to the mutation path on purpose. Use the admin endpoints if you genuinely need to change a preset.
- **Calling admin endpoints with an end-user token.** Admin routes require the service token. If your product has human admins, enforce that role in your app before forwarding the admin request to Truememory.
- **Re-fetching presets per request to render a slash menu.** Skills change rarely; cache the list on the client and invalidate on user-driven create/update/delete.
- **Assuming forks track the preset.** Fork is a static back-pointer. If the preset changes, existing forks do not.

Source: https://truememory-docs.mvstg.com/ai-gateway/skills/index.mdx
