# How to Extend OmniRoute with Custom Skills: A Complete Developer Guide

> Learn how to extend OmniRoute with custom skills using the SkillRegistry API. This guide details secure sandbox execution and versioned skill handling for developers.

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

---

**Developers extend OmniRoute by registering custom skills through the SkillRegistry API ([`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts)), which validates, caches, and executes versioned skill handlers in a secure sandbox environment.**

The OmniRoute framework provides a **Skill framework** that enables developers to add custom business logic without modifying core code. Skills are stored in SQLite, cached in memory for performance, and executed through a secure sandbox. This article covers the complete workflow for creating, registering, and invoking custom skills based on the OmniRoute source code.

## Understanding the OmniRoute Skill Architecture

### Core Components

OmniRoute's skill system is built on three primary components:

- **SkillRegistry** ([`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts)) — A singleton that caches skills in memory, resolves versions, and handles database persistence
- **Custom Skill Helpers** ([`src/lib/skills/custom.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/custom.ts)) — Public API methods for registration, validation, listing, and deletion
- **Sandbox** ([`src/lib/skills/sandbox.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/sandbox.ts)) — An isolated execution environment for running skill handlers securely

### Skill Ownership and Visibility

The registry distinguishes between **global-owner skills** and **API-key-specific skills**:

| Owner Type | Examples | Visibility |
|------------|----------|------------|
| `system` | Built-in platform skills | All API keys |
| `skillsmp` / `skillssh` | Shared marketplace skills | All API keys |
| Custom API key ID | Developer-created skills | Only that API key |

This ownership model ensures appropriate scoping while allowing shared functionality through designated global owners.

## Step 1: Define a Custom Skill Schema

All skills must conform to `SkillCreateInputSchema`, re-exported as `CustomSkillSchema` from [`src/lib/skills/schemas.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/schemas.ts). The schema requires:

```typescript
// src/lib/skills/custom.ts
const payload = {
  name: "weather-summary",           // Unique identifier
  version: "1.0.0",                  // Semantic version
  description: "Summarize current weather",
  schema: {
    input: { location: "string" },   // Input validation schema
    output: { summary: "string" },   // Output shape declaration
  },
  handler: `
    export async function run({ input }) {
      const resp = await fetch(
        \`https://api.weather.com/v3/wx/conditions/current?location=\${input.location}\`
      );
      const data = await resp.json();
      return { summary: \`It is \${data.temperature}°C and \${data.narrative}.\` };
    }
  `,
  apiKeyId: "my-api-key",            // Owner identifier
  enabled: true,
};

```

The **handler** is stored as a string and executed in the sandbox. It must export a `run` function that receives an object with `input`, `context`, and `tools` properties.

## Step 2: Register the Skill with skillRegistry

Use `registerCustomSkill` from [`src/lib/skills/custom.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/custom.ts) to persist and cache the skill:

```typescript
// Server-side registration
import { registerCustomSkill } from "@/lib/skills/custom";

await registerCustomSkill(payload);

```

