OmniRoute Skills System Architecture for Extensible Functionality Explained

OmniRoute implements a registry-executor-handler pattern that isolates custom logic in versioned, schema-validated skills with SQLite-backed persistence and audit trails.

OmniRoute’s extensible functionality is powered by a modular skills system architecture that treats custom business logic as plug-in units. This TypeScript framework enables developers to register reusable handlers, enforce input/output contracts via JSON schemas, and execute code within a sandboxed, observable environment. The architecture separates skill metadata management from execution, allowing the routing engine, MCP tools, and A2A protocol to safely invoke third-party capabilities without compromising core stability.

Core Components of the Skills Architecture

Skill Definition and Schema Validation

At the foundation of the system lies the Skill interface defined in src/lib/skills/types.ts. This contract specifies every skill’s name, version, JSON schema (SkillSchema), and a reference to its handler function. The schema strictly defines expected input and output shapes, enabling validation before execution reaches custom code.

Skill records also carry metadata including enablement status, execution mode, tags, and source provider. This declarative approach ensures that the routing engine can inspect capabilities without loading implementation code, supporting features like semantic version resolution and dependency checking.

The SkillRegistry Singleton

The SkillRegistry class in src/lib/skills/registry.ts operates as a singleton that maintains an in-memory cache of all registered skills. It implements a sophisticated caching strategy:

  • Key structure: Cache entries are keyed by apiKeyId:name@version, ensuring per-API-key isolation.
  • Lazy loading: Skills are loaded on demand from the SQLite skills table via loadFromDatabase, with deduplication of concurrent loads using a pendingLoad promise.
  • TTL-based staleness: The registry supports configurable cacheTTL to refresh data without blocking requests.
  • Version resolution: The resolveVersion method supports semantic constraints (e.g., ^1.2.0, ~2.0.3) to select compatible skill versions dynamically.

The registry exposes CRUD APIs including register, unregister, list, and getSkill, automatically rebuilding internal version caches when mutations occur.

The SkillExecutor and Audit Layer

The SkillExecutor singleton in src/lib/skills/executor.ts serves as the secure runtime boundary for skill code. Before invoking any handler, it verifies that the global skillsEnabled flag is active and that the target skill is both registered and enabled.

Execution is fully audited: the executor inserts a row into the skill_executions table to track start time, input payload, and status, then updates this record with output data, error messages, and duration upon completion. The system supports configurable timeouts (setTimeout) and retry counts (setMaxRetries), with handler resolution occurring via a Map<string, SkillHandler> where keys correspond to handler names stored in skill records.

Runtime Extension Mechanisms

Handler Registration and Built-ins

Skill handlers are ordinary async functions receiving a validated input object and a context containing apiKeyId and optional sessionId. They return plain objects conforming to the declared output schema. Runtime registration occurs via skillExecutor.registerHandler(name, fn).

Built-in handlers (e.g., browser, a2a) reside in src/lib/skills/builtins.ts and auto-register on server startup. This pattern allows the core team to ship default capabilities while maintaining the same extension interface available to third-party developers.

Interception and Injection Hooks

The architecture supports aspect-oriented extensions through two specialized modules:

These hooks extend functionality without modifying core routing logic, adhering to the open/closed principle.

Persistence and Configuration

SQLite Storage Layer

All skill metadata and execution logs persist in SQLite tables (skills and skill_executions). Database access is abstracted through src/lib/db/core.ts, which exports a singleton better-sqlite3 instance. This design ensures ACID-compliant writes and fast reads for the registry cache, with table-specific logic in src/lib/db/skills.ts.

Global Settings and API Key Isolation

Global toggles like skillsEnabled are stored in the settings table and accessed via getSettings() from src/lib/db/settings.ts. Per-API-key scoping is enforced by passing apiKeyId arguments throughout the registry and executor methods, ensuring that skills registered for one tenant remain invisible to others.

