ClawMart AI
← Back to Blog
August 18, 20268 min readClaw Mart Team

Multi-Channel Setup: Run One Agent on Discord, Slack & Email

Multi-Channel Setup: Run One Agent on Discord, Slack & Email

Multi-Channel Setup: Run One Agent on Discord, Slack & Email

Let me be honest about something: running an AI agent on one platform is easy. Running that same agent across Discord, Slack, and email — with consistent behavior, shared memory, and no duplicated code — is where most people's setups fall apart spectacularly.

I've watched developers spend weeks duct-taping together separate bots for each channel, only to end up with three slightly different agents that don't share context, break at different times, and cost triple what they should. It's painful. And it's completely unnecessary if you set things up correctly from the start.

OpenClaw was built for exactly this kind of multi-channel deployment. One agent definition. Multiple channels. Shared state. Let me walk you through how to actually do it — not the theoretical "wouldn't it be nice" version, but the practical "here's the code, here's where it breaks, here's how to fix it" version.

The Core Problem Nobody Warns You About

Before we get into setup, let's talk about why this is hard in the first place. It's not the API connections. Those are straightforward. The hard part is everything that happens after the message arrives.

Consider this scenario: A user asks your agent on Slack, "What's the status of Project Alpha?" The agent responds with a detailed update. Thirty minutes later, that same user hops on Discord and says, "Can you change the deadline on that to next Friday?"

If your agent doesn't know "that" refers to Project Alpha from the Slack conversation, your user just hit a wall. They have to repeat themselves. They lose trust in the agent. And you lose the entire point of having a multi-channel setup.

This is the context fragmentation problem, and it's the number one reason multi-channel agents fail in production. The second reason? Message format hell — every platform wants its own special snowflake format for images, buttons, embeds, and rich content.

OpenClaw solves both of these. Here's how.

Step 1: Define Your Agent Once

The foundational principle is simple: your agent logic should exist in exactly one place. The channels are just transport layers — different doors into the same room.

Here's what a basic multi-channel agent definition looks like in OpenClaw:

from openclaw import Agent, Context, Message
from openclaw.channels import SlackChannel, DiscordChannel, EmailChannel

agent = Agent(
    name="support-agent",
    description="Handles customer support across all channels",
    channels={
        "slack": SlackChannel.from_env(),      # Reads SLACK_BOT_TOKEN, etc.
        "discord": DiscordChannel.from_env(),  # Reads DISCORD_TOKEN, etc.
        "email": EmailChannel.from_env()       # Reads EMAIL_IMAP_*, SMTP_*, etc.
    }
)

@agent.on_message()
async def handle_message(ctx: Context):
    # This runs regardless of which channel the message came from
    user_history = await ctx.session.get_history(
        user_id=ctx.user.unified_id,
        lookback_window="24h"
    )
    
    response = await ctx.generate(
        prompt=ctx.message,
        context=user_history
    )
    
    return Message(text=response)

Notice what's happening here. The handle_message function doesn't know or care whether the message came from Slack, Discord, or email. It processes the message, pulls in cross-channel history via ctx.user.unified_id, generates a response, and sends it back through whatever channel the message arrived on.

That unified_id is doing heavy lifting. OpenClaw maintains an identity mapping layer that links a user's Discord ID, Slack ID, and email address into a single identity. When someone messages you on Discord after a Slack conversation, the agent sees their full history.

Step 2: Set Up Channel Authentication

This is where most tutorials gloss over the details and leave you stuck for a day. Each platform has its own authentication dance, and it's genuinely annoying. OpenClaw abstracts most of it, but you still need to set up the initial credentials.

For Slack:

Create a Slack app at api.slack.com, enable Socket Mode, and grab your tokens. You need three environment variables:

SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_APP_TOKEN=xapp-your-app-token
SLACK_SIGNING_SECRET=your-signing-secret

For Discord:

