# Relay Messaging
Source: https://dorkos.ai/docs/guides/relay-messaging

Send messages between your agents, your cockpit, and chat apps like Telegram or Slack





# Relay Messaging [#relay-messaging]

Relay lets your agents send and receive messages: to each other, to you in the cockpit, or to a Telegram or Slack chat. Every message is saved, so nothing goes missing even if an agent is busy.

This guide gets you sending your first message, then covers adapters, budgets, and tracing for when you want more control.

## Send your first message [#send-your-first-message]

Relay is already on by default, so there's nothing to install. Here's the fastest way to see it work.

<Steps>
  <Step>
    **Confirm Relay is running.** Start DorkOS and look for this in the startup log:

    ```
    DorkOS server listening on port 4242
    Relay message bus enabled
    ```
  </Step>

  <Step>
    **Send a message.** Every message needs a subject: a named channel, like an email address for a message. This one is addressed to an agent called `backend`:

    ```bash
    curl -X POST http://localhost:4242/api/relay/messages \
      -H 'Content-Type: application/json' \
      -d '{
        "subject": "relay.agent.backend",
        "payload": { "task": "Run the test suite" },
        "from": "relay.human.console.my-client"
      }'
    ```
  </Step>

  <Step>
    **See it arrive.** The response confirms delivery:

    ```json
    {
      "id": "01HX...",
      "delivered": 1,
      "deadLettered": 0
    }
    ```

    `delivered: 1` means an agent picked it up. If it had failed, it would show up in `deadLettered` instead (Relay's term for "delivery failed, here's why").
  </Step>
</Steps>

That's the whole loop: publish a message to a subject, Relay routes it, the recipient gets it. Everything below is what you can build once you know that.

## Reference [#reference]

The rest of this page is a deeper look at how Relay works: the built-in tools, chat adapters, budgets, and configuration. Agent-to-agent chains are still a newer part of DorkOS, so if you're chaining more than a couple of hops, set explicit budgets (below) to keep things bounded.

### Using MCP tools [#using-mcp-tools]

Agents running inside DorkOS can call Relay directly, without making an HTTP request themselves:

```
relay_send                  Send a message to a Relay subject
relay_inbox                 Read inbox messages for an endpoint
relay_list_endpoints        List all registered endpoints
relay_register_endpoint     Register a new endpoint
relay_unregister_endpoint   Unregister an endpoint (use to clean up dispatch inboxes)
relay_send_and_wait         Blocking request/reply: sends a message and waits for the response
relay_send_async            Fire-and-poll: dispatches a message, returns immediately with an inbox to poll
relay_get_trace             Get the full delivery trace for a message
relay_get_metrics           Get aggregate delivery metrics
relay_list_adapters         List all adapters with status
relay_enable_adapter        Enable a disabled adapter
relay_disable_adapter       Disable a running adapter
relay_reload_adapters       Hot-reload adapter configuration from disk
```

`relay_send_and_wait` (added in v0.8.0) handles request and reply in one call. It registers a temporary inbox, sends the message with that inbox as the reply address, and waits until the agent replies. Use it when you need the answer before your next step.

`relay_send_async` returns immediately with a dispatch inbox instead of waiting. The agent keeps working in the background. Poll `relay_inbox` with `status: "unread"` and `ack: true` to check progress: each poll only returns messages you haven't seen yet. When you get a payload with `done: true`, call `relay_unregister_endpoint` to clean up.

### Subject naming [#subject-naming]

Subjects follow a dot-separated pattern: `relay.{audience}.{identifier}`.

| Subject Pattern                   | Purpose                                         |
| --------------------------------- | ----------------------------------------------- |
| `relay.agent.{agentId}`           | Messages addressed to a specific agent          |
| `relay.human.console.{clientId}`  | Messages destined for a human's browser console |
| `relay.human.telegram.{chatId}`   | Messages routed to a Telegram chat              |
| `relay.system.tasks.{scheduleId}` | System messages from the Task scheduler         |

You can subscribe with wildcards: `*` stands in for exactly one segment, `>` for one or more trailing segments. Subscribing to `relay.agent.>` gets you messages addressed to any agent.

### What happens when Relay is running [#what-happens-when-relay-is-running]

Agents publish messages to named subjects, and Relay routes each one to the right recipient. Agents don't need to know about each other directly; they just need to agree on a subject name.

Along the way, a message passes through a few checks: is the subject valid, is the sender allowed to send it, is it within its rate limit and budget. If a recipient adapter is failing repeatedly, Relay's circuit breaker (a safety switch that stops sending to something that's clearly broken) pauses deliveries to it rather than retrying forever. Anything that can't be delivered lands in the dead letter queue, and the full path a message took is recorded so you can look it up later (see [Tracing](#message-tracing) below).

When Relay is on, it also takes over how session messages are delivered. Instead of `POST /api/sessions/:id/messages` calling the agent SDK directly, the message is published to `relay.agent.{sessionId}` and the Claude Code adapter picks it up from there. That means every session message gets Relay's tracing, budgets, and retry handling for free.

### Enabling and disabling Relay [#enabling-and-disabling-relay]

Relay is on by default for fresh installs. Its data directory, `~/.dork/relay/`, is created the first time it starts.

To check whether it's active, call the config endpoint:

```bash
curl http://localhost:4242/api/config
```

The response includes `relay.enabled: true` when Relay is active.

<Callout type="info">
  To turn Relay off, set `DORKOS_RELAY_ENABLED=false` in your `.env` file. Relay's routes will
  return 404 and session messaging falls back to calling the agent SDK directly. Your message
  history and search index stay on disk, so turning Relay back on later doesn't lose anything.
</Callout>

### Budget constraints [#budget-constraints]

Every message carries a budget that keeps chains of agent-to-agent messages from running away:

```bash
curl -X POST http://localhost:4242/api/relay/messages \
  -H 'Content-Type: application/json' \
  -d '{
    "subject": "relay.agent.backend",
    "payload": { "task": "Refactor the auth module" },
    "from": "relay.human.console.my-client",
    "budget": {
      "maxHops": 3,
      "ttlMs": 300000,
      "callBudgetRemaining": 50
    }
  }'
```

<Callout type="warn">
  If you leave budget fields out, Relay uses defaults: 5 maximum hops, a 1-hour time-to-live, and no
  call limit. For automated agent-to-agent workflows, set explicit budgets so a chain of agents
  can't consume more time or calls than you intended.
</Callout>

## Built-in adapters [#built-in-adapters]

Adapters connect external platforms to Relay's subject system. DorkOS ships with five, configured through `~/.dork/relay/adapters.json`.

### Claude Code adapter [#claude-code-adapter]

The Claude Code adapter is on by default whenever Relay starts. It listens on `relay.agent.*` subjects and routes incoming messages to Claude Agent SDK sessions. Replies flow back through Relay to the sender's reply address.

```json
{
  "adapters": [
    {
      "id": "claude-code",
      "type": "claude-code",
      "builtin": true,
      "enabled": true,
      "config": {
        "maxConcurrent": 3,
        "defaultTimeoutMs": 300000
      }
    }
  ]
}
```

`maxConcurrent` limits how many agent sessions run at once. `defaultTimeoutMs` sets the longest a session can run before it's stopped.

### Telegram adapter [#telegram-adapter]

The Telegram adapter connects a Telegram bot to Relay. Messages from the chat are published to `relay.human.telegram.{chatId}`, and replies on that subject get delivered back to the chat, including live-updating drafts as the agent writes its answer.

```json
{
  "id": "telegram",
  "type": "telegram",
  "builtin": true,
  "enabled": true,
  "config": {
    "token": "123456:ABC-DEF..."
  }
}
```

### Slack adapter [#slack-adapter]

The Slack adapter connects a Slack workspace to Relay using Socket Mode, so you don't need a public URL. It supports streaming replies, threads, typing indicators (shown as an emoji reaction), and access control per channel.

```json
{
  "id": "slack",
  "type": "slack",
  "builtin": true,
  "enabled": true,
  "config": {
    "botToken": "xoxb-...",
    "appToken": "xapp-...",
    "signingSecret": "abc123...",
    "streaming": true,
    "typingIndicator": "reaction",
    "respondMode": "thread-aware",
    "dmPolicy": "open"
  }
}
```

`respondMode` controls when the bot replies in a channel: `thread-aware` (default: replies to @mentions and to threads it's already part of), `mention-only` (only @mentions), or `always` (every message). `dmPolicy` controls direct messages: `open` (default) or `allowlist` (only the user IDs listed in `dmAllowlist`). You can also set overrides per channel with `channelOverrides`.

### Webhook adapter [#webhook-adapter]

The webhook adapter is a generic HTTP bridge. It can receive messages from other services and forward Relay messages out as HTTP requests, both directions signed with HMAC-SHA256 (a signature that proves the request wasn't tampered with).

```json
{
  "id": "my-webhook",
  "type": "webhook",
  "builtin": true,
  "enabled": true,
  "config": {
    "url": "https://example.com/hook",
    "secret": "your-hmac-secret",
    "subjectPrefix": "relay.webhook.incoming"
  }
}
```

Inbound requests arrive at `POST /api/relay/webhooks/{adapterId}`. The adapter checks the signature, reads the payload, and publishes it under the configured subject prefix. You can rotate your secret with a 24-hour overlap window so nothing breaks mid-rotation.

### Hot reload [#hot-reload]

DorkOS watches `~/.dork/relay/adapters.json` for changes. Edit the file and it reconciles automatically: adapters you removed or disabled stop, and newly enabled ones start. No restart needed.

You can also trigger a reload by hand:

```bash
curl -X POST http://localhost:4242/api/relay/adapters/reload
```

## Message tracing [#message-tracing]

Every message gets a trace that follows it from publish to delivery. Look one up by its ID:

```bash
curl http://localhost:4242/api/relay/messages/{messageId}/trace
```

```json
{
  "traceId": "01HXABC123",
  "spans": [
    {
      "id": "01HXDEF456",
      "messageId": "01HXABC123",
      "traceId": "01HXABC123",
      "subject": "relay.agent.backend",
      "status": "delivered",
      "sentAt": "2025-02-26T12:00:00.000Z",
      "deliveredAt": "2025-02-26T12:00:00.050Z",
      "processedAt": "2025-02-26T12:00:00.200Z",
      "errorMessage": null,
      "metadata": null
    }
  ]
}
```

A span's `status` is one of: `sent` (published, delivery in progress), `delivered` (handled successfully), `failed` (the recipient errored), or `timeout` (blocked by budget, access control, or expiry).

For more on debugging failed deliveries, see the [Relay Observability](/docs/guides/relay-observability) guide.

## Relay and Tasks [#relay-and-tasks]

When both Relay and Tasks are on, scheduled jobs are dispatched through Relay. Tasks publishes to `relay.system.tasks.{scheduleId}`, and the Claude Code adapter starts the agent session from there. That gives every scheduled run Relay's budget limits, tracing, and retry handling too.

```bash
export DORKOS_RELAY_ENABLED=true
export DORKOS_TASKS_ENABLED=true
dorkos
```

Subscribe to `relay.system.tasks.*` on the SSE stream to watch every Tasks dispatch as it happens.

## Configuration reference [#configuration-reference]

<TypeTable
  type="{
  DORKOS_RELAY_ENABLED: {
    type: 'boolean',
    description:
      'Overrides the Relay subsystem on or off. Relay is on by default in your saved config; set this only when you want to force a different value',
  },
}"
/>

### Data directory [#data-directory]

| Path                              | Purpose                                                            |
| --------------------------------- | ------------------------------------------------------------------ |
| `~/.dork/relay/index.db`          | SQLite index database (messages, traces; WAL mode)                 |
| `~/.dork/relay/endpoints/`        | Maildir message store, one directory per endpoint                  |
| `~/.dork/relay/adapters.json`     | Adapter configuration (hot-reloaded)                               |
| `~/.dork/relay/config.json`       | Reliability settings (rate limits, circuit breakers, backpressure) |
| `~/.dork/relay/access-rules.json` | Access control rules (hot-reloaded)                                |

### Reliability settings [#reliability-settings]

The `config.json` file controls rate limiting, circuit breakers, and backpressure (what happens when an endpoint's mailbox is filling up faster than it's read). Changes are hot-reloaded.

<TypeTable
  type="{
  'rateLimit.maxPerWindow': {
    type: 'number',
    description: 'Maximum messages per sender per window',
    default: '100',
  },
  'rateLimit.windowMs': {
    type: 'number',
    description: 'Sliding window duration in milliseconds',
    default: '60000',
  },
  'circuitBreaker.failureThreshold': {
    type: 'number',
    description: 'Consecutive failures before circuit opens',
    default: '5',
  },
  'circuitBreaker.cooldownMs': {
    type: 'number',
    description: 'Time before half-open probe in milliseconds',
    default: '30000',
  },
  'backpressure.warnThreshold': {
    type: 'number',
    description: 'Mailbox percentage that triggers a warning signal',
    default: '0.8',
  },
  'backpressure.maxMessages': {
    type: 'number',
    description: 'Maximum messages in an endpoint mailbox before rejection',
    default: '1000',
  },
}"
/>

### REST API endpoints [#rest-api-endpoints]

| Method   | Path                                  | Description                                      |
| -------- | ------------------------------------- | ------------------------------------------------ |
| `POST`   | `/api/relay/messages`                 | Publish a message to a subject                   |
| `GET`    | `/api/relay/messages`                 | List messages with filters and cursor pagination |
| `GET`    | `/api/relay/messages/:id`             | Get a single message by ID                       |
| `GET`    | `/api/relay/messages/:id/trace`       | Get the full delivery trace for a message        |
| `GET`    | `/api/relay/conversations`            | List grouped request/response exchanges          |
| `GET`    | `/api/relay/endpoints`                | List registered endpoints                        |
| `POST`   | `/api/relay/endpoints`                | Register a new endpoint                          |
| `DELETE` | `/api/relay/endpoints/:subject`       | Unregister an endpoint                           |
| `GET`    | `/api/relay/endpoints/:subject/inbox` | Read inbox for an endpoint                       |
| `GET`    | `/api/relay/dead-letters`             | List dead letter messages                        |
| `GET`    | `/api/relay/dead-letters/aggregated`  | Dead letters grouped by source and reason        |
| `GET`    | `/api/relay/metrics`                  | Relay system metrics                             |
| `GET`    | `/api/relay/trace/metrics`            | Aggregate delivery metrics                       |
| `GET`    | `/api/relay/stream`                   | SSE event stream (supports `?subject=` filter)   |
| `GET`    | `/api/relay/adapters`                 | List adapters with status                        |
| `GET`    | `/api/relay/adapters/catalog`         | List available adapter types                     |
| `GET`    | `/api/relay/adapters/:id`             | Get a single adapter's status                    |
| `POST`   | `/api/relay/adapters/reload`          | Hot-reload adapter configuration                 |
| `POST`   | `/api/relay/adapters/test`            | Test an adapter configuration before saving it   |
| `POST`   | `/api/relay/adapters/:id/enable`      | Enable an adapter                                |
| `POST`   | `/api/relay/adapters/:id/disable`     | Disable an adapter                               |
| `GET`    | `/api/relay/bindings`                 | List adapter-to-agent bindings                   |
| `POST`   | `/api/relay/bindings`                 | Create an adapter-to-agent binding               |
| `DELETE` | `/api/relay/bindings/:id`             | Remove a binding                                 |
| `POST`   | `/api/relay/webhooks/:adapterId`      | Inbound webhook receiver                         |

## Next steps [#next-steps]

<Cards>
  <Card title="Relay Observability" href="/docs/guides/relay-observability">
    Message tracing, delivery metrics, and debugging failed deliveries.
  </Card>

  <Card title="Relay Concepts" href="/docs/concepts/relay">
    Relay's architecture, envelope format, delivery pipeline, and storage model.
  </Card>

  <Card title="Agent Discovery" href="/docs/guides/agent-discovery">
    Discover and register agents that communicate through Relay using Mesh.
  </Card>
</Cards>
