# How to Create and Register Custom Skills in OmniRoute's Framework

> Learn to create and register custom skills in OmniRoute's framework. Implement the Skill interface, add to the registry, and define Zod schemas for validation.

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

---

**To create and register custom skills in OmniRoute, implement the `Skill` interface defined in [`src/lib/skills/types.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/types.ts), add your skill to the registry in [`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts), and optionally define a Zod input schema for validation.**

OmniRoute's skill system is an extensible plug-in framework that lets developers add new capabilities invokable from the router's API or MCP tools. This guide walks through the complete workflow based on the `diegosouzapw/OmniRoute` source code, from defining a skill's contract to making it discoverable across the application.

---

## Understanding the Skill Architecture

Before writing code, it helps to understand how OmniRoute organizes its skill layer. The framework separates concerns across five core files:

| Component | Purpose | Source Location |
|-----------|---------|-----------------|
| **Skill interface** | TypeScript contract every skill must satisfy | [`src/lib/skills/types.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/types.ts) |
| **Skill registry** | Singleton map holding all registered skills | [`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts) |
| **Schema definitions** | Zod schemas for input validation | [`src/lib/skills/schemas.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/schemas.ts) |
| **Execution wrapper** | Sandboxing, error handling, and remote execution | [`src/lib/skills/executor.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/executor.ts) |
| **Built-in skills** | Reference implementations (browser, githubCollector, a2a) | [`src/lib/skills/builtins.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/builtins.ts) |

The registry aggregates skills from [`builtins.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/builtins.ts) plus any custom additions, then exposes `getSkill(name)` and `listSkills()` for the router and MCP server to consume.

---

## Step 1: Create a Skill File

Create a new TypeScript file under `src/lib/skills/`. The convention uses camelCase filenames matching your skill name.

```typescript
// src/lib/skills/helloWorld.ts
import { Skill, SkillContext } from '@/lib/skills/types';
import { z } from 'zod';

export const helloWorldSchema = z.object({
  name: z.string().default('World'),
});

export const helloWorld: Skill = {
  name: 'helloWorld',
  description: 'Returns a friendly greeting.',
  inputSchema: helloWorldSchema,
  async execute(input, _ctx: SkillContext) {
    const { name } = input;
    return { greeting: `Hello, ${name}!` };
  },
};

```

Every skill requires four properties:

- **`name`** — unique identifier used for lookups
- **`description`** — human-readable summary for tool listings
- **`inputSchema`** — optional but strongly recommended Zod schema
- **`execute`** — async function receiving validated input and context

The `SkillContext` parameter provides runtime dependencies like logger, configuration, and sandbox controls.

---

## Step 2: Define Input Validation Schemas

For complex inputs, define a dedicated schema in [`src/lib/skills/schemas.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/schemas.ts) or co-locate it with your skill:

```typescript
// src/lib/skills/schemas.ts (or myCustomSkill.ts)
import { z } from 'zod';

export const mySkillSchema = z.object({
  url: z.string().url(),
  timeoutMs: z.number().int().positive().default(5000),
  retries: z.number().int().min(0).max(3).default(0),
});

```

Using `zod` enables automatic validation, type inference, and OpenAPI-compatible JSON Schema generation for MCP tool definitions.

---

## Step 3: Register Your Skill in the Registry

Open [`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts) and import your skill into the `builtinSkills` array:

```typescript
// src/lib/skills/registry.ts
import { helloWorld } from './helloWorld';
import { myCustomSkill } from './myCustomSkill';
import { builtinSkills as existingBuiltins } from './builtins';

export const builtinSkills = [
  ...existingBuiltins,
  helloWorld,
  myCustomSkill,
];

```

The registry builds its internal map from this exported array. Skills are registered at import time, making them immediately available to:

- Router strategies resolving skills by name
- MCP server tool listings
- API endpoints using `getSkill()`

---

## Step 4: Execute Skills Programmatically

Once registered, invoke skills through the `SkillExecutor`:

```typescript
import { getSkill } from '@/lib/skills/registry';
import { SkillExecutor } from '@/lib/skills/executor';

async function handleRequest(req: Request) {
  const skill = getSkill('helloWorld');
  if (!skill) {
    throw new Error('Skill not found');
  }
  
  const result = await SkillExecutor.execute(skill, req.body);
  return result;
}

```

The executor handles:

- Input validation against `inputSchema`
- Error boundary catches and formatting
- Sandbox context injection
- Optional remote execution flags

---

## Exposing Skills via MCP Tools

To make your skill callable from MCP clients, no additional code is typically required. The framework in `open-sse/mcp-server/tools/` automatically forwards incoming tool calls to `SkillExecutor.execute` using the skill name as the tool identifier.

Ensure your skill's `name` matches the MCP tool name and that `inputSchema` produces a valid JSON Schema for the MCP protocol.

---

## Summary

- **Implement `Skill`** from [`src/lib/skills/types.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/types.ts) with `name`, `description`, `inputSchema`, and `execute`
- **Add Zod schemas** for robust input validation and type safety
- **Register in [`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts)** by importing into the `builtinSkills` array
- **Execute via `SkillExecutor`** for sandboxed, error-handled invocation
- **Expose automatically** through MCP when following naming conventions

---

## Frequently Asked Questions

### What interface must a custom skill implement?

The `Skill` interface in [`src/lib/skills/types.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/types.ts) requires `name: string`, `description: string`, `inputSchema?: ZodTypeAny`, and `execute(input: any, ctx: SkillContext): Promise<any>`. All properties except `inputSchema` are mandatory.

### Where do I register a new skill so the router can find it?

Register custom skills in [`src/lib/skills/registry.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts) by importing them and adding them to the exported `builtinSkills` array. The registry singleton provides `getSkill(name)` lookups used throughout the application.

### Can I skip defining an input schema?

Yes, but it's discouraged. Without `inputSchema`, your skill receives raw unvalidated input. The `SkillExecutor` will still run the skill, but type safety and automatic MCP schema generation are lost.

### How do built-in skills differ from custom skills?

Functionally, nothing. Both implement the same `Skill` interface. Built-in skills live in [`src/lib/skills/builtins.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/builtins.ts) and ship with OmniRoute; custom skills follow identical patterns in separate files you create.