Channels and voice
Put AI agents in Slack, Telegram, GitHub, email and webhooks with camelRun
Let people talk to camelRun AI agents in Slack, Telegram and email, and have agents answer GitHub pull requests, issues and signed webhooks.
View as Markdown · Docs index for coding agents
A channel lets people talk to agents from a messaging service (Telegram, Slack
or email), or has agents answer activity on GitHub. A webhook channel
lets a service that signs its webhooks (Sentry, Linear, Shopify, your own) start
agents. For a short
tour, see Slack AI agents, Telegram AI
bots and GitHub AI
agents on camelRun.
Manage channels with /v1/channels or the console's Channels page:
curl https://run.camelai.com/v1/channels \
-H "Authorization: Bearer $CAMELAI_API_KEY" -H "Content-Type: application/json" \
-d '{"type": "telegram", "credentials": {"botToken": "<from @BotFather>"},
"definition": "def_...", "access": {"allow": ["@ada", "123456789"]}}'Each conversation's agent is made from the channel's definition. A channel
created without one gets an empty definition of its own, which is deleted with
the channel.
| Service | Credentials | Messages arrive by | One conversation (one agent) is |
|---|---|---|---|
telegram | botToken | Webhook, registered for you | A chat |
slack | botToken (xoxb-...), signingSecret | Webhook, pasted into the app | A thread started by an @mention, or a DM |
github | appId, privateKey, webhookSecret | Webhook, pasted into the GitHub App | A pull request or issue |
webhook | secret, optional replyUrl | Webhook, pasted into the sending service | Whatever the key template names (one agent by default) |
email | None | SES, on the runtime's domain | An email thread |
Discord is not a channel type. Discord bots live in camelBots:
creating a discord or discord-managed channel answers 400
CHANNEL_TYPE_RETIRED. A Discord channel made before then is kept as it was:
GET /v1/channels lists it with disabled: true, it answers nothing, it can't
be changed, and DELETE removes it.
Creating a channel checks its credentials with the service. Credentials are stored encrypted, and the API returns only masked values.
Set up each service
The runtime also supports email channels: an address that agents answer
mail at, with allowlists, threads and forwarded support mail. They need a
runtime with its own mail domain set up to receive mail, so they're available
when you self-host, and not on run.camelai.com.
What all channels share
- Each external conversation gets its own agent, made on first contact from the channel's definition, at its revision then.
- Senders must be on the allowlist (
access.allow) unless the channel setsaccess.public: Telegram user ids or @usernames, Slack member ids (U0123ABCD), GitHub logins. Each sender is rate limited (limits.perSenderPerMinute, default 10), and the channel has a daily turn cap (limits.turnsPerDay, default 1,000). - Each message carries its sender as
from, and tool calls carry a runtime-setorigin({channel, conversationId, sender}) that tools can authorize against. See Multi-user. - Attachments are streamed into the agent's workspace at
uploads/<requestId>/<name>: up to 10 files a message, 25 MiB each (20 MB on Telegram) and 100 MiB in all. A file too large or that fails to download is left out, and the prompt says so. Voice notes and audio files are transcribed for the model as they arrive: see Voice. - The turn's final answer is sent back when the turn ends, split to the
service's message limit (4,096 characters on Telegram, 4,000 on Slack), with a typing indicator meanwhile where the service has one.
Channel agents also get a
send_messagetool for updates mid-turn, which can send files too. - Files the agent presents follow the reply: up to 10 MB as photos and 50 MB as documents on Telegram, and 100 MiB on Slack. A larger file is sent as a signed link that works for 24 hours.
- Messages are recorded durably before they are acknowledged, duplicates are dropped, and replies go through a durable outbox, so each part is sent once.
- Questions and approvals appear as text, and the next fitting message from an allowed person answers them. See Human input.
Generic webhooks
A webhook channel turns each delivery from a service into a prompt. Give it
the secret the service signs with, then point the service at the channel's
webhookUrl:
curl https://run.camelai.com/v1/channels \
-H "Authorization: Bearer $CAMELAI_API_KEY" -H "Content-Type: application/json" \
-d '{"type": "webhook", "credentials": {"secret": "<the signing secret>"}, "definition": "def_...",
"settings": {"signature": {"type": "hmac-sha256", "header": "Sentry-Hook-Signature"},
"key": "sentry-{{data.issue.id}}",
"filter": [{"path": "action", "in": ["created"]}],
"prompt": "Sentry issue {{data.issue.title}} ({{data.issue.web_url}}) was created. Triage it."}}'| Setting | |
|---|---|
signature | How a delivery proves itself: {"type": "standard"} (Standard Webhooks, the default), {"type": "hmac-sha256", "header", "prefix"?, "encoding"?} (GitHub, Sentry, Linear, Shopify), or {"type": "token", "header"} (GitLab). Unsigned or wrongly signed deliveries get a 401. A signature over a timestamp and the body, such as Stripe's, isn't one of these, so it can't be verified |
key | Picks the conversation, and so the agent. Deliveries that render to the same key go to the same agent, which keeps its history. Without a key, the channel has one agent. A key with characters other than letters, digits, _, . and -, or longer than 64, is cleaned up and given a hash suffix |
prompt | What the agent is told. By default, the payload itself, pretty-printed (cut off past 8,000 characters). Either way the whole payload is attached as payload.json |
sender | Who a delivery is from, for from and rate limits (default webhook), such as {{actor.email}} |
filter | Conditions that must all hold (path with in, equals or exists), or the delivery is acknowledged and ignored. Values are compared as text |
idPath, idHeader | Where a delivery's id is, so retries are dropped. Without either, the id is the first of webhook-id, X-Request-Id, X-Delivery-Id, X-GitHub-Delivery, Linear-Delivery and Idempotency-Key, else a hash of the body |
The hmac-sha256 header holds the HMAC-SHA256 of the raw body, in hex (or
"encoding": "base64"), after the optional prefix: Linear uses
Linear-Signature, Shopify X-Shopify-Hmac-Sha256 with base64. For
standard, a whsec_ secret is base64 after the prefix; any other secret is
used as it is.
Templates are {{path}}: a dotted path into the JSON payload
(data.items.0.id), {{headers.<name>}} for a request header, or
{{body.<path>}} for a payload field named headers. A missing value renders
as nothing, and an object as JSON. Bodies must be JSON, up to 1 MiB.
There's no conversation to answer, so the agent acts through its tools and the
turn's end reaches you as a run.completed event. To get
the reply too, set credentials.replyUrl: each reply is POSTed there as
{"type": "message", "conversationId", "text"}, signed per Standard Webhooks
with the channel's secret, and retried like any channel's messages. Each
send_message goes there too, and files are sent as a link.
A webhook channel is public (access.public: true) and allows 60 deliveries a
minute per sender: the signature is what keeps others out. It still has the
daily turn cap, and access and limits can be set as for any channel.