# How to Develop and Register a Custom A2A Skill for OmniRoute's Agent Protocol

> Learn to develop and register custom A2A skills for OmniRoute's agent protocol. Follow our guide to extend OmniRoute with your own capabilities easily.

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

---

**To add a custom A2A skill to OmniRoute, create a TypeScript module under `src/lib/a2a/skills/` that exports an `execute` function returning a `Promise<StreamTaskResult>`, then register it in the `A2A_SKILL_HANDLERS` map inside [`src/lib/a2a/taskExecution.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/taskExecution.ts).**

OmniRoute implements its Agent-to-Agent (A2A) communication layer as a JSON-RPC 2.0 server with a modular, extensible skill framework. This architecture allows developers to extend the platform's capabilities by implementing custom logic that can be invoked remotely by other agents in the network.

## Understanding the A2A Skill Architecture

The A2A layer in OmniRoute treats skills as discrete units of work that accept a task input and return a stream of artifacts. According to the source code in [`src/lib/a2a/taskExecution.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/taskExecution.ts) (lines 20-44), each skill must adhere to a specific contract: it receives an `A2ATask` object containing the request payload and must return a `Promise<StreamTaskResult>`. The result object contains an `artifacts` array—where each artifact has a `type` and `content`—and an optional `metadata` map for additional context. The framework handles streaming these results back to the caller via Server-Sent Events (SSE) through [`src/lib/a2a/streaming.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/streaming.ts).

## Step 1 – Create the Skill Module

### File Location and Naming Convention

Create a new TypeScript file in `src/lib/a2a/skills/`. Name the file descriptively (e.g., [`yourSkill.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/yourSkill.ts)). This directory already contains built-in implementations like [`smartRouting.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/smartRouting.ts) and [`quotaManagement.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/quotaManagement.ts) that serve as reference implementations.

### Implementing the Execute Function

Export a function named `execute<YourSkillName>` that accepts an `A2ATask` and returns a `Promise<StreamTaskResult>`. Import the type from `@/lib/a2a/taskManager` to ensure type safety.

```typescript
// src/lib/a2a/skills/example.ts
import type { A2ATask } from "@/lib/a2a/taskManager";

/** A2A Skill: Example – echoes back the input */
export async function executeExample(task: A2ATask) {
  const input = task.input ?? {};
  const result = {
    artifacts: [{ 
      type: "message", 
      content: `You sent: ${JSON.stringify(input)}` 
    }],
    metadata: { 
      echoedAt: new Date().toISOString() 
    },
  };
  return result;
}

```

The `artifacts` array drives the response stream. Each artifact must contain a `type` string and a `content` string. The optional `metadata` object can include any contextual information you want to return alongside the artifacts.

## Step 2 – Register the Skill Handler

Open [`src/lib/a2a/taskExecution.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/taskExecution.ts) and locate the `A2A_SKILL_HANDLERS` record. This map connects JSON-RPC method names to their executing functions. Add a new entry that lazy-imports your module and invokes your exported function:

```typescript
export const A2A_SKILL_HANDLERS: Record<string, A2ASkillHandler> = {
  // …existing handlers…
  "example": async (task) => {
    const mod = await import("./skills/example");
    return mod.executeExample(task);
  },
};

```

The lazy-loading pattern (`await import()`) ensures that your skill code is only loaded into memory when invoked, keeping the server's startup footprint small. The key you choose (e.g., `"example"`) becomes the JSON-RPC method name that clients use to invoke the skill.

## Step 3 – Expose in the Agent Skill Catalog (Optional)

If you want other agents to discover your skill through the public Agent Skills catalog (accessible at [`/.well-known/agent.json`](https://github.com/diegosouzapw/OmniRoute/blob/main//.well-known/agent.json)), you must register it in the skill catalog system. This step is strictly for discovery purposes; the A2A server will function without it.

Add your skill definition to either [`src/lib/agentSkills/catalog.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/agentSkills/catalog.ts) or [`src/lib/agentSkills/types.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/agentSkills/types.ts) to ensure the catalog generator includes it in the output. This enables the built-in `list-capabilities` skill to expose your new functionality to the network.

## Testing Your Custom Skill

Verify your implementation by invoking the skill via the JSON-RPC endpoint. Send a POST request to `/a2a` with the following structure:

```json
{
  "jsonrpc": "2.0",
  "method": "example",
  "params": { "msg": "Hello" },
  "id": "123"
}

```

The server will execute your registered handler and stream back the artifacts array through the SSE transport layer managed by [`src/lib/a2a/streaming.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/streaming.ts) (lines 92-126).

For automated validation, run the protocol-level end-to-end tests:

```bash
npm run test:protocols:e2e

```

These tests verify that your skill correctly handles task lifecycle management, TTL enforcement, and proper streaming semantics as implemented in [`src/lib/a2a/taskManager.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/taskManager.ts).

## Summary

- **Create** your skill in `src/lib/a2a/skills/<yourSkill>.ts` by exporting an `execute` function that accepts `A2ATask` and returns `Promise<StreamTaskResult>`.
- **Register** the skill in [`src/lib/a2a/taskExecution.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/taskExecution.ts) by adding a lazy-loaded entry to the `A2A_SKILL_HANDLERS` map with your chosen skill ID.
- **Structure** your return object with an `artifacts` array containing `{ type, content }` objects and optional `metadata`.
- **Expose** the skill in [`src/lib/agentSkills/catalog.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/agentSkills/catalog.ts) only if you require network discovery via the agent-card endpoint.
- **Test** using `npm run test:protocols:e2e` or direct JSON-RPC calls to `/a2a`.

## Frequently Asked Questions

### What is the exact function signature required for an A2A skill?

Your exported function must be named `execute<SkillName>` and implement the signature `(task: A2ATask) => Promise<StreamTaskResult>`. The `A2ATask` type is imported from `@/lib/a2a/taskManager`, and `StreamTaskResult` is an object containing an `artifacts` array and optional `metadata` map. This matches the implementation pattern seen in [`src/lib/a2a/skills/smartRouting.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/skills/smartRouting.ts).

### Do I need to restart the server after adding a new skill?

Yes. While the skill module uses lazy-loading via dynamic imports, the `A2A_SKILL_HANDLERS` registration map in [`src/lib/a2a/taskExecution.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/taskExecution.ts) is initialized at server startup. Changes to this registry require a restart to take effect, though subsequent updates to the skill implementation itself will be picked up on the next invocation due to the import cache behavior.

### How do I handle long-running tasks in an A2A skill?

Long-running operations should return intermediate artifacts through the streaming mechanism. The [`src/lib/a2a/streaming.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/streaming.ts) module automatically handles SSE streaming of your returned artifacts. For tasks that exceed typical HTTP timeouts, ensure your skill updates task status through the task manager and returns partial results; the A2A protocol supports streaming partial artifacts before the final `metadata` closes the stream.

### Where is the skill catalog consumed by other agents?

The skill catalog is exposed at the [`/.well-known/agent.json`](https://github.com/diegosouzapw/OmniRoute/blob/main//.well-known/agent.json) endpoint, generated from definitions in `src/lib/agentSkills/`. The `listCapabilities` skill (defined in [`src/lib/a2a/skills/listCapabilities.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/skills/listCapabilities.ts)) reads this catalog to inform requesting agents about available methods. Without registering in [`src/lib/agentSkills/catalog.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/agentSkills/catalog.ts) or [`types.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/types.ts), your skill will remain functional but invisible to automated agent discovery.