Practical Implementation Examples

Registering a New Skill

import { skillRegistry } from "./src/lib/skills/registry";

await skillRegistry.register({
  name: "weather",
  version: "1.0.0",
  description: "Fetch current weather for a city",
  schema: {
    input: { city: "string" },
    output: { temperature: "number", condition: "string" },
  },
  handler: "weatherHandler",
  enabled: true,
  apiKeyId: "public",
});

Registering the Corresponding Handler

import { skillExecutor } from "./src/lib/skills/executor";

skillExecutor.registerHandler("weatherHandler", async (input, ctx) => {
  const city = (input as any).city;
  const data = await fetch(`https://api.weather.example.com/${encodeURIComponent(city)}`).then(r => r.json());
  return { temperature: data.temp_c, condition: data.condition };
});

Executing a Skill via MCP Toolset

import { skillExecutor } from "./src/lib/skills/executor";

const execution = await skillExecutor.execute(
  "weather@1.0.0",
  { city: "Berlin" },
  { apiKeyId: "user-123", sessionId: "sess-abc" }
);

console.log(execution.output);

Listing Skills for a Specific API Key

import { skillRegistry } from "./src/lib/skills/registry";

const mySkills = skillRegistry.list("user-123");
mySkills.forEach(s => console.log(`${s.name}@${s.version}`));

Resolving Semantic Versions

const skill = skillRegistry.resolveVersion("weather", "^1.0.0", "user-123");
console.log(skill?.version);

Summary

  • OmniRoute employs a registry-executor-handler pattern that decouples skill metadata from execution logic.
  • The SkillRegistry in src/lib/skills/registry.ts provides in-memory caching with per-API-key isolation and semantic version resolution.
  • The SkillExecutor in src/lib/skills/executor.ts enforces timeouts, retries, and comprehensive audit logging to skill_executions.
  • Skills are defined via strict TypeScript interfaces in src/lib/skills/types.ts with JSON schema validation for inputs and outputs.
  • Runtime extensibility is achieved through handler registration, interception hooks, and injection points without modifying core routing code.
  • SQLite persistence via src/lib/db/core.ts ensures ACID compliance, while global settings in src/lib/db/settings.ts control feature enablement.

Frequently Asked Questions

How does OmniRoute handle versioning for skills?

OmniRoute supports semantic versioning through the SkillRegistry.resolveVersion method in src/lib/skills/registry.ts. Developers can register multiple versions of the same skill, and the system resolves compatible versions using standard constraints like caret (^) and tilde (~) ranges. The cache keys use the format apiKeyId:name@version, allowing precise version isolation per tenant.

What is the difference between the SkillRegistry and SkillExecutor?

The SkillRegistry manages metadata: it caches skill definitions, handles CRUD operations, and resolves versions from the SQLite skills table. The SkillExecutor handles runtime concerns: it validates that skills are enabled, manages execution timeouts and retries, invokes the actual handler functions, and writes audit records to skill_executions. This separation ensures that looking up a skill is lightweight and safe, while executing it occurs within a controlled, observable sandbox.

How does the skills system ensure isolation between different API keys?

Isolation is enforced at the registry level by keying cache entries with apiKeyId:name@version strings. When calling skillRegistry.list() or resolveVersion(), the apiKeyId parameter filters the query scope. Similarly, the SkillExecutor passes the apiKeyId through execution contexts, ensuring that handlers cannot access data belonging to other tenants. This design prevents cross-tenant skill leakage while allowing shared infrastructure.

Can skills modify HTTP requests and responses in the routing pipeline?

Yes, through the interception and injection modules. src/lib/skills/interception.ts allows skills to modify inbound requests before they reach downstream executors, while src/lib/skills/injection.ts enables augmentation of outbound responses. These hooks provide aspect-oriented extension points, letting developers implement cross-cutting concerns like logging, authentication, or payload transformation without altering core routing logic.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →