Skip to main content
Most agents ship with a hardcoded list of tools or skills. As your registry grows past a handful, the prompt bloats, the LLM gets confused, and you have no signal about which skills actually work. The Skill Router fixes both. It picks which skills to surface on every user turn based on the query, and it logs every decision so DecimalAI can compute which skills are pulling their weight — automatically.

How fast?

~250–350ms warm. Cache hit (multi-turn) is free. Adds ~10–25% to total turn latency.

What if it fails?

Every method degrades to an empty fragment + a warning log. Your agent never breaks on a Router hiccup.

What's the payoff?

Per-skill, per-model effectiveness scoring in the dashboard. The data is the moat.

Before / after

The shift: you stop choosing skills at agent-creation time and start choosing them at prompt-assembly time, based on what the user actually asked.

Integrate in 3 steps

1

Initialize the SDK

2

Install the framework adapter

Pick your framework. Each adapter is a one-line install with the same flag:
Skills are injected into every BaseChatModel.invoke() / ainvoke() as a SystemMessage. Works with LangChain agents, LangGraph nodes, and bare LLM calls.
3

Run your agent — telemetry flows automatically

Make a normal call. Open the DecimalAI dashboard. You’ll see:
  • Which skills the Router offered for each user turn
  • Which ones the LLM activated
  • Per-skill pass rate, per-(skill, model) effectiveness, trend over time
No additional instrumentation required. The framework adapter you installed in step 2 stamps the routing_id on every trace so the join can close server-side.

How it works (high level)

The single load-bearing detail: the same routing_id propagates from the Router through every LLM call in a turn to the trace. That join is what produces per-skill effectiveness scoring.

Routing strategies

The framework adapters default to smart route when a query is detectable in context, full menu otherwise. Smart route has one more trick: when your whole eligible menu fits the description token budget (~1,500 estimated tokens, max 30 rows), it returns the full menu instead of picking top-K — showing everything beats selection when everything is cheap, and it skips the embedding round-trip. You’ll see "strategy": "full_menu" in the response when this fires.

Response shape

Both get_menu() and smart_route() return the same envelope:
prompt_fragment is a ready-to-splice markdown block. routing_id is the join key you (or your adapter) attach to the resulting trace. Full-menu responses additionally carry the description-tier budget accounting:
desc_tokens is the estimated token cost of the rows shown; truncated: true means rows were dropped to stay under the budget (rows_total is how many were eligible).

Policy controls

Plan limits

The Router itself is Free. What scales with your plan is skill count and a few publishing / analytics capabilities: smart_route is Free because its value scales with skill count — a user with 8 skills doesn’t benefit; a user with 80 does, and at 80 they’ve already left the Free tier.
The server runs a hybrid retrieval pipeline against your registry:
  1. Embed the query with the production embedding model (currently text-embedding-004).
  2. Hybrid retrieve — combine semantic similarity (vector cosine on description_embedding) with lexical match (PostgreSQL full-text search on search_doc). Fuse with Reciprocal Rank Fusion (RRF) — parameter-free, no weight to tune.
  3. Re-rank by effectiveness — blend retrieval score with each skill’s historical pass rate. Skills with proven track records bubble up; new skills aren’t penalised until they have enough activations to be judged.
  4. Persist a routing_decision row so the offered-vs-activated join can close.
The hybrid step is why queries like “snake-language formatter” find Python skills (semantic) and queries like “python” find them too (lexical) — pure-dense retrieval misses one or the other.
Descriptions are the cheap, always-on tier. Bodies (the actual instructions) load on demand:
  1. The menu surfaces name + description rows, budgeted at ~1,500 estimated tokens / 30 rows.
  2. On the openai_agents and pydantic_ai adapters, enabling the skill loader also auto-registers a native load_skill(name) tool on every agent — no wiring on your side. The model reads the menu, calls load_skill("refund-policy"), and the skill’s full body arrives as the tool result. Parallel calls for multiple skills work.
  3. Body loads are budgeted per turn: at most 3 bodies, ~6,000 estimated tokens total, each body trimmed to 8 KB. Past the budget the tool returns a polite refusal the model can act on; re-loading an already-loaded skill is free.
The anthropic and langchain adapters patch a single create() / invoke() call — there’s no tool loop to route a result through — so they stay on prompt injection. Their inject_skill_body=True path applies the same trim + budget to injected bodies.The raw endpoint behind the tool is GET /api/v1/skills/{skill_name}/body, which accepts:The response’s version field is the concrete resolved version number. Skills you subscribe to from the public registry (Use) are fetchable through this endpoint like your own.Kill switch for the tool: decimalai.init(load_skill_tool=False) or DECIMALAI_LOAD_SKILL_TOOL=0.
The Router caches build_prompt_fragment() results in-process for 30 seconds by default. This matters because a single user turn often produces multiple LLM calls (tool-using agent loops, retries, sub-steps). Without caching, each call would re-route the same query, producing duplicate routing_decision rows and wasted embedding calls.With caching, every LLM call within a turn:
  • Hits the platform once (the first call)
  • Reuses the cached fragment and the same routing_id for all subsequent calls
  • Produces exactly one routing_decision row for the entire turn
Every routing call writes a record. Joined with traces, it produces:Per-model effectiveness is the single hardest signal for competitors to replicate — they don’t have your routing telemetry, so they can’t compute it.
Net effect: a Router failure never breaks your agent run. Worst case, the LLM falls back to whatever instructions it had pre-Router.
Or in Python:
  • Execute skills. Skills are markdown — they shape the LLM’s behavior. For code execution, see how skills differ from tools.
  • Decide which model to use. Model routing is separate. The Router operates within your chosen model.
  • Cache bodies across processes. The in-process cache is per-process. For shared caching, put a service in front.

Router vs disk auto-loading (pick one)

Some agent runtimes (Claude Code, Cursor) auto-discover SKILL.md files from .claude/skills/ and inject them into the system prompt themselves. The Router also injects skills into the system prompt — from the platform. Running both at once means the same skill ends up in the prompt twice, which confuses the LLM and inflates token cost. The fix is to pick one source of skill injection per agent process: The SDK auto-detects known disk runtimes via environment variables (CLAUDECODE, CLAUDE_CODE_ENTRYPOINT, CURSOR_AGENT) and logs a one-shot warning when enable_skill_loader=True fires inside one of them. Silence the warning intentionally with DECIMALAI_SUPPRESS_DISK_RUNTIME_WARNING=1 if you’ve consciously chosen the setup (e.g., a benchmark).

disk_sync=False — Router is the only source

When you’re running a Python stack that should rely solely on the platform for skill content, pass disk_sync=False to the framework adapter:
This skips:
  • Local SKILL.md auto-discovery (no disk read)
  • Push of local skills to the platform (no sync_skills)
  • Pull of platform-only skills to disk (no pull_missing)
The Router still calls the platform’s /api/v1/skills/route per turn; that’s the runtime injector. Everything happens over the network — nothing on disk. Available on decimalai.openai_agents.install() and decimalai.langchain.install(). The pydantic_ai and anthropic adapters never touched disk, so they don’t need the flag.