Skills
SkillLoader loads markdown skills from three sources, matches them by user intent, and injects the body into the system prompt as a contribution.
A skill is a markdown file with YAML frontmatter. The runtime
loads it, indexes it by intent, and injects its body into the
system prompt when the user's intent matches. The variable surface
is the markdown body and the activation policy in the frontmatter;
the loader, the cache, and the intent matching are structural. See
Prompts for the contribution shape the skill body
becomes, and Subagents for how a skill with an
execution block delegates to a child runtime.
Skill is one of three concepts in the composing-agents cluster, alongside the subagent spawn that executes it and the agent profile that scores it. The skill carries the spec name; a matching subagent invocation writes one entry into the profile keyed by that same name.
Ships from the @pleach/core/skills barrel — a real, emitted subpath
export (SkillLoader, parseSkillFrontmatter, and the Skill /
SkillFrontmatter types all import from it). There are no
/loader, /parser, or /types sub-subpaths; import everything from
the @pleach/core/skills barrel.
import { SkillLoader, parseSkillFrontmatter } from "@pleach/core/skills";
import type { Skill, SkillFrontmatter } from "@pleach/core/skills";Skill shape
| Field | Type | Notes |
|---|---|---|
name | string | kebab-case identifier |
description | string | One-line summary |
version | string | undefined | Semver, optional |
author | string | undefined | Email or identifier |
allowedTools | string[] | undefined | Whitelist |
disallowedTools | string[] | undefined | Blacklist |
toolMode | "inherit" | "none" | undefined | Parent's resolved tools, or pure reasoning |
intents | string[] | undefined | Keywords from the intent classifier |
activation | enum | auto / manual / intent-matched (default intent-matched) |
content | string | Markdown body, injected into the prompt |
source | enum | builtin / org / user |
path | string | undefined | Filesystem path (builtin skills) |
execution | object | undefined | maxSteps, timeout, canDelegate |
activation decides how the loader surfaces the skill. auto
and intent-matched are both returned by getByIntent(intent);
manual skills are returned only by getByName(name).
Loader sources
SkillLoader.loadAll(orgId?, userId?) walks three sources and
unions the results into a single cache. Builtin first, then store,
then subagent-spec migration; the loader returns the merged list
and caches by name.
| Order | Source | Where it reads | Notes |
|---|---|---|---|
| 1 | Builtin | skills/builtin/<name>/SKILL.md (server only) | Skipped in the browser; missing dir is fine |
| 2 | Org / user store | [orgId, "skills"] then [orgId, userId, "skills"] | Up to 50 per namespace |
| 3 | Subagent specs | The subagentSpecs array passed to the constructor | Skipped if a skill with the same name already exists from sources 1 or 2 |
The constructor takes the store and the spec list:
const loader = new SkillLoader({
store, // anything implementing { search }
subagentSpecs: [
{
name: "researcher",
description: "Fan out searches.",
tools: ["search_corpus", "search_news"],
maxSteps: 5,
timeout: 120_000,
systemPromptSuffix: "You are a research subagent.",
},
],
});
await loader.loadAll(orgId, userId);A migrated subagent spec lands as a builtin skill with
activation: "manual" — the spec was already gated by an
explicit delegation, not by intent matching.
Lookups
| Method | Returns | Effect |
|---|---|---|
loadAll(orgId?, userId?) | Skill[] | Walks the three sources, clears and repopulates the cache |
getByIntent(intent) | Skill[] | Skills whose intents include intent and whose activation is auto or intent-matched |
getByName(name) | Skill | undefined | Exact-match cache lookup |
getAll() | Skill[] | Every cached skill |
size | number | Cache size |
loadAll is the only async method; everything else reads from the
in-memory cache the load populated.
The lookup path is the synchronous half — call loadAll once at
session start, then route per-turn intents through the cache.
await loader.loadAll(orgId, userId);
const matched = loader.getByIntent("vendor_evaluation");
for (const skill of matched) {
systemPrompt.append(skill.content);
}
const fallback = loader.getByName("researcher");
if (fallback) await delegateToSubagent(fallback);getByIntent filters to auto and intent-matched activations.
getByName is the only path to a manual skill.
SKILL.md format
---
name: vendor-evaluation
description: Score vendors against a fixed rubric.
version: 1.0.0
author: ops@example.com
allowed-tools:
- search_web
- fetch_url
intents:
- vendor_evaluation
- rfp_scoring
activation: intent-matched
execution:
maxSteps: 8
timeout: 180000
---
You are a vendor-evaluation agent. Score each candidate against the
fixed rubric in the user's request. Cite every claim with the URL
you pulled it from.parseSkillFrontmatter(fileContent, source, filePath?) is exported
directly for callers that want to parse a SKILL.md without going
through the loader (tests, custom store adapters). The parser is
intentionally a lightweight YAML reader — keys, scalars, inline
arrays, indented arrays, and one level of nested objects. Anything
more elaborate belongs in a real YAML library.
A direct parse, no loader involved:
import { parseSkillFrontmatter } from "@pleach/core/skills";
const raw = await readFile("./skills/builtin/researcher/SKILL.md", "utf-8");
const skill = parseSkillFrontmatter(raw, "builtin", "./skills/builtin/researcher/SKILL.md");
console.log(skill.name, skill.activation, skill.allowedTools);Where to go next
Subagents
A skill with an `execution` block can be delegated as a subagent.
Prompts
The skill's markdown body lands as a system-prompt section contribution.
Tools
`allowedTools` / `disallowedTools` / `toolMode` gate the tool registry per skill.
Subpath exports
`@pleach/core/skills` is a real emitted barrel — import SkillLoader, parseSkillFrontmatter, Skill, and SkillFrontmatter from it.