loadSkills
JavaScript · package toolnexus · SPEC §3 S1/S2/S4/S5 · js/src/skill.ts
function loadSkills(input: string | string[] | LoadSkillsOptions): SkillSource
interface LoadSkillsOptions { dirs?: string | string[] skills?: SkillDef[] filter?: Record<string, boolean> sampleLimit?: number}
interface SkillDef { name: string description?: string content: string resources?: string[] base?: string}This is loadSkills — the same function, one signature — looked at
through its LoadSkillsOptions form instead of a bare path. Pass an options object instead of a
string/array and three more sources of behavior open up: skills supplied directly as data
(bypassing the filesystem entirely), a per-agent allowlist, and a sample cap on the sibling
files a directory-sourced skill reports.
When to use it
Section titled “When to use it”- Skills don’t live on disk. They come from a database, a CMS, an admin UI, or are generated at
startup —
skills: SkillDef[]builds the sameskilltool without ever callingreadFileSync. - Different agents should see different skills.
filternarrows the set per call site, from the same underlying directory or data — no need to maintain separateskills/folders per agent. - A skill directory has many sibling files (reference docs, sample data) and the default sample
of 10 is too many, too few, or you want to suppress the
<skill_files>block entirely.
Why this and not the alternative
Section titled “Why this and not the alternative”dirs and skills are not mutually exclusive — both can be set, and directory-discovered skills
and data-supplied skills merge into one set, first-name-wins, exactly like two directories would.
That is what lets a data-sourced “premium” skill sit alongside a folder of ordinary ones.
Examples
Section titled “Examples”1. The smallest useful call — skills as data, no filesystem at all
Section titled “1. The smallest useful call — skills as data, no filesystem at all”import assert from "node:assert"import { loadSkills } from "toolnexus"
const source = loadSkills({ skills: [ { name: "refund-policy", description: "How to process a refund", content: "Refunds within 30 days..." }, ],})
assert.deepEqual(Object.keys(source.skills), ["refund-policy"])assert.equal(source.skills["refund-policy"].location, "skill://refund-policy/", "a logical, not filesystem, base")
const res = await source.tool.execute({ name: "refund-policy" })assert.ok(res.output.includes("Refunds within 30 days"))
console.log("ok:", Object.keys(source.skills).join(", "))2. Directory + data together, narrowed by an allowlist
Section titled “2. Directory + data together, narrowed by an allowlist”The shared examples/skills fixture (one skill: hello-world) merged with a data-supplied skill,
then narrowed to just one of the two for this particular agent.
import assert from "node:assert"import { loadSkills } from "toolnexus"
const everything = loadSkills({ dirs: "examples/skills", skills: [{ name: "refund-policy", description: "How to process a refund", content: "..." }],})assert.deepEqual(Object.keys(everything.skills).sort(), ["hello-world", "refund-policy"])
// Same sources, but this agent may only use refund-policy.const narrowed = loadSkills({ dirs: "examples/skills", skills: [{ name: "refund-policy", description: "How to process a refund", content: "..." }], filter: { "refund-policy": true },})assert.deepEqual(Object.keys(narrowed.skills), ["refund-policy"])
// The tool still reports what IS available when asked for a filtered-out name.const miss = await narrowed.tool.execute({ name: "hello-world" })assert.equal(miss.isError, true)assert.ok(miss.output.includes("Available skills: refund-policy"))
console.log("ok:", Object.keys(everything.skills).join(","), "->", Object.keys(narrowed.skills).join(","))3. The full surface — resources, a drop-list filter, and the sample cap
Section titled “3. The full surface — resources, a drop-list filter, and the sample cap”A data skill can declare its own resources list (surfaced in <skill_files> exactly like a
directory skill’s sampled siblings). filter also works as a drop-list when every value is
false. sampleLimit: -1 omits <skill_files> entirely.
import assert from "node:assert"import { loadSkills } from "toolnexus"
const source = loadSkills({ dirs: "examples/skills", skills: [ { name: "deploy", description: "Ship a release", content: "Run the deploy pipeline.", resources: ["runbook.md", "rollback.md"], base: "internal://deploy-skill/", }, ], // Drop-list form: only `false` entries are named, everything else stays on. filter: { "hello-world": false }, sampleLimit: 5,})
assert.deepEqual(Object.keys(source.skills).sort(), ["deploy"], "hello-world dropped, deploy kept")
const res = await source.tool.execute({ name: "deploy" })assert.ok(res.output.includes("Base directory for this skill: internal://deploy-skill/"))assert.ok(res.output.includes("<file>runbook.md</file>"))assert.ok(res.output.includes("<file>rollback.md</file>"))
// sampleLimit: -1 would instead omit <skill_files> — including for data skills with resources.const noFiles = loadSkills({ skills: [{ name: "x", content: "body", resources: ["a.md"] }], sampleLimit: -1 })const xRes = await noFiles.tool.execute({ name: "x" })assert.ok(!xRes.output.includes("<skill_files>"))
console.log("ok:", Object.keys(source.skills).join(","), "| files omitted:", !xRes.output.includes("<skill_files>"))Options
Section titled “Options”| Field | Type | What it does |
|---|---|---|
dirs |
string | string[] |
Folders globbed for **/SKILL.md, same as the bare-string/array form. |
skills |
SkillDef[] |
Skills supplied as data. Merges with dirs results; first-name-wins on collision. |
filter |
Record<string, boolean> |
Per-agent allowlist/drop-list keyed on skill name (§3 S2). Unknown names warn once. |
sampleLimit |
number |
Sibling-file sample cap: 0/omitted ⇒ default 10, n > 0 ⇒ cap at n, -1 ⇒ omit <skill_files> entirely (§3 S5). |
SkillDef fields
Section titled “SkillDef fields”| Field | Required | What it does |
|---|---|---|
name |
yes | Keys the skill; missing name ⇒ skipped, same as an on-disk SKILL.md with no name:. |
description |
no | Shown in the catalog (prompt()). A skill with no description never appears there. |
content |
yes | The body injected into <skill_content> when the model loads this skill. |
resources |
no | Logical file list surfaced in <skill_files> — only emitted when non-empty. |
base |
no | Logical base URI reported to the model. Defaults to skill://<name>/. |
See also
Section titled “See also”loadSkills— Glob a skills directory and expose one skill tool with progressive disclosure.listSkills— Enumerate discovered skills and, crucially, the ones that were skipped and why.createToolkit— Takesskills/skillProvider/skillsFilterand callsloadSkillsfor you.