Channels
Turn platform webhooks (Slack, GitHub, …) into agent dispatches.
Channels are how agents get triggered by the outside world. A webhook hits /channels/:name/*,
the handler verifies the signature, normalizes the platform event, and dispatches to a persistent
agent keyed by a stable conversation id. Outbound actions are tools bound at agent init.
The core seam lives in @fabric-harness/sdk; vendor adapters (Slack, GitHub) live in
@fabric-harness/channels behind subpath exports, so platform SDK code stays out of core. Handlers
are written against the Web Request/Response API and crypto.subtle, so the same channel runs on
Node and Cloudflare.
Why channels
- Stateless. A channel is a route container plus a conversation-id (de)serializer. Session continuity falls out of the key — the same Slack thread → same key → same session.
- Exactly-once. The platform event id (Slack
event_id, GitHubX-GitHub-Delivery) becomes thedispatchId, so a webhook redelivery collapses to a single agent turn via the persistent-run idempotency marker. - Identity propagation. The platform user becomes the dispatch
actorand the team/org becomes thetenantId— flowing into the audit trail and into on-behalf-of governance (e.g. Databricks UC). - Policy-gated outbound. Outbound actions are
defineTooltools (effect: 'write'), so the agent'sCapabilityPolicycan gate "can this agent post to Slack".
Slack
A channel lives in .fabricharness/channels/<name>.ts and exports a channel:
import { createSlackChannel } from '@fabric-harness/channels/slack';
export const channel = createSlackChannel({
signingSecret: process.env.SLACK_SIGNING_SECRET!,
agent: 'assistant', // dispatch app mentions / threaded messages here
});The dev server mounts it at POST /channels/slack/events. Point your Slack app's Event
Subscriptions → Request URL there and subscribe to app_mention. The handler verifies the v0
HMAC signature, answers the URL-verification challenge, and dispatches the event keyed by the thread.
Bind the reply tool to the thread at agent init — the instance id is the thread key:
import { createAgent } from '@fabric-harness/sdk';
import { parseSlackConversationKey, replyInSlackThread } from '@fabric-harness/channels/slack';
export default createAgent(({ id }) => {
const thread = parseSlackConversationKey(id);
return {
model: 'anthropic/claude-haiku-4-5',
tools: [replyInSlackThread(thread, { botToken: process.env.SLACK_BOT_TOKEN! })],
};
});GitHub
Same shape, different signature scheme (X-Hub-Signature-256) and key (owner/repo/<kind>/<number>):
import { createGitHubChannel } from '@fabric-harness/channels/github';
export const channel = createGitHubChannel({
secret: process.env.GITHUB_WEBHOOK_SECRET!,
agent: 'triage',
});Mounted at POST /channels/github/webhook. It acknowledges ping, dispatches issue / PR / comment
events (with X-GitHub-Delivery as the dedupe key, the owner as tenant, the sender as actor), and
ignores bot senders to avoid loops. Outbound: commentOnGitHubIssue(ref, { token }).
Authoring your own channel
A channel is defineChannel({ routes, conversationKey, parseConversationKey }). The SDK provides the
building blocks: verifyHmacSha256 (constant-time), hexToBytes, conversationKey /
parseConversationKey (url-safe), and readJsonBody (raw bytes for HMAC and parsed JSON from a
single read). Inside a route handler, call ctx.dispatch(agent, { instanceId, input, dedupeKey, tenantId, actor }).
See also
- Example:
examples/with-slack-channel— Slack mention → agent → in-thread reply, end to end. - Persistent agents — channels dispatch to
createAgent(...)instances. - Triggers — gating which agents accept inbound events.