Create an application at discord.com/developers, create a bot user, enable the message content intent (this trips people up constantly), and grab your token:

DISCORD_TOKEN=your-bot-token
DISCORD_GUILD_ID=your-server-id  # Optional: restrict to one server

For Email:

This one's the most variable depending on your email provider. For a standard IMAP/SMTP setup:

EMAIL_IMAP_HOST=imap.gmail.com
EMAIL_IMAP_USER=agent@yourdomain.com
EMAIL_IMAP_PASSWORD=your-app-password
EMAIL_SMTP_HOST=smtp.gmail.com
EMAIL_SMTP_PORT=587
EMAIL_FROM_ADDRESS=agent@yourdomain.com

Once these are in your environment (or a .env file), the .from_env() calls in your agent definition handle the rest — OAuth flows, token refresh, webhook verification, signature validation. All behind the scenes.

OpenClaw also ships with a CLI tool that walks you through the setup interactively:

openclaw channels setup slack
openclaw channels setup discord
openclaw channels setup email

It opens the browser for OAuth flows, validates your tokens, and saves everything securely. It takes about five minutes per channel instead of the usual afternoon of documentation-spelunking.

Step 3: Handle Message Formats Without Losing Your Mind

Here's a real scenario that will ruin your afternoon if you're not prepared for it. Your agent needs to send an image with a call-to-action button. Here's what that looks like if you're writing platform-specific code:

# Slack wants Block Kit
slack_msg = {
    "blocks": [
        {"type": "image", "image_url": url, "alt_text": "Report"},
        {"type": "actions", "elements": [
            {"type": "button", "text": {"type": "plain_text", "text": "Download"}, "action_id": "download"}
        ]}
    ]
}

# Discord wants embeds + components
discord_msg = {
    "embeds": [{"image": {"url": url}}],
    "components": [{"type": 1, "components": [
        {"type": 2, "style": 1, "label": "Download", "custom_id": "download"}
    ]}]
}

# Email wants... HTML
email_msg = f'<img src="{url}"><br><a href="https://...">Download</a>'

Three completely different formats for the same conceptual message. Now multiply this by every message your agent sends, and you start to understand why people burn out on multi-channel setups.

OpenClaw's answer is a unified message abstraction:

@agent.on_command("weekly_report")
async def send_report(ctx: Context):
    return Message(
        text="Here's your weekly analytics report:",
        attachments=[
            Image(url="https://charts.example.com/weekly.png", alt="Weekly sales chart"),
            Button("Download PDF", action="download_pdf"),
            Button("Share with Team", action="share_report")
        ]
    )

You write it once. OpenClaw converts it to native Block Kit for Slack, embeds and components for Discord, and clean HTML for email. When a platform doesn't support a feature (email can't do interactive buttons), it degrades gracefully — buttons become links, interactive elements become plain text alternatives.

You can also drop down to platform-native formatting when you need to. This is crucial. Any abstraction that doesn't let you escape it when necessary is a trap:

@agent.on_command("advanced_form")
async def show_form(ctx: Context):
    if ctx.channel.platform == "slack":
        return ctx.channel.native_response({
            "blocks": [
                {
                    "type": "input",
                    "element": {"type": "datepicker"},
                    "label": {"type": "plain_text", "text": "Select date"}
                }
            ]
        })
    else:
        return Message(text="Please reply with your preferred date (YYYY-MM-DD):")

Best of both worlds. Use the abstraction when it works. Escape it when you need platform-specific richness.

Step 4: Implement Smart Message Routing

This is the part that saves you money. Literally.

I've seen teams whose LLM costs exploded because their Discord bot was processing every single message in a busy server — "lol", "😂", reaction spam, off-topic chatter. Every message hitting the LLM. Every message costing money.

OpenClaw gives you routing controls to filter what actually reaches your agent logic:

