camelAI Documentation

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:

bash
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.

ServiceCredentialsMessages arrive byOne conversation (one agent) is
telegrambotTokenWebhook, registered for youA chat
slackbotToken (xoxb-...), signingSecretWebhook, pasted into the appA thread started by an @mention, or a DM
githubappId, privateKey, webhookSecretWebhook, pasted into the GitHub AppA pull request or issue
webhooksecret, optional replyUrlWebhook, pasted into the sending serviceWhatever the key template names (one agent by default)
emailNoneSES, on the runtime's domainAn 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 sets access.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-set origin ({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_message tool 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:

bash
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
signatureHow 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
keyPicks 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
promptWhat 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
senderWho a delivery is from, for from and rate limits (default webhook), such as {{actor.email}}
filterConditions that must all hold (path with in, equals or exists), or the delivery is acknowledged and ignored. Values are compared as text
idPath, idHeaderWhere 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.