# How to Set Up A2A Agent Protocol Communication in OmniRoute for Multi-Agent Workflows

> Learn to set up A2A agent protocol communication in OmniRoute for multi-agent workflows. Exchange tasks, stream results, and query status via standardized HTTP endpoints.

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

---

**OmniRoute implements the A2A (Agent-to-Agent) protocol as a JSON-RPC 2.0 service that lets independent agents exchange tasks, stream results, and query task status through a standardized HTTP endpoint.**

The **A2A protocol** enables autonomous agents to delegate work, monitor progress, and consume streaming outputs without tight coupling. In the `diegosouzapw/OmniRoute` repository, this capability is built on three core components: a JSON-RPC router, an in-memory task manager with optional SQLite persistence, and a pluggable skill registry. This guide walks through enabling the protocol, authenticating requests, and integrating custom skills for production multi-agent systems.

## Enable and Secure the A2A Endpoint

### Activate the Protocol in Settings

The `/a2a` route checks a feature flag before processing any request. Store `a2aEnabled: true` in the settings table to activate the endpoint:

```typescript
// Example using the Settings DB API
import { getSettings, updateSettings } from "@/lib/db/settings";

async function enableA2A() {
  const settings = await getSettings();
  await updateSettings({ ...settings, a2aEnabled: true });
}
enableA2A();

```

The router validates this flag via `rejectIfA2ADisabled` in [`src/app/a2a/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/a2a/route.ts) (lines 91-93). Requests return HTTP 404 when disabled.

### Configure Authentication (Optional)

Set the `OMNIROUTE_API_KEY` environment variable to require bearer token validation:

```bash
export OMNIROUTE_API_KEY="sk-omni-abc123xyz"

```

The `authenticate` helper in [`src/app/a2a/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/a2a/route.ts) (lines 66-85) compares the `Authorization: Bearer <token>` header against this value. Omit the variable to run without authentication—suitable only for development.

## Send Tasks via JSON-RPC

### Basic Task Submission with `message/send`

POST JSON-RPC 2.0 payloads to `https://<host>/a2a`. The `message/send` method executes skills synchronously and returns complete artifacts:

```javascript
fetch("https://my-omniroute.example.com/a2a", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + process.env.OMNIROUTE_API_KEY,
  },
  body: JSON.stringify({
    jsonrpc: "2.0",
    id: "task-123",
    method: "message/send",
    params: {
      skill: "smart-routing",
      messages: [{ role: "user", content: "Explain quantum entanglement." }],
      metadata: { role: "explain", model: "gpt-4o" },
    },
  }),
})
  .then((r) => r.json())
  .then(console.log);

```

The router in [`src/app/a2a/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/a2a/route.ts) (lines 33-84) handles this flow: parse → authenticate → create task via `tm.createTask` → dispatch to registered handler → return `jsonRpcResult`.

### Stream Partial Results with `message/stream`

For long-running operations, use `message/stream` to receive Server-Sent Events (SSE):

```javascript
const ev = new EventSource(
  "https://my-omniroute.example.com/a2a",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer " + process.env.OMNIROUTE_API_KEY,
    },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: "stream-1",
      method: "message/stream",
      params: { 
        skill: "smart-routing", 
        messages: [{ role: "user", content: "Summarize the article." }] 
      },
    }),
  }
);

ev.onmessage = (e) => console.log("Chunk:", e.data);
ev.onerror = (e) => console.error("Stream error", e);

```

The `createA2AStream` function in [`src/lib/a2a/streaming.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/streaming.ts) (lines 90-110) wraps the execution in SSE formatting with proper headers from `SSE_HEADERS`.

## Manage Task Lifecycle

### Task State Transitions

