Skills
Skills are markdown files with YAML frontmatter that customize how the agent behaves. A skill can be always-on (part of every prompt) or dynamic (loaded on demand when it’s relevant).
How a skill loads — loading-strategy
Section titled “How a skill loads — loading-strategy”Every skill declares one loading strategy:
| Strategy | Behavior |
|---|---|
always |
Always composed into the system prompt. For identity, voice, and durable cross-cutting rules. Keep it short — it costs context on every turn. |
dynamic |
Loaded on demand. For task / domain skills. Enters context through one of the activation signals below. |
A dynamic skill activates through any of:
- Its
description— the model sees every skill’s name + description in a catalog and activates the relevant one. This is the default path, so write the description to say what the skill does and when to use it (include the words a user would say). Always available — no extra field needed. tool-affinity— auto-activates when a tool in the active set matches one of its globs. For skills bound to specific tools.triggers— auto-activates on an exact phrase, for deterministic “must-fire” cases (e.g. a compliance rule that must load whenever a regulated action is named).
A dynamic skill with no tool-affinity and no triggers loads only when the model activates it on its description.
The skill catalog
Section titled “The skill catalog”Every conversation’s system prompt carries a Skill Catalog — one line (name + description) per dynamic skill available in that workspace: authored org/workspace/user skills, skills published by installed apps, and curated connector overlays. Always-on skills don’t appear (their full text is already in the prompt), and neither do disabled ones.
When a task matches a listed skill, the model loads it with the nb__use_skill tool, which returns the skill’s full body in the tool result. A skill is delivered at most once per conversation — loading one that’s already in context (via nb__use_skill or a connector overlay) returns a short “already loaded” note instead of a second copy.
Skill file format
Section titled “Skill file format”A skill is a markdown file (.md) with YAML frontmatter. Standard fields are top-level; NimbleBrain configuration nests under metadata.nimblebrain:
---name: meeting-notesdescription: > Summarize and search meeting notes from Granola. Use when the user mentions meetings, notes, agendas, minutes, or standups.allowed-tools: granola__search_meetings granola__get_meetingmetadata: author: mat version: "1.0" nimblebrain: loading-strategy: dynamic priority: 50 tool-affinity: - granola__* triggers: - summarize my meetings - meeting notes---
You are a meeting notes assistant. Use the Granola tools to search andsummarize meeting transcripts.
When asked about meetings:1. Search with granola__search_meetings2. Retrieve transcripts with granola__get_meeting3. Summarize the key discussion points, decisions, and action items
Format action items as a checklist.Frontmatter fields
Section titled “Frontmatter fields”Standard (top-level):
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Lowercase letters, digits, and single hyphens (e.g. meeting-notes); ≤64 chars; matches the filename. |
description |
string | Yes | What the skill does and when to use it — the activation signal for dynamic skills, so include the words a user would say. ≤1024 chars. |
license |
string | No | License name or reference to a bundled license file. |
allowed-tools |
string | No | Space-separated globs for tools this skill may call (e.g. granola__*). |
metadata.author / metadata.version |
string | No | Conventional metadata. |
NimbleBrain (metadata.nimblebrain.*):
| Field | Type | Required | Description |
|---|---|---|---|
loading-strategy |
always | dynamic |
Yes | How the skill loads (see above). |
priority |
number | No | Selection / ordering priority, 11–99 (0–10 reserved for core). Default 50. |
status |
active | disabled |
No | Default active. |
tool-affinity |
string[] | No | Globs; a dynamic skill auto-loads when a matching tool is active (e.g. granola__*). |
triggers |
string[] | No | Exact phrases; a dynamic skill auto-loads on a case-insensitive substring match. |
Where skills are loaded from
Section titled “Where skills are loaded from”Skills are loaded from multiple locations, in order:
- Core skills (
src/skills/core/) — shipped with the platform, always loaded - Built-in skills (
src/skills/builtin/) — shipped with the package - Global skills (
~/.nimblebrain/skills/) — user-created - Config directories — any paths listed in
skillDirsinnimblebrain.json
{ "skillDirs": [ "./skills", "/home/user/custom-skills" ]}Files must have a .md extension.
Platform defaults, and overriding one
Section titled “Platform defaults, and overriding one”Three core skills are always-on for every workspace and need no setup:
| Skill | What it does |
|---|---|
soul |
Base identity — use your tools, don’t guess, be concise and direct. |
customer-communication |
Talk about the customer’s goal and the work, not the tool names, IDs, and internal state used to do it. |
capabilities |
How to find the tools an app publishes and promote them into the active set. |
All three ship with the platform, so they can’t be edited in place — skills__update refuses to write a platform-vendored file. Change one by authoring your own skill with the same name in a scope that outranks it.
Skills merge by name, with later tiers winning: user > workspace > org (~/.nimblebrain/skills/) > platform. So a file at ~/.nimblebrain/skills/customer-communication.md replaces the shipped one for the whole org; the same name under a workspace or a single user narrows it further. Writing an org-scoped skill requires an org admin or owner role.
soul and capabilities are reserved names — skills__create refuses them, so an override for those two has to be a file you write yourself. customer-communication is not reserved, so either route works.
To turn a default off rather than reword it, give the override status: disabled — an always-on skill that isn’t active is dropped before the prompt is composed. Author it directly as a file, or create the override and toggle it off in the Skills app:
---name: customer-communicationdescription: Disabled — this deployment wants the machinery visiblemetadata: nimblebrain: loading-strategy: always status: disabled---Connector skill overlays
Section titled “Connector skill overlays”Installing a connector can automatically apply a short usage overlay — curated guidance for that connector’s tools (e.g. “confirm the recipient before sending mail”). Overlays target the connectors you don’t author yourself (Composio and other third-party MCP servers), so they’re sourced from a NimbleBrain-curated public overlay repo keyed by the connector’s flat slug (the Composio toolkit, e.g. gmail; otherwise the connector slug, e.g. notion) at a pinned version.
Overlays differ from authored skills in three ways:
- Surfaced once into the conversation, not the system prompt. An authored
dynamic+tool-affinityskill composes into the system prompt for the turn. A connector overlay instead appears in the conversation history exactly once — on the first call to a matching tool — and then rides the (cached, append-only) history for the rest of the conversation. It’s relevant (only when the connector’s tools are actually used) and cache-friendly (it never re-varies the cached system prefix turn to turn). The tradeoff of this reactive model: the overlay surfaces after the first matching tool call, so guidance whose value is preemptive (e.g. “confirm before sending”) does not gate that very first call. It does reach the model before its next action in the same turn, so it governs every call after the one that triggered it. For hard preconditions, prefer an authoredalwaysskill or a tool-side guard. - Bound to the connector’s tools. An overlay is bound to every tool of the connector it is installed for (
<server-name>__*), or, when its frontmatter declaresmetadata.nimblebrain.tool-affinity, only to the tools it names, read the same way as for a skill published by the server. The block needs nothing else: the other runtime fields are stamped on install, and an overlay always loadsdynamic. - Stored separately. Materialized overlays live in a
connector-skills/store, distinct from authoredskills/. They don’t appear in theskills__listtool output; list them with the connector tool’slist_bound_skillsaction instead.
Resolution is always on and fail-soft: it fetches at a pinned repo version, records the fetched content hash, and treats a missing overlay (or any fetch error) as a no-op — a connector install never fails because its optional guidance couldn’t be fetched, and there’s nothing to enable (an overlay exists only if we’ve curated one). Override the repo / version with CONNECTOR_SKILLS_REPO and CONNECTOR_SKILLS_VERSION. When a connector has a curated overlay, the platform skips synthesizing the server’s own skill://<name>/SKILL.md guidance so the model sees one set of instructions, not two.
Uninstalling the connector removes its materialized overlays.
Skills published by an MCP server
Section titled “Skills published by an MCP server”A server publishes its own guidance as skills through the MCP Skills Extension (io.modelcontextprotocol/skills). The platform declares the extension when it connects, and a server that does not declare it publishes no skills: its skill:// resources are ordinary resources.
To publish skills, a server must:
- Declare the extension in its capabilities:
"extensions": { "io.modelcontextprotocol/skills": {} }, alongside theresourcescapability. On a2026-07-28connection the platform asks only a server that declares it. On a 2025-era connection it asks every server once per discovery window, because some SDKs (the PythonmcpSDK among them) leave the declaration out of the legacyinitializeresult; a server that answers-32601(method not found) publishes no skills. - Implement
skills/list, returning one entry per skill: theSKILL.mdURI, its frontmatter verbatim as JSON, and aresourcesmanifest of every file in the skill with its SHA-256digestand bytesize(or"dynamic"for generated content). - Implement
skills/get, returning the same entry for one skill URI. - Serve each file with
resources/read.
How the platform uses them:
- Discovery reads the listing only. The catalog and each skill’s loading rules come from the listed frontmatter; no
SKILL.mdis fetched until the skill is needed. Analwaysskill is fetched when a turn composes it, adynamicskill when tool-affinity or a trigger selects it or when the agent activates it withnb__use_skill. - Every fetched
SKILL.mdis verified. Its bytes must match the listed digest and size, and its frontmatter the listed frontmatter, or the skill does not load that turn. A verified body is cached by digest, so an unchanged skill is not fetched again; a server that updates a skill lists the new digest.
The skill’s name is its frontmatter name. When two skills on one server share a name, each is named by its full skill path instead (acme/billing/refunds). The platform reads the same metadata.nimblebrain.* block documented above off that published frontmatter, so a published skill loads by the rules its author writes rather than a fixed convention:
| Field | Effect on a published skill |
|---|---|
loading-strategy |
always composes it every turn; dynamic (the default when omitted) loads it on demand. |
priority |
Ordering, same scale. Defaults to 60. |
triggers |
Phrases that load it deterministically, whether or not the server’s tools are in the active tool set. |
tool-affinity |
The server’s own tools the skill governs, as bare names or globs (draft_email, draft_*). The platform prefixes each with <server-name>__. Omitted, the skill is bound to every tool of the server (<server-name>__*). |
Two differences from an on-disk skill:
-
Reading is lenient, not strict. A malformed or absent
metadata.nimblebrainblock leaves the fields unset and the defaults apply — an unrecognized value never rejects the skill. (An on-disk skill with bad frontmatter is skipped and logged.) Blank trigger phrases are dropped: an empty phrase would match every message. -
tool-affinityis scoped to the publishing server. A server cannot know the name it is installed under, so it declares its tools bare and the platform binds them to that install. Matching is anchored after the prefix, so a declared pattern can only narrow within the server’s own tools:*means all of them, and no declared value reaches another connector’s tools. Adynamicskill loads, and is delivered when a tool is promoted or called mid-turn, only when a tool it names is involved, so a server that publishes several workflow skills should give each one the tools it governs. A declared pattern that matches none of the tools the connector advertises is logged as a warning (skills.tool_affinity.unmatched, naming the connector, the skill, and the patterns); the same check applies to an overlay’s patterns.allowed-toolsis not read from the server.---name: writingdescription: How to draft email in the user's voice. Use when drafting or replying.metadata:nimblebrain:loading-strategy: dynamictool-affinity:- draft_email- reply_*---
If a connector has a curated overlay, the platform skips synthesizing its published skills — see the section above.
Listing skills
Section titled “Listing skills”Ask the agent which skills are loaded — it uses the built-in skills__list tool to report each skill’s name, loading strategy, priority, and source file. To see one skill’s full definition, ask the agent for its details.
Auditing how skills loaded
Section titled “Auditing how skills loaded”skills__list says which skills exist. To see which ones actually reached the model, and how, replay the skill-load ledger with skills__loading_log.
Ask the agent and it reports the summary — how many loads, across how many conversations, broken down by channel. The per-load rows come back in the tool’s structured output, so calling it directly (over the API or from an MCP client) is the path for analysis. Each row carries the skill, timestamp, conversation, tokens, and a loaded_by channel:
loaded_by |
Meaning |
|---|---|
always |
Composed into the system prompt because the skill declares loading-strategy: always. |
tool_affinity |
Composed in because a tool matching its globs was in the active set. |
trigger |
Composed in because an exact trigger phrase appeared. |
tool_use |
A connector overlay delivered on the first matching tool call. |
activation |
The model loaded a Skill Catalog entry by name with nb__use_skill. |
Filter by conversation_id, skill (name or id), loaded_by, or a since/until window. This is the way to answer questions the catalog alone can’t — whether a skill is reaching the model at all, and whether it gets there because the model chose it or because a tool call forced it.
Creating a custom skill
Section titled “Creating a custom skill”-
Create a
.mdfile in your skills directory:Terminal window mkdir -p ~/.nimblebrain/skills -
Write the skill file with frontmatter and body:
Terminal window cat > ~/.nimblebrain/skills/code-review.md << 'EOF'---name: code-reviewdescription: >Review code for bugs, security issues, and style. Use when the user asksto review code, check a diff, or look for bugs.allowed-tools: bash__runmetadata:nimblebrain:loading-strategy: dynamicpriority: 50triggers:- review this code- code review---You are a code reviewer. When asked to review code:1. Read the file using bash tools2. Identify bugs, security issues, and style problems3. Suggest specific improvements with code examplesEOF -
Restart the server to load the new skill. In development,
bun run devreloads on source changes. -
Verify it loaded by asking the agent which skills are available (it uses the
skills__listtool).
What’s next
Section titled “What’s next”- Chat — see skills in action during conversations
- Connectors — install the servers a skill’s tools come from
- Configuration: nimblebrain.json — full config reference including
skillDirs