Skip to content

Skills

Updated View as Markdown

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
  • 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 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.

# 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
# 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 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:

{
  "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:

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:

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.

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.

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
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:

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

Response:

{
  "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:

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) 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.

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.

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

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
app_ids string[] | null Apps that can see the skill. null means every app. See 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

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

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

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

Response:

{ "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.

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), 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.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close