Skip to content

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>, and the built-in HTTP channel serves the HTTP API routes and is always available.

Define a custom channel ​

Use defineChannel with one or more typed routes:

ts
import { defineChannel, POST } from "@grok/agent-sdk/channels";
import { z } from "zod";

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

This route is POST /v1/channels/<id>/review. Calls for the same pull request pass the same continuation token, so they share one conversation.

Route responses ​

On Cursor hosting, the deployment answers every custom route with an empty 202 Accepted as soon as the request arrives, then runs the handler. The caller never sees the handler's Response, so a route can't return a session ID, a result, or an error status. A request that fails the alias token, sharedSecretAuth, or hmacSignatureAuth check still gets 401.

Deliver results through events handlers, a callback to the sender, or a tool the turn calls. Local serve returns the handler's response, so return new Response(null, { status: 202 }) to match hosting.

Route verbs and schemas ​

Route paths must begin with /.

GET requires a querySchema.

POST, PUT, and PATCH require a bodySchema. They also accept a querySchema.

DELETE takes a bodySchema, a querySchema, either, or neither.

Plain JSON Schema objects don't type-check, so use z.object({}) or z.unknown() for an open surface. The host validates input before the handler runs, and declared schemas appear in GET /v1/info for the playground and slash commands.

Handler arguments ​

A handler receives the Fetch Request and these arguments:

MemberContract
send(message, options?)Start or resume a session on this channel
getSession(sessionId)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 from the route's auth chain
requestIpThe TCP peer address, or null
hostHost services such as MCP, storage, telemetry, and reminders
waitUntil(promise)Keep work running after the handler returns
sessionUrls(request, sessionId)Playground and trace URLs for a session
artifactsList or tag artifacts

send options ​

OptionContract
continuationTokenChannel-local key that resumes its session, or creates one when new
admission"preempt" (the default) interrupts a busy turn. "coalesce" queues behind it
workspaceFilesRelative files to add before the next turn
workspaceDirAbsolute local working directory
authSession principal. Defaults to the request principal
stateStarting channel state for a new session
title, purpose, dryRun, asOfSet up a new session, as in the POST /v1/session body
deliveryIdNames this send on the session's events, up to 256 characters. Defaults to a fresh dlv_… ID

send returns a ChannelSession with id, the current continuationToken, isNew, and coalesced: true for a coalesced call. On Cursor hosting, it returns the ID before the session exists.

Events ​

The events map subscribes the channel to stream events for its sessions. Keys are types from the event vocabulary, or "*". Each handler receives (event, channel, ctx) with channel.state, session info, and host services, so it can post progress and replies. state on the channel definition is each new session's starting JSON, unless a route passes state to send, and handlers update it through channel.state.

Start and stop a channel ​

onStart(args) runs when the channel mounts, and onStop() runs when the host stops. Besides the route helpers, onStart receives emitAssistantMessage(sessionId, text), which appends assistant text without a model turn, and hasContinuationSession, isContinuationBusy, and interruptContinuation, which check or stop the session behind a token.

Auth policies ​

Every route runs the channel's auth array, which defaults to [localDevStrict()]. The first policy to return an AuthContext admits the request, and if every policy returns null, the route answers 401.

PolicyAdmits
localDevStrict()Direct loopback requests with a loopback hostname and no forwarding headers
localDev()The same, with any hostname
loopbackOnly()Any loopback peer, including relays that add forwarding headers
bearerAuth(tokenOrVerify)A matching bearer token, or one the verifier accepts
sharedSecretAuth({ header, secret })A header that matches the named secret
hmacSignatureAuth({ header, secret, prefix?, separator? })A hex HMAC-SHA256 signature of the raw body under the named secret, or any of several signatures split by separator. The secret can hold up to 32 values, one per line, and any of them verifies
allowAll()Every caller, as an anonymous principal
publicEndpoint()Every caller, with no alias token on hosting. The handler must verify the provider's signature

Use allowAll() only for a public surface or one protected upstream. publicEndpoint() applies only to custom channel routes and doesn't open the built-in session or tool API.

The admitted AuthContext becomes the request principal, which owns the sessions it creates.

On a channel without its own auth, --bearer-token swaps the default for bearerAuth(...) and --allow-anonymous swaps it for allowAll().

Prebuilt channels ​

Import each one from @grok/agent-sdk/channels/<name>.

FunctionDetails
slackChannel()Slack
githubChannel()GitHub
gitlabChannel()GitLab
bitbucketChannel()Bitbucket
pagerdutyChannel()PagerDuty
deploymentsChannel()Deployment events filtered by source, environment, or event name

Use defineChannel for other platforms, such as Discord.

Handler results ​

Hooks on the GitHub, GitLab, Bitbucket, and PagerDuty channels, such as onPullRequest or onIncident, return one of three results:

Return valueWhat happens
{ auth }Runs the model turn with that auth. It can also set the turn's message, title, or context
{ task }Runs your function on the host. That function can call send to start a model turn
nullIgnores the event

Hosted session keys ​

On Cursor hosting, each request to a defineChannel route starts a new session unless it carries X-BDK-Session-Key. Requests with the same value on one channel share one session, in arrival order. The handler's continuationToken doesn't change that choice, and local serve ignores the header.

RuleContract
AuthThe alias token or sharedSecretAuth(). Routes admitted by hmacSignatureAuth() or publicEndpoint() answer 400
Value1 to 256 printable ASCII characters with no space or comma, or the request gets 400. Hosting stores the value, so keep secrets and personal data out of it
ScopeOne channel on one deployment. Prebuilt channels ignore the header
RetriesHosting doesn't deduplicate. A retry runs a second turn