A channel is an agent’s inbound surface — it turns an external event into invocations: HTTP, GitHub webhooks, Telegram messages, Slack events, even the clock (schedules).
Channels consume only the engine-neutral Agent contract. The same channel can drive any conforming agent.
This page is the USER’s view — what channels exist and how to wire them. Building a new channel adapter is a different audience: see Channel development.
Mental model
external event → channel adapter → agent.invoke(scope, prompt) → channel response/actionA channel decides:
- how to verify and parse an external event,
- whether the agent should run,
- which
sessionto use, - what prompt text/images/files to pass,
- how to acknowledge or reply to the external system.
The agent remains the same assembled workspace.
Workspace discovery
A workspace declares channels with files under channels/:
channels/
├── github.ts # POST /webhook
├── telegram.ts # POST /telegram
└── slack.ts # POST /slackA route channel default-exports a ChannelModule:
import type { ChannelModule } from "@fastagent-sh/fastagent/core";
const channel: ChannelModule = ({ agent, stateRoot }) => ({
"POST /webhook": async (req) => {
// parse req, call agent, return a Response; durable channel state goes under
// `${stateRoot}/channels/<kind>` (never process.cwd())
return new Response(null, { status: 204 });
},
});
export default channel;A long-connection channel instead exports a LongConnectionChannelModule object:
import type { LongConnectionChannelModule } from "@fastagent-sh/fastagent/core";
const channel: LongConnectionChannelModule = {
name: "acme websocket",
connect({ agent, stateRoot }, signal) {
// Start the connection. Translate signal abort into the transport's close operation.
return { ready, closed };
},
};
export default channel;fastagent dev and fastagent start discover every channels/*.ts|*.js|*.mjs. Function exports
contribute route tables to the HTTP server; object exports open long connections. Both receive the
assembled agent and resolved state root. Long-connection adapters own reconnects, observe shutdown
through AbortSignal, and report first readiness plus terminal closure through the two promises.
With no enabled channel files, FastAgent mounts the default HTTP/SSE invoke channel at POST /invoke.
A channel file is enabled by its importable extension (.ts, .js, or .mjs); rename it to, for
example, telegram.ts.disabled to keep it in the workspace without mounting it. A declared channel that
fails to load, or overlaps another channel’s route, makes dev / start fail — it never silently
disappears or triggers the /invoke fallback.
Routes
Route keys are either:
/path # any method
METHOD /path # method-specificExamples:
{
"GET /healthz": () => new Response("ok\n"),
"POST /webhook": webhookHandler,
}A route overlap is surfaced as a collision. A bare /webhook conflicts with POST /webhook; GET /webhook and POST /webhook can coexist.
FastAgent adds a default GET /health route unless a channel already covers it.
First-party channels
FastAgent ships lightweight first-party adapters as subpath exports.
| Channel | Package import | Docs | Add command |
|---|---|---|---|
| GitHub webhook | @fastagent-sh/fastagent/github |
GitHub channel | fastagent add github |
| Telegram bot | @fastagent-sh/fastagent/telegram |
Telegram channel | fastagent add telegram |
| Slack app | @fastagent-sh/fastagent/slack |
Slack channel | fastagent add slack |
| Feishu bot (飞书) | @fastagent-sh/fastagent/feishu |
Feishu channel (Lark compatibility) | fastagent add feishu |
| Lark bot (international) | @fastagent-sh/fastagent/lark |
Feishu channel (Lark compatibility) | fastagent add lark |
Example GitHub glue:
import { githubChannel } from "@fastagent-sh/fastagent/github";
export default githubChannel({
secret: process.env.GITHUB_WEBHOOK_SECRET ?? "",
on: (event) =>
event.event === "pull_request" && event.action === "opened" && "pull_request" in event.payload
? [{ session: event.deliveryId, text: `Review PR #${event.payload.pull_request.number}` }]
: [],
});Example Telegram glue:
import { telegramChannel } from "@fastagent-sh/fastagent/telegram";
export default telegramChannel({
secretToken: process.env.TELEGRAM_SECRET_TOKEN ?? "",
botToken: process.env.TELEGRAM_BOT_TOKEN ?? "",
});Example Slack glue:
import { slackChannel } from "@fastagent-sh/fastagent/slack";
export default slackChannel({
botToken: process.env.SLACK_BOT_TOKEN ?? "",
signingSecret: process.env.SLACK_SIGNING_SECRET ?? "",
botRefreshToken: process.env.SLACK_BOT_REFRESH_TOKEN || undefined,
clientId: process.env.SLACK_CLIENT_ID || undefined,
clientSecret: process.env.SLACK_CLIENT_SECRET || undefined,
botTokenExpiresAt: process.env.SLACK_BOT_TOKEN_EXPIRES_AT
? Number(process.env.SLACK_BOT_TOKEN_EXPIRES_AT)
: undefined,
groupBehavior: "context", // default; choose "mentions" only for explicit least privilege
rendering: "native", // Slack Agent streams/tasks; "classic" for compatibility
// aiDisclaimer: "AI-generated; verify important information.", // optional policy footer
// Direct/group asks default to independent sessions + Slack threads; opt out independently:
// directMessageSession: "continuous",
// groupMessageSession: "continuous",
});Example canonical Feishu glue (Lark international exposes a branded larkChannel compatibility
adapter over this engine and reads LARK_*):
import { feishuChannel } from "@fastagent-sh/fastagent/feishu";
export default feishuChannel({
appId: process.env.FEISHU_APP_ID ?? "",
appSecret: process.env.FEISHU_APP_SECRET ?? "",
verificationToken: process.env.FEISHU_VERIFICATION_TOKEN ?? "",
encryptKey: process.env.FEISHU_ENCRYPT_KEY || undefined,
// Direct/group asks default to independent sessions + platform threads; opt out independently:
// directMessageSession: "continuous",
// groupMessageSession: "continuous",
});A route-adapter call returns a ChannelModule; a WebSocket adapter such as
feishuWebSocketChannel returns a LongConnectionChannelModule. In either form the glue holds only
policy (secrets from env, on/route), while agent and the state root flow from the framework to the adapter without transiting your code.
The adapter owns its default route (POST /webhook, POST /telegram, POST /slack, POST /feishu, POST /lark);
wrap it in your own ChannelModule to remap.
Adapter + glue
A channel usually has two layers:
| Layer | Reusable? | Example |
|---|---|---|
| Adapter | yes | verify a GitHub signature, parse a Telegram update, call an SDK |
| Glue | workspace-specific | map one event to { session, text }, choose routing policy |
Keep transport mechanics in reusable adapters. Keep product/agent policy in the workspace’s channels/*.ts file.
Local webhook development
Webhooks need a public HTTPS URL, but fastagent dev serves localhost. Use:
fastagent dev --tunnelWhen cloudflared is installed, FastAgent opens a Cloudflare quick tunnel, prints the public URL, and auto-registers first-party webhooks where possible:
- Telegram: calls
setWebhookusing.envvalues. - GitHub: prints the Payload URL to paste into repo settings.
- Slack: for an app created by
add slack, rotates its owner-local Configuration Token and updates the App Manifest Request URL; scaffold-only/manual apps receive the URL to paste. - Feishu: PATCHes the app’s event subscription to the tunnel URL via the reference cloud’s config API.
- Lark compatibility: probes the same Feishu mechanism; its lagging config route currently falls back to opening the app console and printing the Request URL.
The tunnel is owned by the dev watch supervisor, so the URL survives worker reloads.
Third-party channels
Heavy or long-tail adapters should live outside @fastagent-sh/fastagent:
{
"name": "fastagent-channel-acme",
"peerDependencies": { "@fastagent-sh/fastagent": "^0.x" },
"dependencies": { "@acme/sdk": "^1" }
}The user’s workspace installs the adapter and wires it with a channel file:
import { acmeChannel } from "fastagent-channel-acme";
export default acmeChannel({
secret: process.env.ACME_SECRET ?? "",
on: (event) => ({ session: event.user, text: event.text }),
});Read Channel development for adapter design, packaging, and testing guidance.
Operational notes
- Channels choose the
sessionstring. Core same-session concurrency fails fast; a channel such as Telegram may queue before invoking. - Post-ACK fire-and-forget work is lost if the process exits unless the channel or host persists intents.
- Public endpoints should verify signatures/secrets and cap request bodies before parsing untrusted payloads.
- User-facing error messages should avoid leaking provider or infrastructure details; log full diagnostics for operators.

