Skip to content

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


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.


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.

agent-with-whatsapp.json
{
"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.


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 |


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.


Connects to the Meta WhatsApp Business API. Supports text messages, image attachments, and document attachments.

| 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) |

{
"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 }}"
}

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.

  1. Register the agent and add the WhatsApp channel config.
  2. In Meta Business Manager, set the webhook URL to:
    https://your-runtime-host/event/infer/{assistant_id}/{channel_id}
  3. Set the verify token to match WHATSAPP_VERIFY_TOKEN.
  4. Meta will call GET on the URL to verify — the runtime handles this automatically.

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


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.

| Secret key | Description | |---|---| | SLACK_ACCESS_TOKEN | Slack Bot OAuth token (xoxb-...) |

{
"provider_id": "slack",
"channel_id": "slack-helpdesk",
"slack_access_token": { "$secretRef": "SLACK_ACCESS_TOKEN" },
"template": "{{ answer }}"
}

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.

  1. In your Slack app settings, create a slash command.
  2. Set the Request URL to:
    https://your-runtime-host/event/infer/{assistant_id}/{channel_id}
  3. No webhook verification step is needed — Slack sends the payload directly.

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.

| 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) |

{
"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>"
}

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

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.


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>"

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 |