agent = Agent(
    name="support-agent",
    routing=RoutingConfig(
        discord=DiscordRouting(
            trigger="mention_or_dm",        # Only respond when @mentioned or DM'd
            ignore_bots=True,               # Don't respond to other bots
            allowed_channels=["support", "general"]  # Only listen in specific channels
        ),
        slack=SlackRouting(
            trigger="mention_or_dm",
            thread_behavior="follow",       # Once tagged in a thread, follow it
            ignore_emoji_only=True           # Skip messages that are just emoji
        ),
        email=EmailRouting(
            allowed_domains=["yourdomain.com", "client.com"],
            ignore_auto_replies=True,        # Don't get caught in auto-reply loops
            subject_filter=["support", "help", "question"]
        )
    )
)

The email routing deserves special attention because email auto-reply loops are a real and terrifying thing. Your agent gets an email, responds, the sender has an out-of-office auto-reply, your agent responds to the auto-reply, the auto-responder responds back... infinite loop. OpenClaw's ignore_auto_replies catches common auto-reply headers and breaks the cycle automatically.

Step 5: Rate Limiting That Actually Works

When you're running on multiple channels, rate limiting gets complicated fast. Discord has different limits than Slack. Slack has per-channel and per-workspace limits. Email providers will flag you as spam if you send too fast.

agent = Agent(
    channels={
        "discord": DiscordChannel(
            token=os.getenv("DISCORD_TOKEN"),
            rate_limit=RateLimit(
                messages_per_second=40,
                burst_size=10,
                priority_queue=True
            )
        ),
        "slack": SlackChannel(
            token=os.getenv("SLACK_TOKEN"),
            rate_limit=RateLimit(
                messages_per_minute=60,
                per_channel=True
            )
        ),
        "email": EmailChannel(
            rate_limit=RateLimit(
                messages_per_minute=20,  # Stay under spam thresholds
                per_recipient=True
            )
        )
    }
)

The priority_queue=True flag is particularly useful. When you have urgent messages — system alerts, time-sensitive notifications — they jump the queue instead of waiting behind a backlog of casual responses:

@agent.on_message(priority="high")
async def urgent_alert(ctx: Context):
    await ctx.send("⚠️ Critical: Database failover in progress. ETA 5 minutes.")

Step 6: Conversation State Across Channels

Multi-step conversations are where multi-channel setups traditionally implode. You need to collect three pieces of information from a user, and they might give you the first answer on Slack and the second on Discord.

OpenClaw's ConversationFlow handles this:

from openclaw.state import ConversationFlow, Step

intake_flow = ConversationFlow(
    name="bug_report",
    steps=[
        Step("ask_description", prompt="Describe the bug you're experiencing:"),
        Step("ask_severity", 
             prompt="How severe is this?",
             choices=["Critical", "High", "Medium", "Low"]),
        Step("ask_steps", prompt="What steps reproduce the bug?")
    ]
)

@agent.on_command("report_bug")
async def start_bug_report(ctx: Context):
    session = await ctx.start_flow(intake_flow)
    return session.next_step()

@agent.on_message(in_flow="bug_report")
async def handle_bug_report_step(ctx: Context):
    session = ctx.flow_session
    
    if await session.validate_and_advance(ctx.message):
        if session.complete:
            data = session.collected_data
            await create_ticket(data)
            return Message("Bug report filed. You'll get updates here and via email.")
        return session.next_step()
    else:
        return Message(f"That doesn't look right. {session.current_step.prompt}")

The state persists across channels automatically. User starts the bug report on Slack, goes to lunch, comes back and finishes it on Discord. The flow picks up right where they left off because it's tied to their unified identity, not to a specific channel session.

Step 7: Testing Without Losing Your Sanity

You can't manually test multi-channel flows. Well, you can — but you'll need multiple accounts, multiple devices, and the patience of a monk. OpenClaw's testing harness lets you simulate the whole thing:

import openclaw.testing as test

