Skip to main content

Command Palette

Search for a command to run...

BDK

Channels

A channel connects an external surface to agent sessions. A file at bot/channels/<id>.ts defines channel <id> and mounts its routes under /v1/channels/<id>. The built-in HTTP session channel is always available alongside any custom or prebuilt channels.

Built-in HTTP channel

The built-in channel serves the session, tool, and discovery routes. In a multi-agent host, each agent's routes sit under its slug. See the HTTP API for request and response contracts.

Define a custom channel

Use defineChannel with one or more typed routes:

import { defineChannel, POST } from "@cursor/bdk/channels";import { z } from "zod";export default defineChannel({  routes: [    POST("/review", {      description: "Review a pull request on this channel",      bodySchema: z.object({        message: z.string(),        prUrl: z.string().url(),      }),      handler: async (_request, { send, waitUntil, body }) => {        waitUntil(          send(`${body.message}\n\nPull request: ${body.prUrl}`, {            continuationToken: `pr:${body.prUrl}`,          })        );        return new Response(null, { status: 202 });      },    }),  ],});

The route above is POST /v1/channels/<id>/review. Calls for the same pull request reuse one conversation because they pass the same continuation token.

Route responses

A custom route acknowledges the caller and nothing more. On Cursor-managed hosting, the deployment answers every custom route with 202 Accepted and an empty body as soon as the request arrives, then runs the handler. The caller never receives the handler's Response, so a route can't return a session ID, a computed result, or an error status. Requests rejected before the handler runs, such as a missing alias token or a failed sharedSecretAuth or hmacSignatureAuth check, still get 401.

Deliver results through the channel's events handlers, a callback to the system that sent the request, or a tool the turn calls. Local serve passes the handler's response through, so return new Response(null, { status: 202 }) to keep local calls matching hosted ones.

Route verbs and schemas

Route paths must begin with /. The method helper determines which Zod schemas the route accepts:

HelperSchema contract
GETquerySchema is required
POST, PUT, PATCHbodySchema is required; querySchema is optional
DELETEBoth schemas are optional

Plain JSON Schema objects don't type-check. Use z.object({}) or z.unknown() for an open surface. The host validates the body and query before calling the handler, and an empty body is read as {}. A request that fails validation never reaches the handler; local serve answers it with 400, while a hosted caller has already received its 202. Declared schemas also appear in GET /v1/info for playground requests and slash commands.

Handler arguments

Each handler receives the Fetch Request and a typed arguments object.

MemberContract
send(message, options?)Start or resume a session on this channel
getSession(sessionId)Return this channel's session, or null
receive(channel, input)Hand work to another channel
callTool(name, input, options?)Call a server tool without a model turn
body, query, paramsValidated inputs and :param path segments
authThe AuthContext returned by the route's auth chain
requestIpThe TCP peer address, or null
hostShared MCP, provider, storage, telemetry, and reminder services
waitUntil(promise)Keep work running after the handler returns
sessionUrls(request, sessionId)Build absolute playground and trace URLs for this mount
artifactsList or tag artifacts; pass sessionId when attributing one

send options

OptionContract
continuationTokenResume the session with this channel-local key, or create one when the key is new
admission"preempt" interrupts a busy turn; "coalesce" queues behind it. The default is "preempt"
workspaceFilesAdd relative files for the next turn
workspaceDirUse an absolute local working directory
authSet the session principal; defaults to the request principal
stateSet starting channel state for a new session
titleSet the display title for a new session
purposeUse "eval" to mark a new session as regression traffic
dryRunRun read tools and stub write tools for a new session
asOfFreeze a new session at an ISO-8601 instant with a timezone

send returns a ChannelSession. Its id identifies the session, continuationToken contains its current channel key, and isNew says whether this call created it. A coalesced call also returns coalesced: true.

Events

The events map subscribes the channel to stream events for the sessions it owns. Keys are event types from the event vocabulary, or "*". Each handler receives (event, channel, ctx), including the session's channel.state, session info, and shared host services. Use these handlers to deliver progress and replies to the surface that owns the channel.

Session state

state on the channel definition supplies starting JSON for each new session. A route can instead pass state to send; event handlers read and update the active value through channel.state.

Start and stop a channel

onStart(args) runs when the channel mounts, and onStop() runs when the host stops. onStart receives the route helpers plus these transport controls:

HelperContract
emitAssistantMessage(sessionId, text)Append final assistant text without starting a model turn
hasContinuationSession(token)Check whether a token maps to a session
isContinuationBusy(token)Check whether a turn is active for a token
interruptContinuation(token)Stop the active turn and clear queued coalesced follow-ups

Auth policies

Every route runs the channel's auth array. The default is [localDevStrict()]. Policies may be asynchronous; the first one to return an AuthContext admits the request, and an all-null result returns 401.

PolicyAdmits
localDevStrict()Direct loopback requests with a loopback hostname and no proxy-forwarding headers
localDev()Direct loopback requests with no proxy-forwarding headers, without checking the hostname
loopbackOnly()Any loopback TCP peer, including local relays that carry forwarding headers
bearerAuth(tokenOrVerify)A matching bearer token, or a token accepted by the verifier
sharedSecretAuth({ header, secret })A header matching the named environment or deployment secret
hmacSignatureAuth({ header, secret, prefix? })A hex HMAC-SHA256 signature over the raw body using the named secret
allowAll()Every caller as an anonymous principal
publicEndpoint()Every caller on this custom channel; managed hosting also exposes the route without an alias token

Use allowAll() only for an intentionally public surface or one protected upstream. With publicEndpoint(), the handler must verify the provider's signature. The policy applies only to custom channel routes; it doesn't open the built-in session or tool API.

The resolved AuthContext ({ authenticator, principalId, principalType, attributes? }) becomes the request principal. Sessions belong to the principal that created them, and other principals receive 403 on owned routes.

Server flags interact with authored auth: --bearer-token swaps the default localDevStrict() for bearerAuth(...) on channels that don't author their own chain, and --allow-anonymous swaps it for allowAll(). An authored auth array always takes precedence.

Prebuilt channels

ImportFunctionSurface
@cursor/bdk/channels/slackslackChannel()Slack messages, threads, and streaming replies. See Slack
@cursor/bdk/channels/githubgithubChannel()GitHub and GitHub Enterprise webhooks. See GitHub
@cursor/bdk/channels/gitlabgitlabChannel()GitLab.com and self-managed GitLab hooks. See GitLab
@cursor/bdk/channels/bitbucketbitbucketChannel()Bitbucket Cloud and Data Center hooks. See Bitbucket
@cursor/bdk/channels/deploymentsdeploymentsChannel()Deployment events filtered by source, environment, or event name

For other platforms like Discord or Teams, use the authored defineChannel route form.

Continuation tokens

Each channel defines its continuation-token format. The same token resumes the same conversation; the built-in HTTP channel rotates its opaque token after every accepted follow-up and returns 409 for a stale token. See Session identifiers for the full contract.