The **A2A Task Manager** in [`src/lib/a2a/taskManager.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/taskManager.ts) tracks states: `submitted → working → completed | failed | cancelled`.

| Method | Purpose | Implementation |
|--------|---------|----------------|
| `createTask` | Initialize task record | Lines 94-110 |
| `updateTask` | Transition state, append artifacts | Lines 111-130 |
| `cancelTask` | Move to `cancelled`, cleanup resources | Lines 131-145 |
| `getTask` | Retrieve current state and history | Lines 146-156 |

### Query and Cancel Existing Tasks

Use JSON-RPC methods to inspect or terminate work:

```json
// tasks/get — fetch current state
{
  "jsonrpc": "2.0",
  "id": "query-1",
  "method": "tasks/get",
  "params": { "id": "task-123" }
}

// tasks/cancel — terminate execution
{
  "jsonrpc": "2.0",
  "id": "cancel-1",
  "method": "tasks/cancel",
  "params": { "id": "task-123" }
}

```

## Register Custom A2A Skills

### Implement a Skill Handler

Create a TypeScript module in `src/lib/a2a/skills/` following the `A2ASkillHandler` signature:

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

export async function helloWorldHandler(task: A2ATask) {
  const result = {
    artifacts: [{ type: "text", content: "Hello from A2A!" }],
    metadata: { greeting: true, processedAt: Date.now() },
  };
  return result;
}

```

Handlers receive the full `A2ATask` object and return `Promise<StreamTaskResult>` containing artifacts and optional metadata.

### Add to the Skill Registry

Import and register in [`src/lib/a2a/taskExecution.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/taskExecution.ts) (lines 19-23):

```typescript
import { helloWorldHandler } from "@/lib/a2a/skills/helloWorld";

export const A2A_SKILL_HANDLERS: Record<string, A2ASkillHandler> = {
  "smart-routing": smartRoutingHandler,
  "list-capabilities": listCapabilitiesHandler,
  "cost-analysis": costAnalysisHandler,
  "health-report": healthReportHandler,
  "quota-management": quotaManagementHandler,
  "hello-world": helloWorldHandler, // ← custom skill
};

```

Registration is automatic—the router resolves skill names against this record at runtime.

## Interact via CLI

The `omniroute a2a` commands in `bin/cli/commands/a2a.mjs` mirror the HTTP API:

```bash

# List available skills

npx omniroute a2a list

# Send a task

npx omniroute a2a send --skill smart-routing "Analyze this log file"

# Stream results

npx omniroute a2a stream --skill smart-routing "Generate report"

# Check task status

npx omniroute a2a get task-123

# Cancel running task

npx omniroute a2a cancel task-123

```

The CLI reads `A2A_SKILL_HANDLERS` directly, ensuring consistency with the HTTP interface.

## Key Source Files and Architecture

| File | Responsibility |
|------|---------------|
| [`src/app/a2a/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/a2a/route.ts) | JSON-RPC router, authentication, method dispatch |
| [`src/lib/a2a/taskManager.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/taskManager.ts) | Task lifecycle, state machine, persistence |
| [`src/lib/a2a/taskExecution.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/taskExecution.ts) | Skill registry (`A2A_SKILL_HANDLERS`), execution orchestration |
| [`src/lib/a2a/streaming.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/streaming.ts) | SSE stream construction, header constants |
| `bin/cli/commands/a2a.mjs` | Command-line interface implementation |
| [`docs/frameworks/A2A-SERVER.md`](https://github.com/diegosouzapw/OmniRoute/blob/main/docs/frameworks/A2A-SERVER.md) | Protocol design documentation |

## Summary

- **Enable A2A** by setting `a2aEnabled: true` in the settings table; the router validates this via `rejectIfA2ADisabled` in [`src/app/a2a/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/a2a/route.ts).
- **Authenticate** with `OMNIROUTE_API_KEY` and `Authorization: Bearer` headers, or disable for development.
- **Submit tasks** via `message/send` for synchronous results or `message/stream` for SSE-based streaming.
- **Track lifecycle** through [`taskManager.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/taskManager.ts) methods: `createTask`, `updateTask`, `cancelTask`, `getTask`.
- **Extend capabilities** by implementing `A2ASkillHandler` functions and registering them in `A2A_SKILL_HANDLERS` at [`src/lib/a2a/taskExecution.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/taskExecution.ts).
- **Operate at scale** using the CLI client or direct HTTP JSON-RPC calls from any agent runtime.

## Frequently Asked Questions

### What is the A2A protocol in OmniRoute?

The **A2A (Agent-to-Agent) protocol** is a JSON-RPC 2.0 service layer that standardizes how independent software agents delegate tasks, exchange data, and monitor execution. In OmniRoute, it enables multi-agent workflows where specialized agents can hand off work through a common HTTP interface without direct integration.

### How does streaming work in A2A communication?

OmniRoute uses **Server-Sent Events (SSE)** for streaming. The `message/stream` method triggers `createA2AStream` in [`src/lib/a2a/streaming.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/a2a/streaming.ts), which repeatedly calls `executeA2ATaskWithState` and emits partial artifacts as SSE data chunks. Clients consume these via `EventSource` or any SSE-compatible library.

### Can I persist A2A tasks beyond server restarts?

Yes. The [`taskManager.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/taskManager.ts) implementation supports optional **SQLite persistence** in addition to its default in-memory store. Configure persistence through environment variables or settings—task state survives process restarts and enables recovery of long-running operations.

### How do I add authentication to the A2A endpoint?

Set the `OMNIROUTE_API_KEY` environment variable, then include `Authorization: Bearer <token>` in all requests. The `authenticate` function in [`src/app/a2a/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/a2a/route.ts) validates this header on every call. Return 401 for missing or invalid tokens.