# How to Build Custom Skills with the OmniRoute Skills Framework Registry

> Learn to build custom skills with the OmniRoute Skills Framework Registry. Register reusable TypeScript functions with automatic validation and logging. Get started today!

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

---

**OmniRoute's Skills subsystem lets you register reusable, versioned functions by implementing a TypeScript handler, recording metadata in the SQLite-backed Skill Registry, and invoking them through the Skill Executor with automatic validation, timeout enforcement, and execution logging.**

OmniRoute (diegosouzapw/OmniRoute) ships with a production-ready **Skills** framework that transforms arbitrary JavaScript functions into discoverable, audited tools available to any LLM request. The architecture centers on the **Skill Registry** ([`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts)), which persists definitions to SQLite while maintaining an in-memory cache for high-performance lookups.

## The Three Components of a Skill

Every custom skill in OmniRoute consists of three coordinated pieces: the stored definition, the runtime handler, and the execution engine.

### Skill Registry ([`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts))

The registry is a singleton that manages the `skills` table in SQLite. It stores the **name**, **version**, **input/output schema**, **handler identifier**, and **enablement flag**. When the server starts, it loads all records into memory and provides lookup helpers to resolve `name@version` strings into runnable definitions.

### Skill Handler (User Implementation)

The handler is the actual JavaScript function that implements your business logic. It must conform to the `SkillHandler` interface defined in [`src/lib/skills/types.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/types.ts), accepting validated input and a context object containing `apiKeyId`, `sessionId`, and optional provider or model information. Handlers can live anywhere in your codebase.

### Skill Executor ([`src/lib/skills/executor.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/executor.ts))

The executor orchestrates the runtime. It validates requests against Zod schemas from [`src/lib/skills/schemas.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/schemas.ts), locates the registered handler, enforces timeouts and retries, and writes execution results to the `skill_executions` table. It exposes `registerHandler()`, `execute()`, `listExecutions()`, and `getExecution()` methods.

## Step-by-Step: Creating a Custom Skill

The following walkthrough creates a `weather_lookup` skill that accepts a location and units, then returns mock weather data.

### Step 1: Implement the Handler Function

Create a TypeScript file that exports a function matching the `SkillHandler` signature. Input validation is performed automatically by the executor before your handler receives the data.

```typescript
// src/lib/skills/customWeather.ts
import { SkillHandler } from "./types";

export const weatherLookupHandler: SkillHandler = async (
  input,
  { apiKeyId, sessionId }
) => {
  // Input is already validated by Zod; expected shape:
  // { location: string, units?: "metric" | "imperial" }
  const { location, units = "metric" } = input as {
    location: string;
    units?: "metric" | "imperial";
  };

  // Replace with a real API call in production
  const fakeData = {
    location,
    temperature: units === "metric" ? 22 : 71,
    units,
    description: "Partly cloudy",
  };
  return fakeData;
};

```

### Step 2: Register the Handler with the Executor

During application initialization (e.g., in [`src/lib/skills/init.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/init.ts)), bind the handler to a unique name. This name acts as the bridge between the registry definition and the implementation.

```typescript
// src/lib/skills/init.ts
import { skillExecutor } from "./executor";
import { weatherLookupHandler } from "./customWeather";

skillExecutor.registerHandler("weather_lookup", weatherLookupHandler);

```

### Step 3: Define the Skill in the Registry

You can create the skill record via the REST API (`POST /api/skills`) or programmatically using `skillRegistry.register()`. The programmatic approach is useful for migrations or bootstrap scripts.

```typescript
// src/lib/skills/createWeatherSkill.ts
import { skillRegistry } from "./registry";

await skillRegistry.register({
  name: "weather_lookup",
  version: "1.0.0",
  description: "Fetch current weather for a location",
  schema: {
    input: { location: "string", units: "string?" },
    output: { location: "string", temperature: "number", units: "string", description: "string" },
  },
  handler: "weather_lookup",          // Must match the name passed to registerHandler()
  enabled: true,
  apiKeyId: "system",                // Use "system" for global skills; specific API-key IDs for scoped skills
  mode: "on",
  sourceProvider: "local",
});

```

### Step 4: Execute and Monitor

Invoke the skill from any request pipeline using `skillExecutor.execute()`. Pass the skill identifier (optionally including version with `@`), the input payload, and execution metadata.

```typescript
import { skillExecutor } from "./executor";

const result = await skillExecutor.execute(
  "weather_lookup@1.0.0",                // name@version or just name for latest
  { location: "Berlin", units: "metric" },
  { apiKeyId: "user-123", sessionId: "sess-456" }
);

console.log(result.output);
/* → { location: "Berlin", temperature: 22, units: "metric", description: "Partly cloudy" } */

```

To audit or debug, query the execution history using the executor's built-in methods:

```typescript
const recent = await skillExecutor.listExecutions("user-123", 10);
console.log(recent.map((e) => ({
  id: e.id,
  status: e.status,
  durationMs: e.durationMs,
})));

```

## Type Safety and Schema Validation

All inputs and outputs are strictly type-checked using Zod schemas defined in [`src/lib/skills/schemas.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/schemas.ts), with TypeScript interfaces exported from [`src/lib/skills/types.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/types.ts). The executor automatically validates the input payload against the stored schema before invoking your handler, ensuring that only correctly shaped data reaches your business logic.

## Summary

- **Skill Registry** ([`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts)) persists skill metadata to SQLite and caches it in memory.
- **Skill Executor** ([`src/lib/skills/executor.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/executor.ts)) validates inputs, runs handlers, enforces timeouts, and logs to `skill_executions`.
- **Handlers** are registered once via `skillExecutor.registerHandler(name, fn)` and can reside in any file.
- **Definitions** are created via the REST API or `skillRegistry.register()`, linking a name to a handler and schema.
- **Execution** supports versioning (`name@version`), automatic validation, and detailed audit logging.

## Frequently Asked Questions

### Where are skill definitions stored in OmniRoute?

Skill definitions are stored in the `skills` table of the SQLite database managed by the Skill Registry ([`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts)). The registry maintains an in-memory cache of these records to avoid disk lookups during high-throughput execution.

### How does versioning work when I build custom skills?

When registering a skill, you specify a `version` string (e.g., `"1.0.0"`). When invoking `skillExecutor.execute()`, you can reference a specific version using the `name@version` syntax, or omit the version to use the latest enabled definition. This allows multiple versions of the same skill to coexist for backward compatibility.

### Can I disable a skill without deleting its code?

Yes. Each skill record includes an `enabled` boolean flag. Setting `enabled: false` in the registry (via the REST API or `skillRegistry.register()`) prevents the executor from invoking that skill, effectively taking it offline while preserving the definition and execution history.

### Where should I place my custom handler implementations?

Handlers can live anywhere in your codebase, but the convention in OmniRoute is to place them in `src/lib/skills/` or in domain-specific subdirectories. The built-in skills in [`src/lib/skills/builtins.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/builtins.ts) demonstrate this pattern. The only requirement is that you call `skillExecutor.registerHandler()` with the implementation before attempting to execute skills that reference it.