Skip to content

Toolnexus.Skill.load (data, providers, filters)

Elixir · package toolnexus · SPEC §3 · elixir/lib/toolnexus/skill.ex

@spec load(String.t() | [String.t()] | keyword() | map()) :: Toolnexus.Skill.Source.t()
def load(input)
# options relevant here:
# :skills — [%{name:, description:, content:, resources:, base:}]
# :provider — (-> [skill-data]) resolved once
# :filter — %{name => boolean}

There is no separate load_with function in this port — Toolnexus.Skill.load/1 is one function that accepts :dirs, :skills, :provider and :filter together, and merges whatever you give it into a single skill source. This page is scoped to the three options Skill.load (directories) doesn’t cover: supplying skills as data, supplying them from a lazy provider, and narrowing the result with an allowlist.

  • Skills that don’t live on disk — generated at runtime, pulled from a database, assembled from a config file. :skills takes the same shape load/1 would otherwise parse out of a SKILL.md, minus the parsing.
  • Skills that are expensive to enumerate:provider is a 0-arity function resolved exactly once, so you can defer a database call or an API fetch until load/1 actually runs, without eagerly building the list every time your module loads.
  • The same catalog, narrowed per agent:filter is a name => boolean allowlist/droplist applied after directories, :skills and :provider are all merged, so one skill inventory can back several agents with different subsets.

1. Skills supplied as data — no disk, no SKILL.md

Section titled “1. Skills supplied as data — no disk, no SKILL.md”

A data-sourced skill gets origin: :logical and a skill://name/ base instead of a file:// directory — nothing here ever touches the filesystem.

alias Toolnexus.{Context, Skill}
source =
Skill.load(
skills: [
%{
name: "changelog",
description: "Draft a changelog entry from a diff",
content: "1. Summarize the diff.\n2. Pick a Conventional Commits type.",
resources: ["templates/entry.md", "templates/breaking.md"]
}
]
)
[info] = source.skills
true = info.name == "changelog"
true = info.origin == :logical
true = info.base == "skill://changelog/"
res = source.tool.execute.(%{"name" => "changelog"}, %Context{})
false = res.is_error
true = String.contains?(res.output, "Base directory for this skill: skill://changelog/")
true = String.contains?(res.output, "<file>templates/entry.md</file>")
IO.puts("ok: #{info.name} (#{info.origin}) -> #{info.base}")

2. A lazy :provider, resolved once, isolated on failure

Section titled “2. A lazy :provider, resolved once, isolated on failure”

:provider is called exactly once when load/1 runs. If it raises or returns something that isn’t a list, the failure is isolated with a warning — exactly like one MCP server failing to connect never takes the others down.

alias Toolnexus.Skill
calls = :counters.new(1, [])
provider = fn ->
:counters.add(calls, 1, 1)
[%{name: "triage", description: "Triage an incoming bug report", content: "1. Reproduce it."}]
end
source = Skill.load(provider: provider)
true = Enum.map(source.skills, & &1.name) == ["triage"]
true = :counters.get(calls, 1) == 1
# A failing provider does not crash load/1 — it just contributes nothing.
failing = fn -> raise "upstream unavailable" end
empty_source = Skill.load(provider: failing)
[] = empty_source.skills
true = empty_source.tool.name == "skill"
IO.puts("ok: provider called #{:counters.get(calls, 1)}x, failure isolated")

3. :filter narrows a merged directory + data catalog

Section titled “3. :filter narrows a merged directory + data catalog”

Directories and data skills merge first; :filter is applied to the merged result, so it can drop names from either source alike. has_true semantics: if the filter contains at least one true, it becomes an allowlist (only those survive); otherwise it’s a droplist (only false entries are removed).

alias Toolnexus.Skill
repo = System.get_env("TOOLNEXUS_REPO") || "."
dir = Path.join([repo, "examples", "skills"])
config = [
dirs: [dir],
skills: [
%{name: "triage", description: "Triage a bug report", content: "1. Reproduce it."},
%{name: "release", description: "Cut a release", content: "1. Bump the version."}
]
]
# No filter: everything from every source.
all = Skill.load(config)
true = all.skills |> Enum.map(& &1.name) |> Enum.sort() == ["hello-world", "release", "triage"]
# Allowlist mode: at least one `true` present, so only those survive.
allow = Skill.load(Keyword.put(config, :filter, %{"triage" => true}))
true = Enum.map(allow.skills, & &1.name) == ["triage"]
# Droplist mode: no `true` present, so only `false` entries are removed.
drop = Skill.load(Keyword.put(config, :filter, %{"release" => false}))
true = drop.skills |> Enum.map(& &1.name) |> Enum.sort() == ["hello-world", "triage"]
IO.puts("ok: all=#{length(all.skills)} allow=#{length(allow.skills)} drop=#{length(drop.skills)}")
Option Default What it does
:skills [] Skills as data: %{name:, description:, content:, resources:, base:}. resources is the sibling-file list the skill tool samples from (no disk walk); base defaults to skill://<name>/.
:provider nil A 0-arity function returning a list in the same shape as :skills, resolved exactly once. A raise, or a non-list return, is isolated with a stderr warning — the rest of the source still loads.
:filter nil %{name => boolean}. nil/%{} means all. If any value is true, the filter is an allowlist (only true names survive); otherwise it’s a droplist (only false names are dropped). A name that matches no skill logs a warning. Identical semantics to the MCP tools filter and the builtins tools toggle map.