This performs three operations as implemented in [`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts):

1. **Validation** — Zod schema validation against `CustomSkillSchema`
2. **Database insertion** — Creates row in the `skills` table with JSON-serialized handler
3. **Cache update** — Adds entry to `versionCache` for immediate availability

The registry's `versionCache` is a `Map` keyed by `{name}@{resolvedVersion}` for O(1) lookups.

## Step 3: Invoke the Skill via API

Custom skills are accessible through the REST API layer. The route pattern follows `/api/v1/skills/run/[...slug]`:

```typescript
// Client-side invocation
const response = await fetch("/api/v1/skills/run/weather-summary@1.0.0", {
  method: "POST",
  headers: { 
    "Authorization": `Bearer ${MY_TOKEN}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ 
    input: { location: "NYC" } 
  })
});

const result = await response.json();
// { summary: "It is 22°C and partly cloudy with a chance of rain." }

```

The API route resolves the skill using `skillRegistry.getSkill(name, versionConstraint)` and executes it in the sandbox.

## Version Resolution and Caching Behavior

### Semantic Version Constraints

The registry supports expressive version matching through `resolveVersion` in [`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts):

| Operator | Meaning | Example Match |
|----------|---------|---------------|
| `^` | Compatible with major version | `^1.2.3` matches `1.3.0`, not `2.0.0` |
| `~` | Approximately equivalent | `~1.2.3` matches `1.2.5`, not `1.3.0` |
| `>` `>=` | Greater than (or equal) | `>=1.0.0` matches `2.0.0` |
| `<` `<=` | Less than (or equal) | `<2.0.0` excludes `2.0.0` |
| Exact | Specific version only | `1.2.3` matches only `1.2.3` |
| `latest` | Highest version available | Resolves to max semver |

Pass constraints to `getSkill`:

```typescript
import { skillRegistry } from "@/lib/skills/registry";

// Get exact version
const skill = skillRegistry.getSkill("weather-summary", "1.0.0");

// Get latest 1.x compatible version
const skill = skillRegistry.getSkill("weather-summary", "^1.0.0");

// Get absolute latest
const skill = skillRegistry.getSkill("weather-summary", "latest");

```

### Cache Invalidation

The registry implements **time-based cache invalidation** with a default TTL of 60 seconds. After TTL expiry:

- The registry reloads skills from the database
- `versionCache` and internal lookup maps are rebuilt
- Active executions complete with cached handlers; new requests use fresh data

## Managing Custom Skills

### List All Skills for an API Key

```typescript
import { listCustomSkills } from "@/lib/skills/custom";

const skills = listCustomSkills("my-api-key");
// Returns: [{ id, name, version, description, enabled, createdAt, updatedAt }, ...]

```

Returns metadata for all skills owned by the specified API key, excluding handler source code.

### Delete a Custom Skill

```typescript
import { deleteCustomSkill } from "@/lib/skills/custom";

await deleteCustomSkill("skill-uuid-here", "my-api-key");

```

This operation:

- Validates ownership (prevents deletion of `system` or other API keys' skills)
- Removes the database row
- Invalidates all cache entries for that skill name
- Returns `true` on success, throws `SkillNotFoundError` or `PermissionDeniedError` on failure

## Secure Execution in the Sandbox

The [`src/lib/skills/sandbox.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/sandbox.ts) module provides **isolated execution**:

```typescript
// Simplified internal flow from src/lib/skills/sandbox.ts
export async function executeSkill(
  skill: Skill,
  input: unknown,
  context: ExecutionContext
): Promise<unknown> {
  // 1. Create isolated VM context with limited globals
  const sandbox = createSandbox({
    console: createRestrictedConsole(),
    fetch: createRateLimitedFetch(skill.apiKeyId),
    Buffer,
    TextEncoder,
    TextDecoder,
  });

  // 2. Compile handler with strict mode enabled
  const script = new VM.Script(
    `"use strict";\n${skill.handler}`,
    { timeout: 30000 } // 30 second execution limit
  );

  // 3. Execute and return result
  const runFn = script.runInNewContext(sandbox);
  return await runFn({ input, context, tools: sandbox.tools });
}

```

Security features include:
- **No access** to `process`, `require`, or file system
- **Timeout enforcement** (configurable, default 30s)
- **Fetch rate limiting** per API key
- **Memory limits** on VM context size

## Complete Custom Skill Example

Here's a production-ready custom skill that integrates with an external CRM:

```typescript
// Custom skill: sync-lead-to-crm
const crmSyncSkill = {
  name: "sync-lead-to-crm",
  version: "1.2.0",
  description: "Create or update lead in external CRM system",
  schema: {
    input: {
      email: "string",
      company: "string?",
      score: "number?"
    },
    output: {
      success: "boolean",
      crmId: "string?",
      error: "string?"
    }
  },
  handler: `
    export async function run({ input, context, tools }) {
      const { email, company, score } = input;
      
      // Rate-limited fetch provided by sandbox
      const response = await fetch(context.env.CRM_API_URL + "/leads", {
        method: "POST",
        headers: {
          "Authorization": \`Bearer \${context.env.CRM_API_KEY}\`,
          "Content-Type": "application/json"
        },
        body: JSON.stringify({
          email,
          company: company || null,
          lead_score: score || 0,
          source: "omniroute-automation",
          metadata: { executionId: context.executionId }
        })
      });
      
      if (!response.ok) {
        const error = await response.text();
        return { success: false, error: \`CRM error: \${error}\` };
      }
      
      const data = await response.json();
      return { success: true, crmId: data.id };
    }
  `,
  apiKeyId: "org_abc123",
  enabled: true
};

// Registration
await registerCustomSkill(crmSyncSkill);

// Invocation with context variables
await fetch("/api/v1/skills/run/sync-lead-to-crm@^1.0.0", {
  method: "POST",
  headers: { "Authorization": "Bearer token_here" },
  body: JSON.stringify({
    input: { 
      email: "contact@example.com", 
      company: "Acme Inc",
      score: 85 
    },
    context: {
      env: {
        CRM_API_URL: "https://api.salesforce.com",
        CRM_API_KEY: process.env.CRM_API_KEY // Injected server-side
      }
    }
  })
});

```

## Summary

- **Define skills** using `CustomSkillSchema` with name, version, I/O schemas, and handler source code
- **Register via** `registerCustomSkill()` in [`src/lib/skills/custom.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/custom.ts) to persist to SQLite and cache in memory
- **Resolve versions** flexibly using semver operators (`^`, `~`, `>`, `>=`, exact, `latest`) through `skillRegistry.getSkill()`
- **Execute safely** through the sandbox in [`src/lib/skills/sandbox.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/sandbox.ts) with timeout, rate limiting, and restricted globals
- **Manage lifecycle** with `listCustomSkills()` and `deleteCustomSkill()` for API-key-scoped operations

## Frequently Asked Questions

### What is the performance overhead of custom skill execution?

Custom skills execute in a VM sandbox with ~5-10ms initialization overhead per call. The `versionCache` in [`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts) eliminates database lookups for skill resolution, making repeated calls to the same skill version as fast as direct function invocation. Cache TTL defaults to 60 seconds, balancing freshness with performance.

### Can I share custom skills across multiple API keys?

Direct sharing between arbitrary API keys is not supported for security isolation. However, skills registered with global owners (`system`, `skillsmp`, `skillssh`) are visible to all API keys. Enterprise deployments can request `skillsmp` status through the platform administration interface for approved shared skills.

### How do I debug a custom skill that fails in production?

Enable detailed logging by setting `DEBUG=omniroute:sandbox` in environment variables. The sandbox in [`src/lib/skills/sandbox.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/sandbox.ts) captures `console.log` output and returns it in the error response when `NODE_ENV=development`. For production, use the `context.executionId` passed to handlers to correlate with platform logs containing full stack traces.

### What happens if I register a skill with the same name and version?

The registry enforces unique constraints on `{name, version, apiKeyId}`. Attempting to register a duplicate throws `DuplicateSkillError` from [`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts). Use `updateCustomSkill()` (if available in your OmniRoute version) or deregister first with `deleteCustomSkill()` followed by re-registration with incremented version.