Channels
Channels are inbound/outbound adapters that connect an agent to an external messaging platform. When a message arrives on a channel, the adapter normalizes it into a standard AssistantInference command and dispatches it through the same execution pipeline as a direct API call. When the agent responds, the adapter routes the answer back to the originating platform.
Alquimia ships three channel adapters: WhatsApp (Meta Business API), Slack (slash commands), and Email (IMAP polling + SMTP replies).
How channels work
Section titled “How channels work”External platform → POST /event/infer/{assistant_id}/{channel_id} │ ▼ channel.ingest(payload) │ normalizes to AssistantInference ▼ middleware.is_allowed() │ passes or blocks ▼ anotify_sink() → Kafka → worker/dispatcher → LLM │ channel.respond(answer, context) ◄─────────────┘The channel_id in the URL path maps to a specific channel object in the agent’s channels array. The runtime looks up the channel by channel_id and calls its ingest() method with the raw request body.
Configuring channels in an agent
Section titled “Configuring channels in an agent”Channels are declared in the agent’s AssistantConfig under a top-level channels array. Each entry is a channel object with a provider_id discriminator.
{ "assistant_id": "support-agent", "channels": [ { "provider_id": "whatsapp", "channel_id": "whatsapp-prod", "whatsapp_assistant_phone_number_id": { "$secretRef": "WHATSAPP_PHONE_NUMBER_ID" }, "whatsapp_verify_token": { "$secretRef": "WHATSAPP_VERIFY_TOKEN" }, "whatsapp_access_token": { "$secretRef": "WHATSAPP_ACCESS_TOKEN" }, "whatsapp_api_base_url": { "$secretRef": "WHATSAPP_API_BASE_URL" }, "template": "{{ answer }}" } ], "response": { ... }}The channel_id value is arbitrary but must be unique within the agent. It becomes the {channel_id} path segment in the webhook URL.
Channel endpoints
Section titled “Channel endpoints”Two HTTP endpoints handle channel traffic. Neither requires an Authorization header — authentication is handled by the channel provider’s own verification mechanism (e.g., WhatsApp’s hub.verify_token).
GET /event/infer/{assistant_id}/{channel_id}
Section titled “GET /event/infer/{assistant_id}/{channel_id}”Webhook validation. Called by the channel provider during setup to verify the endpoint URL.
| Parameter | Description |
|---|---|
| assistant_id | The agent to validate for |
| channel_id | Must match a channel_id in the agent’s channels array |
The runtime delegates to channel.resolve_challenge(). For WhatsApp, this checks hub.verify_token against the configured secret and returns hub.challenge.
Error responses:
| Status | Condition |
|---|---|
| 400 | channel_id not found in agent config |
| 500 | Challenge resolution failed |
POST /event/infer/{assistant_id}/{channel_id}
Section titled “POST /event/infer/{assistant_id}/{channel_id}”Receive an inbound message and dispatch it as an inference request.
The request body format is provider-specific (WhatsApp JSON webhook, Slack URL-encoded form, etc.). The channel adapter normalizes it. The response is a CommonAttributes object identical to POST /event/infer/{assistant_id}.
Error responses:
| Status | Condition |
|---|---|
| 400 | channel_id not found in agent config |
| 400 | Channel produced no commands from the payload (e.g., empty WhatsApp webhook) |
| 500 | Connection error to Redis or CloudEvent sink |
Middleware
Section titled “Middleware”Every channel supports an optional middleware array. Middleware runs before the inference command is dispatched. If any middleware raises PermissionError, the request is blocked and the agent’s respond() method is called with the rejection reason.
{ "provider_id": "whatsapp", "channel_id": "whatsapp-prod", "middleware": [ { "provider_id": "allowlist", "allowed_users": ["+15551234567", "+15559876543"] } ], ...}See Security Middleware for available middleware providers.
WhatsApp channel
Section titled “WhatsApp channel”Connects to the Meta WhatsApp Business API. Supports text messages, image attachments, and document attachments.
Required secrets
Section titled “Required secrets”| Secret key | Description |
|---|---|
| WHATSAPP_PHONE_NUMBER_ID | The phone number ID from Meta Business Manager |
| WHATSAPP_VERIFY_TOKEN | Token used to verify the webhook URL during setup |
| WHATSAPP_ACCESS_TOKEN | Meta Graph API access token |
| WHATSAPP_API_BASE_URL | Meta Graph API base URL (e.g., https://graph.facebook.com/v19.0) |
Configuration
Section titled “Configuration”{ "provider_id": "whatsapp", "channel_id": "whatsapp-prod", "whatsapp_assistant_phone_number_id": { "$secretRef": "WHATSAPP_PHONE_NUMBER_ID" }, "whatsapp_verify_token": { "$secretRef": "WHATSAPP_VERIFY_TOKEN" }, "whatsapp_access_token": { "$secretRef": "WHATSAPP_ACCESS_TOKEN" }, "whatsapp_api_base_url": { "$secretRef": "WHATSAPP_API_BASE_URL" }, "template": "{{ answer }}"}How it works
Section titled “How it works”Ingest: The adapter parses the WhatsApp webhook JSON payload. Text messages become the query string. Images and documents are fetched from the Meta API, base64-encoded, and attached as Blob objects to the inference command. The sender’s phone number becomes the user_id and session_id, so each phone number gets its own conversation history.
Respond: The adapter calls POST /{phone_number_id}/messages on the Meta Graph API with a text message. If the original message had an ID, the reply is threaded using the context.message_id field.
Webhook setup
Section titled “Webhook setup”- Register the agent and add the WhatsApp channel config.
- In Meta Business Manager, set the webhook URL to:
https://your-runtime-host/event/infer/{assistant_id}/{channel_id}
- Set the verify token to match
WHATSAPP_VERIFY_TOKEN. - Meta will call
GETon the URL to verify — the runtime handles this automatically.
Supported message types
Section titled “Supported message types”| WhatsApp type | Handling |
|---|---|
| text | Becomes the inference query |
| image | Fetched from Meta API, attached as a Blob |
| document | Fetched from Meta API, attached as a Blob |
| Other types | Ignored silently |
Slack channel
Section titled “Slack channel”Connects to Slack slash commands. When a user runs a slash command in Slack, the payload is sent to the channel endpoint and the agent’s response is posted back to the channel.
Required secrets
Section titled “Required secrets”| Secret key | Description |
|---|---|
| SLACK_ACCESS_TOKEN | Slack Bot OAuth token (xoxb-...) |
Configuration
Section titled “Configuration”{ "provider_id": "slack", "channel_id": "slack-helpdesk", "slack_access_token": { "$secretRef": "SLACK_ACCESS_TOKEN" }, "template": "{{ answer }}"}How it works
Section titled “How it works”Ingest: The adapter parses the URL-encoded form body sent by Slack. The text field (everything after the slash command) becomes the query. The user ID is {team_id}-{user_id} for cross-workspace uniqueness.
Respond: The adapter calls chat.postMessage on the Slack Web API, posting the answer to the originating channel.
Slash command setup
Section titled “Slash command setup”- In your Slack app settings, create a slash command.
- Set the Request URL to:
https://your-runtime-host/event/infer/{assistant_id}/{channel_id}
- No webhook verification step is needed — Slack sends the payload directly.
Email channel
Section titled “Email channel”Connects to an email inbox via IMAP polling and sends replies via SMTP. The adapter polls for unseen messages on a configurable interval and marks them as read after processing.
Required secrets
Section titled “Required secrets”| Secret key | Description |
|---|---|
| EMAIL_USERNAME | Email address (used for both IMAP login and SMTP From) |
| EMAIL_PASSWORD | Email account password or app password |
| EMAIL_SMTP_SERVER | SMTP server hostname (e.g., smtp.gmail.com) |
| EMAIL_IMAP_SERVER | IMAP server hostname (e.g., imap.gmail.com) |
| EMAIL_SMTP_SSL | true to use STARTTLS (boolean) |
| EMAIL_SMTP_PORT | SMTP port (integer, e.g., 587) |
Configuration
Section titled “Configuration”{ "provider_id": "email", "channel_id": "email-support", "email_username": { "$secretRef": "EMAIL_USERNAME" }, "email_password": { "$secretRef": "EMAIL_PASSWORD" }, "email_smtp_server": { "$secretRef": "EMAIL_SMTP_SERVER" }, "email_imap_server": { "$secretRef": "EMAIL_IMAP_SERVER" }, "email_smtp_ssl": { "$secretRef": "EMAIL_SMTP_SSL" }, "email_smtp_port": { "$secretRef": "EMAIL_SMTP_PORT" }, "read_mailbox": "inbox", "email_select_status": "UNSEEN", "template": "<html><body>{{ answer | markdown_to_html }}</body></html>"}Configuration fields
Section titled “Configuration fields”| Field | Default | Description |
|---|---|---|
| read_mailbox | "inbox" | IMAP mailbox to poll |
| email_select_status | "UNSEEN" | IMAP search criteria for messages to process |
| flag_on_read | "(\\Seen)" | IMAP flag set when a message is picked up |
| unflag_on_error | "(\\Seen)" | IMAP flag removed if processing fails |
| template | HTML template | Jinja2 template for the reply body. Supports markdown_to_html and text_to_quoted_html filters. |
How it works
Section titled “How it works”Ingest: The adapter connects to the IMAP server, searches for messages matching email_select_status, and marks each one as read before processing. The email body is parsed to extract only the latest reply (quoted history is stripped). Attachments are extracted and attached as Blob objects. The sender’s email address becomes the user_id and session_id.
Respond: The adapter sends an HTML reply via SMTP. The reply is threaded using In-Reply-To and References headers. The response body is rendered through the Jinja2 template.
Response templates
Section titled “Response templates”All channels support a template field — a Jinja2 template that formats the agent’s answer before sending it to the platform.
The {{ answer }} variable contains the agent’s raw response text.
"template": "{{ answer }}"The email channel additionally supports two filters:
| Filter | Description |
|---|---|
| markdown_to_html | Converts Markdown to HTML |
| text_to_quoted_html | Wraps text in an HTML <blockquote> |
"template": "<html><body>{{ answer | markdown_to_html }}</body></html>"ExtraDataContext
Section titled “ExtraDataContext”When a channel processes a message, it populates an ExtraDataContext object with platform-specific metadata. This context is passed through the inference pipeline and is available to the agent’s tools and evaluation strategies.
| Field | Set by | Description |
|---|---|---|
| whatsapp_message_id | WhatsApp | Original message ID (used for threading replies) |
| whatsapp_message_from | WhatsApp | Sender’s phone number |
| email_message_recipient | Email | Sender’s email address |
| email_message_subject | Email | Email subject line |
| email_message_id | Email | Message-ID header (used for threading) |
| email_message_body | Email | Full email body |
| slack_team_id | Slack | Slack workspace ID |
| slack_user_id | Slack | Slack user ID |
| slack_channel_id | Slack | Slack channel ID |
| slack_response_url | Slack | Slack response URL |
Related pages
Section titled “Related pages”- Agent Configuration — full
AssistantConfigschema includingchannels - Inference Endpoints —
POST /event/infer/{assistant_id}/{channel_id}endpoint details - Tools & Integrations — connecting tools to channel-enabled agents