Skip to Content
SkillsThree skill modes

Three skill modes

A skill can be pure documentation, or it can ship its own tools. There are three ways to package a skill: plain markdown instructions (Mode A), a tools.py of Python tools (Mode B), or scripts declared in the frontmatter (Mode C). Start with Mode A; add tools only when the skill needs to do something the agent’s existing tools can’t. A single skill can combine all three.

Mode A: pure markdown

SKILL.md teaches the model how to use the tools it already has (read, write, bash, your @tool functions). The model issues those tool calls itself based on the body’s instructions.

skills/git-discipline/ └── SKILL.md
--- name: git-discipline description: Conventional Commits format, signed-off-by, and the pre-commit hook checklist. --- # Git discipline Use Conventional Commits format: type(scope): subject body Run `bash` with `pre-commit run --all-files` before any commit. Push with `git push --signed`. If pre-commit fails, fix the lint issues — never `--no-verify`.

The skill ships no code. The model already has bash from the agent setup; the skill’s body teaches it what commands to issue.

Mode B: tools.py ships @tool functions

Auto-discovered by filename presence. tools.py is imported when the skill is loaded (not at Skill(...) construction); its @tool functions are registered into the agent’s tool host at that point.

skills/greeter/ ├── SKILL.md └── tools.py
# skills/greeter/tools.py from loomflow import tool @tool async def say_hi(name: str) -> str: """Say hi.""" return f"Hi {name}!"
--- name: greeter description: Greet users by name. --- Always greet by name. Use the `greeter__say_hi` tool.

The model calls greeter__say_hi(name="Anupam") directly. In-process, fast, can share the agent’s state.

Lazy tools.py import

Skill('path/') does not import tools.py at construction time — the import is deferred until the first load_skill call inside the running agent loop. This keeps construction safe inside an already-running event loop:

# A tools.py with module-level event-loop work is imported lazily # inside the agent loop, where event-loop work is fine — so # Skill('skills/pdf/') is safe to construct from within a running # event loop (e.g. a Jupyter cell).

Internally, Skill.materialize_tools(ctx) does the import on first call and caches the result. The framework’s built-in load_skill tool calls materialize_tools from inside the agent’s run.

If tools.py calls asyncio.run(...) at module level (rare, and only in older skills), the resulting SkillError includes a hint pointing at the build_tools(ctx) factory pattern below instead of just the raw asyncio traceback.

Migration note. Tests that asserted “construction raises on a bad tools.py” now need to call skill.materialize_tools(ctx) to trigger the import. The error surfaces at load time, not construction.

build_tools(ctx) factory protocol

Skills can ship tools that close over caller-supplied state without globals. Define build_tools(ctx) in tools.py and the framework calls it with the live RunContext at load_skill time:

# skills/pdf-retrieval/tools.py from loomflow import tool def build_tools(ctx): # ctx is the live RunContext for this run. # ctx.metadata is whatever the caller passed via # agent.run(metadata={...}). vs = ctx.metadata["vectorstore"] @tool async def retriever(query: str) -> list: return await vs.search_hybrid(query=query) return [retriever]

Now any code that loads this skill can inject its own vector store:

agent = Agent("...", model="...", skills=["skills/pdf-retrieval/"]) result = await agent.run( "Find the 2024 financial report.", metadata={"vectorstore": my_chroma_store}, )

Resolution order:

  • If tools.py exports build_tools, the framework calls it with the active RunContext and uses the returned list.
  • Otherwise the framework falls back to discovering module-level @tool functions.

build_tools must return list[Tool]; anything else raises SkillError with a clear message pointing at the factory.

This is dependency injection for skill tools. Useful when:

  • The same skill ships in multiple agents that need different vector stores / DB connections / API clients.
  • You want to test a skill with a FakeVectorStore without rewriting its tools.py.
  • The skill’s tools reference state that isn’t available at module-import time.

Mode C: frontmatter declares a script as a typed tool

Any language. The framework wraps the script in a subprocess-backed Tool with proper args; the model calls it like any built-in tool.

skills/calc/ ├── SKILL.md └── scripts/ └── add.py
--- name: calc description: Arithmetic helpers. tools: add: description: Sum two integers. script: scripts/add.py args: a: type: string description: First int b: type: string description: Second int ---
# scripts/add.py — plain Python, no decorators import sys print(int(sys.argv[1]) + int(sys.argv[2]))

The model calls calc__add(a="2", b="3") → framework execs the script → captures stdout → returns to the model.

Mode C scripts can be Python, Bash, Node, anything that can be exec’d. Args become positional argv to the script.

Script tools are marked destructive, since they run arbitrary code: under StandardPermissions(mode=Mode.DEFAULT) (or ACCEPT_EDITS) each call goes to the approval handler, like bash. When you pass a sandbox as the agent’s tools=, skill tools are registered under that sandbox too, so its checks apply to them. Under an OSSandbox the script runs in the sandboxed child, which can read the skill’s own directory. (Before 0.17, script tools weren’t destructive and were registered beside the sandbox, outside its checks.)

Mode B tools run in the child too: under SubprocessSandbox or OSSandbox the child loads tools.py from its file, and an OS sandbox lets it read the skill’s directory. Closures returned by a build_tools(ctx) factory can’t cross a process boundary, so under those two sandboxes their calls fail with an error result; write them as module-level @tool functions, use FilesystemSandbox, or ship the logic as a Mode C script. (In 0.17.0 to 0.17.2 every Mode B tool failed under a process sandbox.)

Modes coexist

A single skill can ship all three. SKILL.md (the body), tools.py (Mode B), AND a tools: frontmatter block declaring scripts (Mode C). The modes don’t conflict. They layer.

When to use each mode.

  • Mode A for skills that are pure documentation (style guides, process recipes, output templates).
  • Mode B when you want fast in-process Python tools that share the agent’s runtime.
  • Mode C when you want language-agnostic scripts (a Bash linter, a Node compiler, a Python helper that depends on a separate venv) or when the tool already exists as a CLI you want to expose.
Last updated on