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; 17 vendor adapters 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 }).
Other first-party channels
Scaffold the provider module, environment template, dependency set, and starter test:
fh add channel discord
fh add channel teams
fh add channel telegram
fh add channel twilio
fh add channel whatsapp
fh add channel google-chat
fh add channel linear
fh add channel stripe
fh add channel zendeskEach first-party adapter verifies the provider's native request authentication, assigns the provider
event ID as dedupeKey, derives a stable conversation key, and propagates tenant/actor identity.
Provider setup and outbound tool examples are in the channel catalog.
Microsoft Teams includes Bot Connector key discovery and validates issuer, App ID audience, token
lifetime, service URL, and channel endorsement without requiring an application JWT library.
The subpath exports include typed webhook payloads and conversation refs for custom routing. The built-in normalization covers the event families an agent commonly needs:
| Provider | Normalized events |
|---|---|
| Slack | mentions, messages, edited messages |
| GitHub | opened, edited, reopened, synchronized, and issue/PR comments |
| Discord | commands and component callbacks |
| Teams | messages, message updates, and deletes |
| Telegram | messages, edited messages, channel posts, and callback queries |
| Twilio | SMS/WhatsApp messages and delivery statuses |
| text messages and delivery/read statuses | |
| Google Chat | messages and interactions |
| Linear | issue and project resource changes |
| Notion | workspace entity changes |
| Stripe | account and financial resource events |
| Zendesk | ticket events |
| Intercom | conversation and ticket notifications |
| Shopify | versioned commerce webhooks |
| Messenger | messages and postbacks |
| Resend | inbound and delivery email events |
| Salesforce Marketing Cloud | signed ENS event batches |
Delivery-status dedupe keys include both message ID and status, so a sent event cannot suppress a
later delivered or read event.
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. - Example:
examples/with-channel-adapters— all 18 verified ingress adapters and their governed outbound tools in one runnable workspace. - Persistent agents — channels dispatch to
createAgent(...)instances. - Triggers — gating which agents accept inbound events.