> ## Documentation Index
> Fetch the complete documentation index at: https://koreai.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Channels

Channels connect your deployed agents to the surfaces where users interact: websites, messaging apps, and voice calls. The Agent Platform (Artemis) provides channel adapters that handle the protocol-specific details, so a single agent definition works across every channel without modification.

This guide covers embedding a web chat or voice widget, connecting to Slack, WhatsApp, and voice, and using rich content and file attachments across channels.

A channel always binds to a deployment in a specific environment. Configure deployments first, then add channels on top. For the deployment lifecycle and the **Deployments** -> **Channels** workspace, see [Deploy Agents](/agent-platform/deployment#channels).

## How channels connect

The platform exposes two connection mechanisms. The one you use depends on the surface.

| Mechanism              | Used by                                              | How it works                                                                                                                              |
| ---------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **SDK channel**        | Web chat and voice widgets                           | A public API key (`pk_`) bootstraps a browser session. You embed the Web SDK or the `<agent-widget>` web component in your page.          |
| **Channel connection** | Slack, WhatsApp, voice, and other messaging adapters | You register the external provider's credentials. The platform returns a webhook URL that the provider calls when a user sends a message. |

Both mechanisms route to the same agent definition. The adapter normalizes inbound and outbound formats, so your agent doesn't need channel-specific logic for basic conversations. Use rich content when you want to tailor formatting to a specific channel.

| Surface            | Adapter                                          | Connection              |
| ------------------ | ------------------------------------------------ | ----------------------- |
| Web chat and voice | Web SDK, `<agent-widget>`                        | SDK channel (`pk_` key) |
| Slack              | Slack adapter                                    | Channel connection      |
| WhatsApp           | Meta Cloud API, Infobip, Twilio                  | Channel connection      |
| Microsoft Teams    | Bot Framework                                    | Channel connection      |
| Email              | Inbound MIME parsing                             | Channel connection      |
| Telegram           | Telegram adapter                                 | Channel connection      |
| Voice              | Kore Voice Gateway, AudioCodes, Twilio, BYOC SIP | Channel connection      |

## Deploy on Web

Deploy your agent on a website with the Web SDK. The SDK embeds a chat or voice widget that connects to your deployed agent through a public API key.

The browser-facing SDK never sends the public `pk_` key directly to the runtime. It exchanges the key for a short-lived SDK session token, then authenticates each request with that token. For the full SDK reference, see [Agent Platform SDKs Overview](/agent-platform/sdks).

### Create a Public API Key

The Web SDK authenticates with a public key (`pk_` prefix). Public keys are scoped to a project and are safe to expose in client-side code.

<Steps>
  <Step title="Open API keys">
    Go to **Project** -> **Settings** -> **API Keys**.
  </Step>

  <Step title="Create the key">
    Click **Create API key** and select the SDK key type.
  </Step>

  <Step title="Set allowed origins">
    Add the website origins that are allowed to use the key. The runtime validates the `Origin` header on every SDK request and rejects requests from unlisted origins.
  </Step>

  <Step title="Copy the key">
    Copy the key. It's displayed only once.
  </Step>
</Steps>

### Configure the Web SDK channel

Bind the channel to a deployment so the widget knows which agents to serve.

1. Go to **Deployments** -> **Channels**.
2. Add a Web SDK channel and select the target environment or deployment. The channel can follow an environment automatically or pin to a specific deployment version.
3. Associate the public API key you created.

### Embed the widget

Add the widget to any page with the web component, or use the SDK directly for full control over the interface.

<Tabs>
  <Tab title="Web component">
    The `<agent-widget>` web component is the fastest way to add a widget. It renders a chat button in the configured position and opens the chat interface when clicked.

    ```html theme={null}
    <script src="https://agents.kore.ai/sdk/agent-widget.js"></script>

    <agent-widget
      project-id="your-project-id"
      api-key="pk_your-public-key"
      mode="chat"
      position="bottom-right"
    ></agent-widget>
    ```

    Set `mode` to `chat`, `voice`, or `unified`. Set `position` to `bottom-right`, `bottom-left`, `top-right`, or `top-left`.
  </Tab>

  <Tab title="Vanilla JavaScript">
    Use the `AgentSDK` class when you build your own interface.

    ```typescript theme={null}
    import { AgentSDK } from '@agent-platform/web-sdk';

    const sdk = new AgentSDK({
      projectId: 'your-project-id',
      apiKey: 'pk_your-public-key',
      endpoint: 'https://agents.kore.ai',
    });

    await sdk.connect();

    const chat = sdk.chat();
    chat.on('message', (msg) => console.log(msg.content));
    await chat.send('Hello, I need help!');
    ```
  </Tab>

  <Tab title="React">
    Wrap your app in `AgentProvider` and use the `useChat` hook.

    ```tsx theme={null}
    import { AgentProvider, useChat } from '@agent-platform/web-sdk/react';

    function App() {
      return (
        <AgentProvider
          projectId="your-project-id"
          apiKey="pk_your-public-key"
          endpoint="https://agents.kore.ai"
        >
          <ChatWidget />
        </AgentProvider>
      );
    }

    function ChatWidget() {
      const { messages, isTyping, sendMessage, isConnected } = useChat();
      return (
        <div>
          {messages.map((msg) => (
            <div key={msg.id}>{msg.content}</div>
          ))}
          {isTyping && <div>Agent is typing...</div>}
          <button onClick={() => sendMessage('Hello!')} disabled={!isConnected}>
            Send
          </button>
        </div>
      );
    }
    ```
  </Tab>
</Tabs>

### Enable Voice

Set the widget mode to `voice` for a voice-only experience, or `unified` to offer chat and voice together.

```html theme={null}
<agent-widget
  project-id="your-project-id"
  api-key="pk_your-public-key"
  mode="unified"
  position="bottom-right"
></agent-widget>
```

When you build your own interface, obtain the voice client with `sdk.voice()` and call `start()` to request microphone access. Voice features require HTTPS in production, because microphone access needs a secure context. For voice states, events, and configuration, see the [VoiceClient reference](/agent-platform/sdks#voiceclient).

### Restrict to specific origins

Origin restrictions prevent unauthorized sites from embedding your widget. Set the allowed origins on the public API key under **Project** -> **Settings** -> **API Keys**.

```json theme={null}
{
  "allowedOrigins": [
    "https://www.example.com",
    "https://app.example.com"
  ]
}
```

Requests from origins that aren't listed receive a `403 Forbidden` response.

### Pass user context

Identify authenticated users by attaching metadata to a message. The metadata is validated server-side and is available to your agent for that turn through `session.messageMetadata`.

```typescript theme={null}
const chat = sdk.chat();

await chat.send('Look up my account', {
  metadata: {
    accountId: 'acct_123',
    plan: 'premium',
  },
});
```

Use session variables and persistent memory lookups in your agent to act on this context. See [Memory and State](/agent-platform/memory-and-state).

### Target a specific deployment

To point the widget at a staging or production deployment, set the channel's binding under **Deployments** -> **Channels**. Pin the channel to a specific deployment version, or let it follow an environment so it always serves the active deployment for that environment.

### Troubleshooting

| Symptom                                        | Resolution                                                                                                                    |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Widget doesn't appear**                      | Verify the script URL and that the `api-key` is valid. Check the browser console for errors.                                  |
| **"Invalid or expired API key"**               | The key may have been rotated or the channel deactivated. Create a new key under **Project** -> **Settings** -> **API Keys**. |
| **CORS errors**                                | Add the website's origin to the key's `allowedOrigins` list.                                                                  |
| **Widget loads but the agent doesn't respond** | Confirm an active deployment exists for the project and the channel is bound to it.                                           |

## Set up Slack

Connect your agent to Slack so users can interact with it directly in Slack channels and direct messages. Slack uses a channel connection: you register your Slack app's credentials with the platform, then point Slack's event webhook at the URL the platform returns.

### Create a Slack app

<Steps>
  <Step title="Create the app">
    Go to [api.slack.com/apps](https://api.slack.com/apps) and select **Create New App** -> **From scratch**. Name the app and select the workspace.
  </Step>

  <Step title="Add bot token scopes">
    Under **OAuth & Permissions**, add the bot token scopes your agent needs.

    | Scope                 | Purpose                                            |
    | --------------------- | -------------------------------------------------- |
    | `chat:write`          | Send messages.                                     |
    | `app_mentions:read`   | Respond to @mentions.                              |
    | `im:read`, `im:write` | Handle direct messages.                            |
    | `files:read`          | Read file attachments, if your agent handles them. |
  </Step>

  <Step title="Install and copy credentials">
    Install the app to your workspace and copy the **Bot User OAuth Token** (`xoxb-...`). Under **Basic Information**, copy the **Signing Secret**.
  </Step>
</Steps>

### Create a channel connection

Register the Slack app credentials with the platform. The response includes the `webhookUrl`. Copy it for the next step.

```bash theme={null}
curl -X POST https://your-platform/api/projects/$PROJECT_ID/channel-connections \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_type": "slack",
    "display_name": "Production Slack",
    "external_identifier": "T12345678:A12345678",
    "credentials": {
      "bot_token": "xoxb-your-bot-token",
      "signing_secret": "your-signing-secret"
    },
    "environment": "production"
  }'
```

### Configure the Slack webhook

<Steps>
  <Step title="Enable event subscriptions">
    In your Slack app settings, go to **Event Subscriptions**, enable events, and paste the `webhookUrl` from the channel connection response. Slack sends a verification challenge, and the platform responds automatically.
  </Step>

  <Step title="Subscribe to bot events">
    Under **Subscribe to bot events**, add `message.im` for direct messages and `app_mention` for @mentions in channels. Save your changes.
  </Step>
</Steps>

### Threaded replies

The Slack adapter supports threaded conversations and enables them by default. When a user mentions the bot in a channel, the agent replies in a thread to keep the channel clean.

### Slash commands

Register a custom slash command that routes to your agent.

1. In your Slack app settings, go to **Slash Commands** and create a command, for example `/ask`.
2. Set the request URL to `https://your-platform/api/v1/channels/slack/slash/{connection-identifier}`.
3. The connection identifier is the external identifier from your channel connection, in the format `team_id:app_id`.

### Interactive components

If your agent uses interactive elements such as buttons or select menus, enable interactivity in your Slack app.

1. Go to **Interactivity & Shortcuts** and toggle it on.
2. Set the request URL to the same webhook URL you used for events.
3. The platform routes interactive payloads, such as button clicks and menu selections, to your agent automatically.

See [Rich content](#rich-content) for the Slack Block Kit and interactive action syntax.

### Multi-workspace apps

To distribute your agent across multiple Slack workspaces, use the generic webhook URL without a connection identifier. The platform extracts the workspace identifier from the event payload.

```
https://your-platform/api/v1/channels/slack/webhook
```

Create a separate channel connection for each workspace that installs the app.

### Troubleshooting

| Symptom                                         | Resolution                                                                                                                                                                      |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **"Channel not configured for this workspace"** | The connection's external identifier doesn't match the workspace. Verify the `team_id:app_id` matches your Slack app installation.                                              |
| **Bot doesn't respond**                         | Confirm the Event Subscriptions URL verification succeeded (green checkmark in Slack) and the `bot_token` includes the `chat:write` scope.                                      |
| **Signature verification fails**                | The `signing_secret` must match the Signing Secret on the app's Basic Information page. Secrets are case-sensitive.                                                             |
| **Duplicate responses**                         | Slack requires a response within 3 seconds, so the platform queues messages and responds asynchronously. If your app retries events, the platform deduplicates using event IDs. |

## Set up WhatsApp

Connect your agent to WhatsApp so users can interact with it through WhatsApp messages. WhatsApp uses a channel connection. You register your WhatsApp Business API credentials with the platform, then point your provider's webhook at the URL the platform returns.

### Set up a WhatsApp Business API account

Before connecting to the platform, set up a WhatsApp Business API provider account. The platform supports Meta's Cloud API directly and third-party providers such as Infobip and Twilio.

For Meta Cloud API:

1. Go to [developers.facebook.com](https://developers.facebook.com) and create a Meta app with the WhatsApp product enabled.
2. Under **WhatsApp** -> **Getting Started**, note your **Phone Number ID** and **WhatsApp Business Account ID**.
3. Generate a permanent **System User Access Token** with the `whatsapp_business_messaging` permission.
4. Under **Configuration**, note the **App Secret** for webhook signature verification.

### Create a channel connection

Register the WhatsApp credentials with the platform. The response includes the `webhookUrl`.

```bash theme={null}
curl -X POST https://your-platform/api/projects/$PROJECT_ID/channel-connections \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_type": "whatsapp",
    "display_name": "Production WhatsApp",
    "external_identifier": "your-phone-number-id",
    "credentials": {
      "access_token": "your-system-user-access-token",
      "app_secret": "your-app-secret",
      "verify_token": "a-random-string-you-choose"
    },
    "config": {
      "phoneNumberId": "your-phone-number-id"
    },
    "environment": "production"
  }'
```

### Configure the Meta webhook

1. In your Meta app settings, go to **WhatsApp** -> **Configuration**.
2. Under **Webhook**, click **Edit** and paste the `webhookUrl`.
3. Enter the same `verify_token` you used when creating the channel connection.
4. Click **Verify and Save**. Meta sends a GET request to verify the webhook.
5. Under **Webhook fields**, subscribe to `messages`.

### Connect through Infobip

For WhatsApp through Infobip, use the provider-specific configuration.

```bash theme={null}
curl -X POST https://your-platform/api/projects/$PROJECT_ID/channel-connections \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_type": "whatsapp",
    "display_name": "Infobip WhatsApp",
    "external_identifier": "your-sender-number",
    "credentials": {
      "access_token": "your-infobip-api-key",
      "app_secret": "your-webhook-secret",
      "verify_token": "unused-for-infobip"
    },
    "config": {
      "provider": "infobip",
      "phoneNumberId": "your-sender-number"
    }
  }'
```

Set the Infobip webhook URL to:

```
https://your-platform/api/v1/channels/whatsapp/infobip/webhook
```

### Media support

The WhatsApp adapter receives images, documents, audio, and video from users. The adapter downloads media files and routes them through the attachment pipeline automatically, so no extra configuration is needed. See [File attachments](#file-attachments) for how your agent processes uploaded media.

### Interactive messages

Your agent can send interactive WhatsApp messages, such as buttons and lists, through the `WHATSAPP` rich content format.

```yaml theme={null}
FLOW:
  offer_options:
    REASONING: false
    RESPOND: "How can I help you today?"
    RICH_CONTENT:
      WHATSAPP: |
        {
          "type": "interactive",
          "interactive": {
            "type": "list",
            "body": {"text": "Choose a topic:"},
            "action": {
              "button": "Select",
              "sections": [
                {
                  "title": "Support",
                  "rows": [
                    {"id": "billing", "title": "Billing"},
                    {"id": "technical", "title": "Technical Issue"}
                  ]
                }
              ]
            }
          }
        }
    THEN: handle_selection
```

### Troubleshooting

| Symptom                                              | Resolution                                                                                                                                                                |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Webhook verification fails**                       | The `verify_token` in the channel connection must match the value entered in the Meta webhook configuration. It's a plain string you choose, not a Meta-generated secret. |
| **Messages don't arrive**                            | Confirm you subscribed to the `messages` webhook field in Meta's configuration, and that the phone number is registered with an active WhatsApp Business account.         |
| **Signature verification fails on inbound messages** | The `app_secret` must be the Meta App Secret from Basic Settings, not the system user access token.                                                                       |
| **Media messages aren't processed**                  | Large media files can time out during download. Check the attachment processing logs for the session.                                                                     |

## Set up Voice

Set up a voice channel so users can interact with your agent through phone calls. The platform uses speech-to-text and text-to-speech for natural voice conversations, and supports Kore Voice Gateway, AudioCodes, Twilio, and BYOC SIP trunks. Each uses a channel connection.

### Kore Voice Gateway

Create a channel connection for Kore Voice Gateway. The response includes a SIP endpoint to point your phone number at.

```bash theme={null}
curl -X POST https://your-platform/api/projects/$PROJECT_ID/channel-connections \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_type": "korevg",
    "display_name": "Voice Support Line",
    "external_identifier": "support-line",
    "config": {
      "provider": "korevg",
      "welcomeMessage": "Hello! How can I help you today?",
      "voiceConfig": {
        "sttProvider": "deepgram",
        "sttLanguage": "en-US",
        "ttsProvider": "elevenlabs",
        "ttsVoice": "rachel"
      }
    },
    "environment": "production"
  }'
```

### Twilio Voice

For Twilio-based voice, create a channel connection with Twilio credentials, then configure the Twilio phone number's webhook to point to the platform's voice endpoint.

```bash theme={null}
curl -X POST https://your-platform/api/projects/$PROJECT_ID/channel-connections \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_type": "voice_twilio",
    "display_name": "Twilio Voice",
    "external_identifier": "+15551234567",
    "credentials": {
      "account_sid": "your-twilio-account-sid",
      "auth_token": "your-twilio-auth-token"
    },
    "config": {
      "phoneNumber": "+15551234567"
    },
    "environment": "production"
  }'
```

### Use caller identity

For voice sessions, the runtime makes caller identity available through the `session` namespace. Your agent can pass the caller's number or the dialed number into tools without asking the caller to repeat it.

| Field                        | Description                                                                                                         |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `session.anonymousId`        | Normalized caller identity. For phone calls, this is the caller ANI when the gateway provides a valid phone number. |
| `session.sessionPrincipalId` | The recognized session principal used for contact resolution and session ownership checks.                          |
| `session.calledNumber`       | Normalized number the caller dialed, when available.                                                                |
| `session.rawCallerId`        | Raw caller value before phone normalization.                                                                        |
| `session.rawFrom`            | Raw upstream `From` value before phone normalization.                                                               |
| `session.rawCalledNumber`    | Raw called value before phone normalization.                                                                        |
| `session.rawTo`              | Raw upstream `To` value before phone normalization.                                                                 |

The runtime normalizes phone-like values from the gateway. For example, `sip:+15551234567@example.com` and `+1 (555) 123-4567` both become `+15551234567`. If the first caller value isn't a phone number, the runtime checks trusted SIP identity headers before falling back to the raw identity value.

Use the normalized fields for most tools:

```yaml theme={null}
TOOLS:
  lookup_customer(phone: string, called_number?: string) -> object
    description: "Find the customer record for a voice caller."

FLOW:
  identify_caller

  identify_caller:
    REASONING: false
    CALL: lookup_customer
      WITH:
        phone: session.anonymousId
        called_number: session.calledNumber
      AS: customer
    RESPOND: "Thanks. I found your account."
```

Use raw fields only when your integration needs the original gateway value for audit or carrier-specific troubleshooting.

```yaml theme={null}
CALL: record_voice_context
  WITH:
    caller_id: session.anonymousId
    raw_from: session.rawFrom
    called_number: session.calledNumber
    raw_to: session.rawTo
  AS: voiceContext
```

Caller identity fields can contain phone numbers or carrier-provided identifiers, so treat them as end-user identity data. Don't pass raw fields to external tools unless the tool needs the original gateway value.

### Add voice-specific responses

Use `VOICE:` blocks to provide voice-optimized output alongside text. The block accepts `ssml`, `instructions`, and `plain_text` properties.

```yaml theme={null}
FLOW:
  welcome:
    REASONING: false
    RESPOND: "Welcome to Acme Support. How can I help?"
    VOICE:
      instructions: "Use a warm, professional tone. Speak at a moderate pace."

  confirm_booking:
    REASONING: false
    RESPOND: "Your booking is confirmed. Confirmation number: {{confirmation_id}}."
    VOICE:
      instructions: "Read the confirmation number slowly and clearly, one character at a time."
      ssml: |
        <speak>
          Your booking is confirmed.
          Confirmation number: <say-as interpret-as="characters">{{confirmation_id}}</say-as>.
        </speak>
```

### BYOC SIP

Connect your existing SIP trunk with the bring-your-own-carrier provider.

```bash theme={null}
curl -X POST https://your-platform/api/projects/$PROJECT_ID/channel-connections \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_type": "korevg",
    "display_name": "Enterprise SIP",
    "external_identifier": "enterprise-sip",
    "config": {
      "provider": "byoc_sip",
      "sipGateway": "192.168.1.100:5060",
      "voiceConfig": {
        "sttProvider": "deepgram",
        "ttsProvider": "elevenlabs",
        "ttsVoice": "rachel"
      }
    }
  }'
```

### AudioCodes Voice Gateway

For AudioCodes VoiceAI Connect integration:

```bash theme={null}
curl -X POST https://your-platform/api/projects/$PROJECT_ID/channel-connections \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_type": "audiocodes",
    "display_name": "AudioCodes Voice",
    "external_identifier": "audiocodes-main",
    "config": {
      "botUrl": "https://your-platform/api/v1/channels/audiocodes/webhook"
    }
  }'
```

### Browser-based Voice

Generate a Twilio token for browser-based voice calls from your web application.

```bash theme={null}
curl -X POST https://your-platform/api/v1/voice/token \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "your-project-id",
    "identity": "user-123"
  }'
```

Use the returned token with the Twilio Client SDK in your frontend:

```typescript theme={null}
import { Device } from '@twilio/voice-sdk';

const device = new Device(token);
await device.register();

const call = await device.connect({
  params: { projectId: 'your-project-id' },
});
```

### Control speech with SSML

Use SSML for fine-grained control over speech output, such as pauses, emphasis, and pronunciation.

```yaml theme={null}
RESPOND: "Your account balance is ${{balance}}."
VOICE:
  ssml: |
    <speak>
      Your account balance is
      <say-as interpret-as="currency">USD{{balance}}</say-as>.
      <break time="500ms"/>
      Is there anything else I can help with?
    </speak>
```

### Troubleshooting

| Symptom                                         | Resolution                                                                                                                               |
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **No audio in calls**                           | Confirm the STT and TTS providers are configured with valid API keys. Check the voice service health endpoint.                           |
| **Webhook signature validation fails (Twilio)** | The `auth_token` must match the Twilio account's Auth Token. The platform uses it to validate `X-Twilio-Signature` on incoming webhooks. |
| **Voice responses cut off**                     | Long responses can exceed TTS limits. Keep voice responses concise, or use SSML `<break>` tags to create natural pauses.                 |
| **SIP registration fails**                      | For BYOC SIP, verify the SIP gateway IP and port, and confirm the gateway allows connections from the platform's IP range.               |

## Rich Content

Use rich content to send formatted responses that adapt to each channel's capabilities: Markdown, Adaptive Cards, Slack Block Kit, WhatsApp interactive messages, carousels, and interactive elements.

Attach a `RICH_CONTENT:` block to any `RESPOND` statement. The runtime selects the format that matches the delivery channel and falls back to the plain text `RESPOND` message when no matching format exists. For the complete reference, see [Rich Content and Expressions](/agent-platform/abl-reference/rich-content-and-expressions).

| Format          | Description                                             |
| --------------- | ------------------------------------------------------- |
| `MARKDOWN`      | Formatted Markdown text.                                |
| `ADAPTIVE_CARD` | JSON conforming to the Microsoft Adaptive Cards schema. |
| `HTML`          | HTML content for web-based channels.                    |
| `SLACK`         | JSON conforming to the Slack Block Kit format.          |
| `WHATSAPP`      | JSON for WhatsApp interactive messages.                 |
| `AG_UI`         | JSON for AG-UI events.                                  |
| `CAROUSEL`      | A scrollable collection of cards.                       |

### Add Rich Content to a response

Provide channel-specific formatting alongside the plain text response.

```yaml theme={null}
FLOW:
  show_results:
    REASONING: false
    RESPOND: "Here are your search results."
    RICH_CONTENT:
      MARKDOWN: |
        ## Search Results

        | Hotel | Price | Rating |
        |-------|-------|--------|
        {{#each hotels}}
        | {{name}} | ${{price}} | {{rating}} stars |
        {{/each}}
```

If you don't provide a channel-specific format, the runtime falls back to the plain text `RESPOND` message.

### Adaptive Cards for Microsoft Teams

Send rich interactive cards on Teams with the Adaptive Cards format.

```yaml theme={null}
RESPOND: "Your booking summary"
RICH_CONTENT:
  ADAPTIVE_CARD: |
    {
      "type": "AdaptiveCard",
      "version": "1.5",
      "body": [
        {"type": "TextBlock", "text": "Booking Confirmed", "size": "large", "weight": "bolder"},
        {"type": "FactSet", "facts": [
          {"title": "Hotel", "value": "{{hotel_name}}"},
          {"title": "Dates", "value": "{{checkin}} - {{checkout}}"},
          {"title": "Total", "value": "${{total}}"}
        ]}
      ]
    }
```

### Slack Block Kit

Send formatted messages with Slack blocks.

```yaml theme={null}
RESPOND: "Your order status"
RICH_CONTENT:
  SLACK: |
    {
      "blocks": [
        {"type": "header", "text": {"type": "plain_text", "text": "Order Status"}},
        {"type": "section", "fields": [
          {"type": "mrkdwn", "text": "*Order:* #{{order_id}}"},
          {"type": "mrkdwn", "text": "*Status:* {{status}}"}
        ]},
        {"type": "divider"},
        {"type": "section", "text": {"type": "mrkdwn", "text": "Estimated delivery: {{delivery_date}}"}}
      ]
    }
```

### WhatsApp Interactive Messages

Send interactive list or button messages on WhatsApp.

```yaml theme={null}
RESPOND: "Select an option"
RICH_CONTENT:
  WHATSAPP: |
    {
      "type": "interactive",
      "interactive": {
        "type": "button",
        "body": {"text": "How would you like to proceed?"},
        "action": {
          "buttons": [
            {"type": "reply", "reply": {"id": "modify", "title": "Modify Booking"}},
            {"type": "reply", "reply": {"id": "cancel", "title": "Cancel Booking"}},
            {"type": "reply", "reply": {"id": "info", "title": "More Info"}}
          ]
        }
      }
    }
```

### Carousel of Cards

Send a scrollable carousel of cards across supported channels. Each card has a title, an optional subtitle and image, and action buttons.

```yaml theme={null}
RESPOND: "Available hotels"
RICH_CONTENT:
  CAROUSEL:
    - title: "Grand Hotel"
      subtitle: "$200/night - 4.5 stars"
      imageUrl: "https://example.com/grand-hotel.jpg"
      buttons:
        - id: book_grand
          type: button
          label: "Book Now"
          value: "grand_hotel"
    - title: "City Lodge"
      subtitle: "$120/night - 4.0 stars"
      imageUrl: "https://example.com/city-lodge.jpg"
      buttons:
        - id: book_city
          type: button
          label: "Book Now"
          value: "city_lodge"
```

### Interactive actions

Add buttons, select menus, and input fields with an `ACTIONS:` block. Handle the user's interaction with `ON_ACTION`.

```yaml theme={null}
FLOW:
  offer_actions:
    REASONING: false
    RESPOND: "What would you like to do?"
    ACTIONS:
      - id: view_details
        type: button
        label: "View Details"
      - id: select_category
        type: select
        label: "Category"
        options:
          - id: billing
            label: "Billing"
          - id: technical
            label: "Technical"
          - id: general
            label: "General"
    ON_ACTION:
      view_details:
        RESPOND: "Here are the details..."
        GOTO: show_details
      select_category:
        SET:
          selected_category = action.value
        GOTO: route_to_category
```

The `type` property accepts `button`, `select`, or `input`. For `input` elements, set `inputType` to `text`, `number`, `date`, `time`, or `email`, and add a `submitLabel` and `submitId` for the form's submit button. See [Interactive actions](/agent-platform/abl-reference/rich-content-and-expressions#interactive-actions) for the full property list.

### Multi-channel templates

Define reusable, named responses in the `TEMPLATES:` block. Templates support `{{}}` interpolation and multi-format variants. Reference a template with `TEMPLATE(name)`.

```yaml theme={null}
TEMPLATES:
  booking_summary:
    DEFAULT: |
      Booking Confirmed!
      Hotel: {{hotel_name}}
      Total: ${{total}}
    MARKDOWN: |
      ## Booking Confirmed

      | Detail | Value |
      |--------|-------|
      | **Hotel** | {{hotel_name}} |
      | **Total** | ${{total}} |

FLOW:
  confirm:
    REASONING: false
    RESPOND: TEMPLATE(booking_summary)
    THEN: COMPLETE
```

<Note>
  The reference defines `DEFAULT`, `MARKDOWN`, `ADAPTIVE_CARD`, and `ACTIONS` as template variants. The `HTML` and voice template variants from earlier drafts aren't in the reference. Verify with the product team before documenting them.
</Note>

### How the runtime selects a format

The runtime selects the response format based on the delivery channel and falls back through the priority chain until it finds a defined format. Plain text is always the final fallback.

| Channel     | Priority format                          | Fallback       |
| ----------- | ---------------------------------------- | -------------- |
| Web and SDK | `RICH_CONTENT` (Markdown, HTML, AG-UI)   | Plain text     |
| Mobile      | `RICH_CONTENT` (Adaptive Card, Carousel) | Plain text     |
| Voice       | `VOICE` (SSML, instructions, plain text) | `RESPOND` text |
| Slack       | `RICH_CONTENT` (Slack)                   | Markdown       |
| WhatsApp    | `RICH_CONTENT` (WhatsApp)                | Plain text     |

### Troubleshooting

| Symptom                              | Resolution                                                                                                                                       |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Rich content doesn't render**      | The channel may not support the format. Always provide a plain text `RESPOND` fallback. The runtime uses it when no matching rich format exists. |
| **Adaptive Card JSON parse error**   | Validate the JSON with the [Adaptive Cards Designer](https://adaptivecards.io/designer/). Template variables resolve before JSON parsing.        |
| **Slack blocks rejected by the API** | Slack limits block structure to 50 blocks and 3,000 characters per text element. Validate against Slack's Block Kit Builder.                     |
| **Interactive action isn't handled** | Confirm each button or select element has an `id` that matches a handler key in the `ON_ACTION` block.                                           |

## File Attachments

Handle file attachments so your agent can accept, process, and respond to user-uploaded images, documents, audio, and video.

<Note>
  The `ATTACHMENTS` ABL block and the attachments REST API in this section are pending a published reference. Verify block properties, endpoint paths, and field names before publishing.
</Note>

### Declare attachment fields

Use the `ATTACHMENTS` block in your agent to declare the file types your agent accepts. The runtime prompts the user for each required attachment and validates file type and size before processing.

```yaml theme={null}
AGENT: Document_Processor
GOAL: "Process uploaded documents and extract information"

ATTACHMENTS:
  id_document:
    prompt: "Please upload a photo of your ID"
    category: image
    required: true
    maxFileSizeMb: 10
    allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"]
    ocrEnabled: true

  supporting_document:
    prompt: "Upload any supporting documents (optional)"
    category: document
    required: false
    maxFileSizeMb: 20
    allowedMimeTypes: ["application/pdf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"]
```

### Upload attachments through the API

When you build a custom integration, upload files through the attachments API. The response includes an `attachmentId` and a processing `status`.

```bash theme={null}
curl -X POST https://your-platform/api/projects/$PROJECT_ID/sessions/$SESSION_ID/attachments \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@document.pdf"
```

For the Web SDK, upload with `chat.uploadAttachment(file)` and send the returned ID with a message. The SDK accepts files up to 25 MB each, with a maximum of 10 files per message. See [Agent Platform SDKs Overview](/agent-platform/sdks#file-uploads).

### Check processing status

```bash theme={null}
curl https://your-platform/api/projects/$PROJECT_ID/sessions/$SESSION_ID/attachments/$ATTACHMENT_ID/status \
  -H "Authorization: Bearer $TOKEN"
```

| Field              | Description                                           |
| ------------------ | ----------------------------------------------------- |
| `scanStatus`       | Malware scan result: pending, clean, or infected.     |
| `processingStatus` | Content extraction: pending, processing, or complete. |
| `embeddingStatus`  | Embedding generation: pending or complete.            |

### Extract text from images with OCR

Enable OCR to extract text from images, which is useful for receipts, IDs, and handwritten notes. The extracted text is available in the session as `receipt.extractedText`.

```yaml theme={null}
ATTACHMENTS:
  receipt:
    prompt: "Upload a photo of your receipt"
    category: image
    required: true
    ocrEnabled: true
```

### Transcribe audio

Enable transcription for audio uploads. The transcribed text is available as `voicemail.transcription`.

```yaml theme={null}
ATTACHMENTS:
  voicemail:
    prompt: "Upload the voicemail recording"
    category: audio
    required: true
    transcriptionEnabled: true
    allowedMimeTypes: ["audio/mpeg", "audio/wav", "audio/ogg"]
```

### Extract key frames from video

Extract key frames from video for visual analysis.

```yaml theme={null}
ATTACHMENTS:
  product_video:
    prompt: "Upload a video of the defective product"
    category: video
    required: false
    maxFileSizeMb: 50
    keyFrameExtraction: true
```

### Request attachments in a flow step

Request attachments at specific points in a flow.

```yaml theme={null}
FLOW:
  collect_documents:
    REASONING: false
    PROMPT: "I need to verify your identity. Please upload a photo of your ID."
    GATHER:
      FIELDS:
        - id_photo
          type: attachment
          category: image
          required: true
    THEN: verify_identity

  verify_identity:
    REASONING: true
    GOAL: "Verify the uploaded ID document"
    AVAILABLE_TOOLS: [verify_document, extract_id_data]
    EXIT_WHEN: identity_verified IS SET
    THEN: proceed
```

### Channel-specific handling

Different channels handle attachments differently. The platform's channel adapters normalize all formats, so no agent-side configuration is needed.

| Channel  | Attachment support                                       |
| -------- | -------------------------------------------------------- |
| Web SDK  | Drag-and-drop file upload in the chat widget.            |
| Slack    | Native file sharing, downloaded automatically.           |
| WhatsApp | Image, document, audio, and video through the media API. |
| Teams    | File attachments through the Bot Framework.              |
| Email    | MIME attachments parsed from inbound email.              |
| Telegram | Photos, documents, audio, and video messages.            |

### List and retrieve attachments

The download URL is time-limited (one hour by default) and scoped to the authenticated user.

```bash theme={null}
# List all attachments for a session
curl https://your-platform/api/projects/$PROJECT_ID/sessions/$SESSION_ID/attachments \
  -H "Authorization: Bearer $TOKEN"

# Get a download URL for a specific attachment
curl https://your-platform/api/projects/$PROJECT_ID/sessions/$SESSION_ID/attachments/$ATTACHMENT_ID/url \
  -H "Authorization: Bearer $TOKEN"
```

### Troubleshooting

| Symptom                                             | Resolution                                                                                                                                                             |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **"PAYLOAD\_TOO\_LARGE" on upload**                 | The file exceeds the maximum size, either the upload endpoint limit or the `maxFileSizeMb` declared in the attachment field. Reduce the file size or adjust the limit. |
| **Processing status stuck on "pending"**            | The multimodal processing service may be unavailable. Check service health.                                                                                            |
| **OCR returns empty text**                          | The image may be too low resolution, or the text isn't clearly visible. Recommend that users upload high-quality images.                                               |
| **Attachment isn't available in session variables** | Wait for `processingStatus` to reach `complete` before accessing extracted content. Use the status endpoint to poll.                                                   |

## Related pages

* [Deploy Agents](/agent-platform/deployment) -- deployments, environments, and the **Deployments** -> **Channels** workspace.
* [Agent Platform SDKs Overview](/agent-platform/sdks) -- Web SDK and ABL SDK reference.
* [Rich Content and Expressions](/agent-platform/abl-reference/rich-content-and-expressions) -- the full ABL rich content reference.
* [Safety and Guardrails](/agent-platform/safety-and-guardrails) -- rate limiting for SDK channels.
* [Build Your First Agent](/agent-platform/tutorials/build-your-first-agent) -- end-to-end tutorial including web deployment.
