Ratel Docs
Python SDK

Register skills

Register Markdown playbooks in a SkillCatalog and surface them through search_capabilities alongside tools.

Skills are Markdown playbooks (a deploy runbook, a debugging checklist) ranked by a separate BM25 corpus from tools. What a skill is and when to reach for one is on Tools vs MCP vs skills; Ratel's own coding-agent skills (including ratel-decompose-prompt, which extracts skills from a long system prompt) are on the Skillset.

Register a skill

import asyncio

from ratel_ai import Skill, SkillCatalog, get_skill_content_tool, search_capabilities_tool

skills = SkillCatalog()
asyncio.run(skills.register(
    Skill(
        id="vercel-deploy",
        name="vercel-deploy",
        description="How to deploy to Vercel: env vars, preview vs production, rollbacks.",
        tags=["deploy", "ship to production"],      # indexed for ranking
        tools=["vercel__deploy", "fs__read_file"],  # surfaced alongside the skill when it matches
        metadata={"stacks": ["next", "vercel"]},    # non-indexed context for higher-layer ranking
        body="## Deploying to Vercel\n1. ...",       # returned by get_skill_content_tool
    )
))

search = search_capabilities_tool(catalog, skills)  # 2nd arg → result gains a populated `skills` bucket
load = get_skill_content_tool(skills)               # id == "get_skill_content"

Only id, name, and description are required; tags, tools, metadata, and body are optional. get_skill_content({skillId}) returns {"body": ...}, or {"error": ..., "isError": True} for an unknown id.

How skills surface

Pass a SkillCatalog as the second argument to search_capabilities_tool and search returns the skills bucket alongside tools, each with its own result budget, so a relevant skill is never starved by matching tools. The agent pulls a skill's full body into context on demand via get_skill_content_tool (id get_skill_content, constant GET_SKILL_CONTENT_ID).

A skill can also declare the tools its instructions call: when the skill matches a query, those tools are pulled into the tools bucket (additively, deduped) so the agent gets the playbook and the tools it needs in one turn instead of a second search.

The SkillCatalog surface

SkillCatalog(trace=None, method="bm25", embedding=None) takes the same constructor keywords as ToolCatalog, and its method surface mirrors it over the skill corpus:

skills.search(query, top_k)   # → list[SkillHit] (.skill_id, .score, .rank, .fused); synchronous, BM25-only
skills.search_async(query, top_k)  # awaitable; any method; origin=, method= as on ToolCatalog
skills.has(skill_id)          # → bool
skills.get(skill_id)          # → Skill | None
skills.size()                 # → int
skills.invoke(skill_id)       # → str: the body; raises ValueError on an unknown id
skills.replace_all(iterable)  # → PendingReplace: the batch becomes the whole catalog

Order hits by .rank (0-based, stable across methods) and branch on .fused: True means .score is an RRF value (hybrid search, or the experimental adaptive usage arm), not a raw BM25 or cosine score.

record_event() and drain_trace_events() work exactly as on ToolCatalog. Note that invoke is synchronous here, no await: loading a skill body is a lookup, not an executor call. It records a skill_invoke trace event.

Replace the whole catalog

replace_all makes the batch the entire catalog (ADR 0015): ids absent from it are removed, including skills registered in-process. It exists for a source that fetches the full catalog rather than pushing deltas; a host that keeps local skills alongside a remote source composes the batch itself:

reload = skills.replace_all([*local_skills, *remote_skills])
reload.added, reload.removed, reload.updated, reload.unchanged  # final before awaiting
await reload  # drives the embedding pass — always await

Two-phase, exactly like register: the corpus swap lands synchronously, and the returned PendingReplace already carries the final counts — awaiting drives only the embedding pass and resolves to a plain ReplaceOutcome. Always await the result so an embedding failure raises instead of being swallowed.

On a failed pass the new corpus is already live and BM25 ranks it; semantic search raises embeddings-not-built until a later pass succeeds. A reload racing an in-flight operation — dense work, but also a plain search_async — is rejected with RuntimeError: registry busy rather than blended; retry it. Only new or re-worded skills are embedded, so reloading an unchanged catalog costs zero embedding calls.

PendingReplace and ReplaceOutcome are exported from ratel_ai. The tool corpus has no equivalent: register stays the only way into a ToolCatalog.

Embeddings for skills

The skill corpus has its own embedding cache, maintained exactly like the tool one (The embedding cache): construct SkillCatalog with method="semantic" or "hybrid" so await skills.register(...) embeds each skill; a BM25 catalog cannot be retrofitted. The two caches are independent: skills can rank hybrid while tools stay on BM25, or run a different embedding model.

Embeddings cover the same projection the ranking indexes (name, description, tags), never the body, so a long playbook costs one small embedding. When the engines are worth it is on Semantic & hybrid search.

Next steps

On this page