async def test_cross_channel_context():
    sim = test.ChannelSimulator()
    
    agent = create_agent(
        channels={
            "slack": sim.create_channel("slack"),
            "discord": sim.create_channel("discord")
        }
    )
    
    # User asks on Slack
    await sim.send_message(
        channel="slack", user="user_42", text="What's the status of Project Alpha?"
    )
    response = await sim.wait_for_response(channel="slack")
    assert "Project Alpha" in response.text
    
    # Same user continues on Discord
    await sim.send_message(
        channel="discord", user="user_42", text="Push the deadline to next Friday"
    )
    response = await sim.wait_for_response(channel="discord")
    assert "deadline" in response.text.lower()
    assert "friday" in response.text.lower()
    # Agent should know we're talking about Project Alpha
    assert "Project Alpha" in response.text

Deterministic. Repeatable. No test accounts needed. You can run this in CI and catch regressions before they hit production.

Debugging When Things Go Wrong

They will go wrong. The question is whether you can figure out why in five minutes or five hours.

OpenClaw's tracing gives you a complete view of every message's journey:

Trace ID: msg_7f3a2b
├─ [Slack] User @sarah: "cancel my subscription" (14:22:01)
├─ [Agent] Matched intent: subscription_cancel (14:22:01)
├─ [Agent] Fetching user history across channels (14:22:02)
├─ [LLM] Request sent to model (14:22:02)
├─ [LLM] Response received, 143 tokens (14:22:04)
├─ [Slack] Sending confirmation with buttons (14:22:04)
│  └─ ✓ Delivered, message_ts: 1234567890.123
├─ [Email] Sending cancellation receipt (14:22:05)
│  └─ ✓ Sent to sarah@company.com
└─ [Discord] Skipped — user not active on Discord

Every channel, every step, every success and failure. When something breaks, you see exactly where in the chain it happened.

The Fastest Way to Get This Running

Here's my honest recommendation. You can set all of this up from scratch. Everything I've walked through above is well-documented in OpenClaw's docs, and if you enjoy configuring things from the ground up, have at it.

But if you'd rather skip the boilerplate and start with something that already works, Felix's OpenClaw Starter Pack on Claw Mart is the move. It's $29, and it includes pre-configured skills for exactly this kind of multi-channel setup — message routing, identity mapping, conversation state management, format handling. The channel configurations come pre-built so you're basically just plugging in your API tokens and going.

I've seen people get a fully functional multi-channel agent running in under an hour with it, versus a full weekend doing it from scratch. The skill definitions are clean and well-commented, so you can easily modify them to fit your specific use case once you're up and running. It's not a black box — it's a head start.

What to Do Next

Here's my recommended order of operations:

  1. Start with two channels, not three. Get Slack + Discord working perfectly before adding email. Email has enough quirks (auto-reply loops, HTML formatting, attachment handling) that it deserves focused attention.

  2. Set up identity mapping early. Don't bolt this on later. The first time a user messages you on a second channel, you want their context to carry over. If you wait, you'll have orphaned conversations that never get linked.

  3. Implement routing rules before going live. Especially on Discord. If your agent is in a busy server without proper mention-only triggers, you'll burn through your LLM budget in hours.

  4. Write tests for cross-channel flows. Not optional. The channel simulator is there for a reason. Use it. A cross-channel context bug in production is brutal to debug without it.

  5. Monitor your rate limits from day one. Set up the dashboard, watch the graphs, and tune your limits before you hit them in production during a traffic spike.

The whole point of a multi-channel agent is that your users shouldn't have to think about which channel they're on. They just talk to your agent, wherever they are, and it works. OpenClaw makes that possible without requiring you to become an expert in four different platform APIs.

Build once. Deploy everywhere. Move on to the things that actually matter for your product.

Claw Mart Daily

Get one AI agent tip every morning

Free daily tips to make your OpenClaw agent smarter. No spam, unsubscribe anytime.

More From the Blog