Skip to content

Author a skill

A skill teaches knaif a domain. It is a folder — declarative YAML at the top, a Python package underneath, and optionally a Rust crate beside it.

The core stays completely domain-agnostic. It loads skills, validates plans, expands intents, resolves variables, enforces safety and dispatches handlers. Everything specific to video, or documents, or whatever you are adding, lives in your bundle.

skills/<name>/
skill.yaml # manifest: name, display copy, deps, runtimes
tools.yaml # the tool registry the model reads
prompt.yaml # model-facing rules + curated examples
SPEC.md # human-facing spec, lead with System Requirements
python/ # Step / Intent classes + a Skill subclass + tests
native/ # Rust crate, if it ships in the native runtime
data/ # eval, train and safety corpora + the locked snapshot
eval/ # fixtures and skill-specific verifiers

Only skill.yaml, tools.yaml, python/__init__.py and python/handlers.py are strictly required. The rest is what turns a working folder into a skill worth shipping.

The declarative half sits at the top of the bundle because both runtimes read it. A tool’s name, arguments, keywords and safety metadata are declared exactly once, in YAML, so the Python and Rust implementations agree by construction rather than by discipline.

skills/my_skill/skill.yaml
name: my_skill
description: "One-line description."
tools: tools.yaml
skill_class: handlers.MySkill
prompt: prompt.yaml
display:
title: My Skill
tagline: "What it does, in one sentence a non-developer understands."
category: media
skills/my_skill/python/handlers.py
from knaif.handler_api import HandlerContext
from knaif.skill_base import Skill
from knaif.tool import Step
class MyToolStep(Step):
name = "my_tool"
def handle(self, args: dict, ctx: HandlerContext) -> dict:
return {"result": "..."}
class MySkill(Skill):
tools = [MyToolStep]

skill_class: resolves module-relative — handlers.MySkill means class MySkill in your handlers.py. The loader fails fast if a listed class is not a Step/Intent, has no matching tools.yaml entry, or collides on name.

Resolve paths from ctx.skill_dir, never __file__

Section titled “Resolve paths from ctx.skill_dir, never __file__”

ctx.skill_dir is always the bundle root, so handlers find profiles/, vocab.yaml and data/ from there. Resolving relative to a handler’s own __file__ breaks the moment the bundle layout changes or the skill is loaded from a different root — and it has no equivalent at all in the native runtime.

Authoring the handlers is step one of five. A skill is not done when it works; it is done when it is measured.

  1. Author the tools and handlers — this track.
  2. Evaluate. Write data/eval.jsonl, climb the verifier ladder, lock a snapshot. Evaluate a skill.
  3. Fine-tune, if routing needs it. Fine-tuning.
  4. Port to native, if it ships in the binary. Python to native.
  5. Publish — the catalog picks it up once it has a locked acceptance bar.

Steps 2 and 4 are where most of the real work is. Budget accordingly.

Steps and Intents The tool contract, and when one tool should become a workflow
The tool registry tools.yaml, argument schemas, keywords, public vs internal
Safety How the confirmation gate works, and how to classify honestly
Publishing display:, catalog stages, and what makes a skill appear