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

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.

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 (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.

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). This directory already contains built-in implementations like smartRouting.ts and 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.

// 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 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:

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), 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 or 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:

{
  "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 (lines 92-126).

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

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.

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 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 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.

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 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 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 endpoint, generated from definitions in src/lib/agentSkills/. The listCapabilities skill (defined in src/lib/a2a/skills/listCapabilities.ts) reads this catalog to inform requesting agents about available methods. Without registering in src/lib/agentSkills/catalog.ts or types.ts, your skill will remain functional but invisible to automated agent discovery.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →