Custom Channels

Purpose and Scope

Custom channels are the extension point for surfaces that eve does not ship as a first-party integration. A channel is the boundary between an external platform and an eve agent: it exposes HTTP or WebSocket endpoints, translates platform-specific requests into agent sessions, watches runtime events, and delivers responses back to the originating product. Use this page when the built-in channel family is not enough, for example when you need an internal webhook, a custom chat product, a voice gateway, or a browser-facing endpoint with a route shape that differs from the default eve channel.

Sources: docs/channels/custom.mdx

A custom channel is not just a thin route handler. It owns both ingress and egress for its platform. On ingress, it parses the raw request, chooses the message sent to the agent, attaches authentication information, and decides whether a continuation token should resume an existing workflow. On egress, it reacts to session events such as completed messages and posts the result wherever the external system expects it. This separation is important because eve can run durable agent work after the original request has returned, while the channel remains responsible for platform delivery semantics.

Sources: docs/channels/custom.mdx

Relevant Source Files

  • docs/channels/custom.mdx - Defines the public custom-channel guide, including file placement, channel identity, route helpers, route-handler helper arguments, event-handler signatures, CORS options, and WebSocket route examples.

File Location and Channel Identity

Custom channel files live under the root agent channel directory. The file stem becomes the channel identifier used by eve, so a file named for an internal webhook is addressed by that stem rather than by an exported name. The module should default-export the channel definition. This convention keeps channel discovery filesystem-first: adding a file in the expected location creates a named integration surface without a central registry. The same section also notes an important boundary: local subagents do not declare channels today, so channel files belong to the root agent.

Sources: docs/channels/custom.mdx

Think of the channel id as the stable address of the integration inside the agent project. It should describe the external surface, not the implementation technique. A route inside the file might accept posts at a short path such as a message endpoint, but the surrounding channel identity still comes from the file. That makes it possible for the framework and other channel helpers to refer to the channel consistently, including when cross-channel handoff is needed through the route helper that can pass inbound work to a different channel.

Sources: docs/channels/custom.mdx

Define a Channel

A channel is defined with the channel helper exported from the channel module. The guide imports route helpers for common HTTP methods and passes a configuration object with a route list and an event map. Routes are responsible for incoming traffic. Events are responsible for runtime notifications. This pairing is the main design pattern for custom integrations: receive the platform request, start or resume the agent session, then observe agent events and deliver platform-specific responses when the agent has output to send.

Sources: docs/channels/custom.mdx

import { defineChannel, GET, POST } from "eve/channels";
 
export default defineChannel({
  routes: [
    POST("/message", async (req, { send }) => {
      const body = await req.json();
      const session = await send(body.message, {
        auth: null,
        continuationToken: body.token,
      });
 
      return Response.json({ sessionId: session.id });
    }),
    GET("/sessions/:sessionId/stream", async (_req, { getSession, params }) => {
      const session = getSession(params.sessionId);
      const stream = await session.getEventStream();
 
      return new Response(stream, {
        headers: { "content-type": "application/x-ndjson; charset=utf-8" },
      });
    }),
  ],
  events: {
    "message.completed"(event, channel, ctx) {
      // deliver completed messages back to the surface that owns this channel
    },
  },
});

The example shows two common route shapes. A post route accepts a platform message, calls the session helper, and returns the new session identifier. A get route accepts a session id from the path, looks up that session, and returns its event stream as newline-delimited JSON. These examples are intentionally low-level: the handler receives the raw request object, so the channel author decides how to parse JSON, headers, parameters, credentials, tokens, uploaded data, or any platform envelope before handing a clean message to the agent.

Sources: docs/channels/custom.mdx

Route Handler Helpers

Each route handler receives a helper object that turns ordinary HTTP handling into agent-aware behavior. The most important helper is the session sender. It starts a new session or resumes one when a continuation token is provided. The sender accepts a message plus options for authentication, continuation, optional state, and an optional title. The title changes the display title for a new workflow session without changing the model message, which is useful when the external surface needs a friendly conversation label that should not leak into the prompt.

Sources: docs/channels/custom.mdx

The session lookup helper is the counterpart to the sender. It accepts a session id and returns a session object. That session can expose an event stream, optionally starting from a supplied index, which lets a custom channel implement browser streaming, long polling, replay, or platform reconnect flows. Path parameters are collected in a params object, so dynamic route segments can be used for session identifiers or other platform ids. This keeps route definitions concise while still giving handlers access to structured values extracted from the route pattern.

Sources: docs/channels/custom.mdx

The helper list also includes cross-channel handoff and request-lifetime controls. The receive helper hands inbound work to another channel, which is useful when one surface receives a trigger but another surface owns delivery. The wait-until helper extends the request lifetime for background work, matching the reality that many platforms expect a fast acknowledgement while delivery or follow-up work continues asynchronously. The request-ip value is included when the host can provide it, and is null when that information is unavailable, so channel code should treat it as optional evidence rather than a guaranteed identity.

Sources: docs/channels/custom.mdx

Events and Delivery

Event handlers are declared under the events key. The documented event shape receives event data, the channel object, and the eve session context. The event data is the payload for that runtime event. The channel object carries platform handles and continuation operations, so delivery logic should use it rather than rebuilding platform state from scratch. The context argument is the session context for the turn. The custom-channel guide calls out one exception: the failed-session event receives only the event data and channel, without a session context argument.

Sources: docs/channels/custom.mdx

A completed-message event is the canonical place to bridge model output back to the external platform. For a webhook-style channel, that could mean posting a callback to an internal service. For a chat surface, it could mean writing into a conversation thread. For a custom browser backend, it might be unnecessary if the client consumes the event stream directly. The important point is that route handlers and event handlers solve different halves of the integration. Routes create or resume work; events observe the resulting work and perform delivery using platform-specific rules.

Sources: docs/channels/custom.mdx

CORS Configuration

Custom HTTP channels do not alter CORS behavior unless you opt in. That default is conservative because many custom channels are server-to-server webhooks where browser access is not expected. When direct browser calls are required, the channel can enable permissive access with a boolean option, including preflight handling. For production browser clients, the guide shows a serializable options object that narrows the allowed origin, methods, and headers. Treat CORS as transport policy, not authorization; the handler should still validate callers and set meaningful auth when sending to the agent.

Sources: docs/channels/custom.mdx

import { defineChannel, POST } from "eve/channels";
 
export default defineChannel({
  cors: {
    origin: ["https://app.example.com"],
    methods: ["POST"],
    allowHeaders: ["authorization", "content-type"],
  },
  routes: [POST("/message", async () => new Response("ok"))],
});

WebSocket Routes

Use the WebSocket route helper when the external surface needs a persistent connection. The route handler runs for an upgrade request and returns lifecycle hooks for that connection. The guide’s voice-style example defines a message hook, reads text from the incoming peer message, and calls the same session sender helper used by HTTP routes. This matters because WebSocket and HTTP channels share the same agent-facing primitives: send messages, resume by continuation token, inspect path params, look up sessions, hand off to another channel, extend background work, and read request IP when available.

Sources: docs/channels/custom.mdx

import { defineChannel, WS } from "eve/channels";
 
export default defineChannel({
  routes: [
    WS("/voice/ws", async (_req, { send }) => ({
      async message(_peer, message) {
        await send(message.text(), {
          auth: null,
          continuationToken: "voice-demo",
        });
      },
    })),
  ],
});

Compact Reference

ComponentContractWhen to use
defineChannel()Default-export a channel definition with routes, events, and optional cors.Create a custom integration surface.
GET() and POST()Declare HTTP routes and receive (Request, helpers).Accept webhooks, browser calls, stream endpoints, or platform callbacks.
WS()Declare a WebSocket endpoint whose handler returns connection lifecycle hooks.Support persistent transports such as voice or realtime clients.
send(message, options)Starts or resumes a session with auth, continuationToken, optional state, and optional title.Turn inbound platform content into agent work.
getSession(sessionId)Retrieves an existing session that can provide an event stream.Implement stream or reconnect endpoints.
receive(channel, ...)Hands inbound work to another channel.Bridge triggers between surfaces.
waitUntil(promise)Extends request lifetime for background work.Acknowledge quickly while continuing delivery.
requestIpClient IP, or null when unavailable.Apply optional network-aware policy or logging.
eventsRuntime event handlers receiving event payload, channel handles, and usually session context.Deliver completed messages or react to runtime state.

Implementation Guidance

Start by choosing the minimal public contract your external platform needs. If the platform posts webhooks, add a post route that parses the request body, validates whatever platform credentials are required, and calls the sender helper with the message and auth you want the agent to see. If clients need to observe output directly, add a stream route that retrieves the session and returns the event stream. If the platform owns delivery, prefer an event handler that posts the completed message back to the platform instead of exposing the entire stream to clients.

Sources: docs/channels/custom.mdx

Be deliberate with continuation tokens. The sender helper can start a new workflow or resume an existing one, so a token coming from an untrusted request should be treated as sensitive routing state. The custom-channel guide shows a token flowing from the request body into the sender options, but the channel author still owns validation and caller policy. Similarly, passing null auth is useful for minimal examples, but real integrations should decide what principal, tenant, or system identity is responsible for the turn before dispatching durable agent work.

Sources: docs/channels/custom.mdx

After implementing a custom channel, compare it with the built-in channel pages to decide whether any delivery behavior should be copied into your integration. The default eve channel is useful when you need canonical session routes and streaming semantics, while platform channels such as Discord illustrate fast acknowledgements, background work, and event-driven delivery. For broader architecture, read the channels overview to understand the shared channel contract, then continue to sessions, runs, and streaming to understand the event stream your custom routes can expose to clients.