# OmniRoute Skills System Architecture for Extensible Functionality Explained

> Discover OmniRoute's skills system architecture. Learn how the registry-executor-handler pattern enables extensible functionality with isolated, versioned, and validated skills.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: architecture
- Published: 2026-07-12

---

**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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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:

- **[`src/lib/skills/interception.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/interception.ts)**: Allows skills to modify requests before they reach downstream executors (e.g., altering prompts or injecting headers).
- **[`src/lib/skills/injection.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/injection.ts)**: Enables response enrichment, letting skills augment output during translation phases.

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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/core.ts) ensures ACID compliance, while global settings in [`src/lib/db/settings.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/interception.ts) allows skills to modify inbound requests before they reach downstream executors, while [`src/lib/skills/injection.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/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.