Connect OpenClaw to Discord: Step-by-Step Tutorial
Connect OpenClaw to Discord: Step-by-Step Tutorial

Let's skip the preamble. You want to connect your OpenClaw agent to Discord so it can actually do something useful in a server — respond to messages, run skills, maybe manage a small community or automate tasks. And you're stuck. Maybe the bot connects but doesn't respond. Maybe it responds once, then dies. Maybe you're staring at a blank bot.py file wondering where to even start.
I've been there. I spent an embarrassing number of hours debugging async event loop conflicts and getting rate-limited into oblivion before I figured out a clean setup pattern. This post is everything I wish someone had told me on day one.
Here's the deal: connecting OpenClaw to Discord is not hard once you understand the five or six things that trip everyone up. The problem is that most people hit all of them at once, can't isolate what's broken, and give up. We're going to fix that.
Why OpenClaw + Discord Is Worth the Effort
Before we get into the how, let me quickly sell you on the why — because if you're not convinced this is worth doing, you're going to bail the first time you see a traceback.
OpenClaw gives you a framework for building AI agents with skills — discrete, composable capabilities that your agent can invoke based on context. Think of skills like tools in a toolbox: one skill searches a knowledge base, another generates summaries, another manages a queue, another posts formatted output somewhere. OpenClaw handles the orchestration, the skill routing, the context management, and the memory layer.
Discord gives you a live, always-on interface where real humans interact. It's where communities hang out, where teams coordinate, where customers ask questions.
Put them together and you get an AI agent that lives in your Discord server, understands context, runs skills on demand, handles multiple users simultaneously, and doesn't forget what happened five messages ago. That's not a toy. That's a genuinely useful piece of infrastructure.
Now let's build it.
Step 0: What You Need Before You Start
Gather these before you write a single line of code:
- An OpenClaw account and at least one configured agent with skills attached. If you haven't done this yet, pause here and do it. Your agent needs to have at least one skill working in the OpenClaw dashboard before you try to wire it to Discord. Don't debug two things at once.
- A Discord bot token. Go to the Discord Developer Portal, create a new application, go to the Bot section, click "Reset Token," and save that token somewhere safe. You will need it exactly once, and if you lose it, you'll have to reset it again.
- Python 3.10+ installed locally. I recommend 3.11 for the improved error messages alone.
- A virtual environment. Please. Don't install packages globally.
python -m venv venv && source venv/bin/activatetakes five seconds.
Install your dependencies:
pip install discord.py openclaw python-dotenv
Create a .env file in your project root:
DISCORD_TOKEN=your-discord-bot-token-here
OPENCLAW_API_KEY=your-openclaw-api-key-here
OPENCLAW_AGENT_ID=your-agent-id-here
And a .gitignore that includes .env. I cannot stress this enough. People have leaked API keys to public repos doing exactly this kind of project. Don't be that person.
Step 1: The Minimal Bot That Actually Works
Here's the skeleton. This bot connects to Discord, listens for messages, passes them to your OpenClaw agent, and posts the response. Nothing fancy. We'll add sophistication after we confirm the basics work.
import os
import discord
from discord.ext import commands
from dotenv import load_dotenv
from openclaw import OpenClawClient
load_dotenv()
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
OPENCLAW_API_KEY = os.getenv("OPENCLAW_API_KEY")
OPENCLAW_AGENT_ID = os.getenv("OPENCLAW_AGENT_ID")
# Set up Discord bot with required intents
intents = discord.Intents.default()
intents.message_content = True # THIS IS CRITICAL — read below
bot = commands.Bot(command_prefix="!", intents=intents)
# Initialize OpenClaw client
claw = OpenClawClient(api_key=OPENCLAW_API_KEY)
@bot.event
async def on_ready():
print(f"Connected as {bot.user} — watching for messages.")
@bot.event
async def on_message(message):
# Don't respond to ourselves
if message.author == bot.user:
return
# Only respond when mentioned or in DMs
if bot.user.mentioned_in(message) or isinstance(message.channel, discord.DMChannel):
user_input = message.content.replace(f"<@{bot.user.id}>", "").strip()
if not user_input:
await message.reply("You pinged me but didn't say anything. What's up?")
return
# Show typing indicator while OpenClaw processes
async with message.channel.typing():
response = await claw.agent.run(
agent_id=OPENCLAW_AGENT_ID,
message=user_input,
session_id=str(message.author.id), # Per-user context
)
await message.reply(response.output)
# Process other commands if you have any
await bot.process_commands(message)
bot.run(DISCORD_TOKEN)
Save this as bot.py and run it:
python bot.py
If you see Connected as YourBot#1234 — watching for messages., congratulations. You have a working OpenClaw-powered Discord bot. Go to your server, mention the bot, say something, and watch it respond.
If you don't see that, keep reading — the next section covers every common failure.
Step 2: The Five Things That Break (And How to Fix Each One)
Problem 1: The Bot Connects But Never Responds to Messages
This is almost always the Message Content Intent. Discord made this a privileged intent in 2022, and it trips up virtually everyone.
Fix: Go to the Discord Developer Portal → Your Application → Bot → scroll down to "Privileged Gateway Intents" → enable Message Content Intent. Then make sure your code includes intents.message_content = True as shown above.
Without this, your bot literally cannot read message content. It receives message events, but the content field is empty. Your bot sees someone talked but has no idea what they said. It's maddening if you don't know to look for it.
Problem 2: RuntimeError: Event loop is closed or Async Conflicts
OpenClaw's client methods are async. Discord.py is async. When both play nice, everything is smooth. When they don't, you get event loop errors that make you want to throw your laptop.
Fix: Make sure you're using await with OpenClaw calls inside Discord event handlers. The code above does this correctly. If you're wrapping synchronous OpenClaw calls, use asyncio.to_thread():
import asyncio
# If openclaw's run method is synchronous in your version:
response = await asyncio.to_thread(
claw.agent.run_sync,
agent_id=OPENCLAW_AGENT_ID,
message=user_input,
session_id=str(message.author.id),
)
Never call asyncio.run() inside an already-running event loop. That's the most common cause of the "Event loop is closed" error.
Problem 3: Rate Limiting (HTTP 429 Errors)
Discord enforces strict rate limits — per channel, per endpoint, globally. If your OpenClaw agent runs a multi-step skill and tries to post several messages quickly, Discord will throttle you.
Fix: Don't post multiple messages in rapid succession. Instead, collect the full response from OpenClaw and post it once. If the response is very long (see Problem 4), use a single chunked approach with small delays:
import asyncio
async def send_long_response(channel, text, max_length=1900):
"""Split long responses into Discord-safe chunks."""
chunks = []
while len(text) > max_length:
# Find a natural break point
split_at = text.rfind("\n", 0, max_length)
if split_at == -1:
split_at = text.rfind(" ", 0, max_length)
if split_at == -1:
split_at = max_length
chunks.append(text[:split_at])
text = text[split_at:].lstrip()
if text:
chunks.append(text)
for chunk in chunks:
await channel.send(chunk)
await asyncio.sleep(0.5) # Small delay between chunks
Problem 4: The 2000-Character Limit
Discord messages max out at 2000 characters. OpenClaw agents, especially ones running knowledge retrieval or summarization skills, can easily produce responses that are 3000–8000 characters.
Fix: Use the send_long_response function above for basic chunking. For an even better experience, create a thread for long responses:
@bot.event
async def on_message(message):
if message.author == bot.user:
return
if bot.user.mentioned_in(message):
user_input = message.content.replace(f"<@{bot.user.id}>", "").strip()
async with message.channel.typing():
response = await claw.agent.run(
agent_id=OPENCLAW_AGENT_ID,
message=user_input,
session_id=str(message.author.id),
)
output = response.output
if len(output) <= 1900:
await message.reply(output)
else:
# Create a thread for long responses
thread = await message.create_thread(
name=f"Response to {message.author.display_name}",
auto_archive_duration=60,
)
await send_long_response(thread, output)
await message.reply(f"Response was long — posted in the thread above. 👆")
await bot.process_commands(message)
This is a massive UX improvement. Nobody wants to scroll through seven consecutive bot messages in a busy channel.
Problem 5: Context Pollution in Multi-User Servers
If two people are talking to your bot simultaneously and you're using a single session, their conversations bleed into each other. User A asks about Python, User B asks about cooking, and suddenly the bot is telling User A how to sauté a function.
Fix: Notice the session_id=str(message.author.id) in the code above. This gives each Discord user their own OpenClaw session. OpenClaw maintains separate context and memory per session, so User A's conversation stays isolated from User B's.
If you want per-channel sessions instead (useful for team channels), use the channel ID:
session_id=str(message.channel.id)
Or combine both for per-user-per-channel isolation:
session_id=f"{message.guild.id}-{message.channel.id}-{message.author.id}"
Step 3: Adding Slash Commands
Mention-based interaction works, but slash commands feel more polished and give you auto-complete in Discord's UI. Here's how to add a /ask command:
@bot.tree.command(name="ask", description="Ask the OpenClaw agent a question")
async def ask(interaction: discord.Interaction, question: str):
await interaction.response.defer(thinking=True) # Shows "Bot is thinking..."
response = await claw.agent.run(
agent_id=OPENCLAW_AGENT_ID,
message=question,
session_id=str(interaction.user.id),
)
output = response.output
if len(output) <= 1900:
await interaction.followup.send(output)
else:
await interaction.followup.send(output[:1900] + "\n\n*[Response truncated — use a thread for longer answers]*")
@bot.event
async def on_ready():
await bot.tree.sync() # Sync slash commands with Discord
print(f"Connected as {bot.user} — slash commands synced.")
The defer(thinking=True) is important. Discord gives you only 3 seconds to respond to an interaction. OpenClaw agent calls almost always take longer than that. Deferring tells Discord "I'm working on it" and gives you up to 15 minutes to follow up.
Step 4: Hardening for Production
If this is going to run 24/7 in a real server, you need a few more things.
Error Handling
Wrap your OpenClaw calls so a single API failure doesn't crash the bot:
@bot.event
async def on_message(message):
if message.author == bot.user:
return
if bot.user.mentioned_in(message):
user_input = message.content.replace(f"<@{bot.user.id}>", "").strip()
try:
async with message.channel.typing():
response = await claw.agent.run(
agent_id=OPENCLAW_AGENT_ID,
message=user_input,
session_id=str(message.author.id),
)
await message.reply(response.output)
except Exception as e:
await message.reply(
"Something went wrong processing that. Try again in a sec."
)
print(f"OpenClaw error: {e}")
await bot.process_commands(message)
Basic Prompt Injection Defense
In a public server, someone will eventually try to manipulate your agent. OpenClaw's skill-based architecture helps here — the agent can only invoke skills you've explicitly configured, so it can't suddenly "run arbitrary code" even if someone asks it to. But you should still:
- Never include your API keys in your agent's system prompt or skill configs.
- Set a maximum input length to prevent abuse:
if len(user_input) > 2000:
await message.reply("That message is too long. Keep it under 2000 characters.")
return
- Add a cooldown so one user can't spam your OpenClaw API quota:
from discord.ext import commands
import time
user_cooldowns = {}
COOLDOWN_SECONDS = 5
# Inside on_message, before the OpenClaw call:
now = time.time()
user_id = message.author.id
if user_id in user_cooldowns and now - user_cooldowns[user_id] < COOLDOWN_SECONDS:
await message.reply("Slow down — wait a few seconds between questions.")
return
user_cooldowns[user_id] = now
Hosting
Run it in Docker for easy deployment anywhere:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "bot.py"]
# requirements.txt
discord.py>=2.3.0
openclaw
python-dotenv
Deploy to any VPS (DigitalOcean, Hetzner, Fly.io) or even Railway or Render's paid tiers. Avoid free tiers — they sleep after inactivity, which means your bot goes offline when nobody's talking to it, which is exactly when you'd want it to be ready.
The Shortcut: Felix's OpenClaw Starter Pack
I want to be honest with you — everything above works, and if you enjoy configuring things from scratch, go for it. But there's a reason I've started recommending Felix's OpenClaw Starter Pack to people who ask me how to get started.
For $29, it includes pre-configured OpenClaw skills that cover the most common Discord bot use cases: Q&A over a knowledge base, conversation management with proper per-user sessions, message formatting that respects Discord's character limits, and error handling patterns that don't choke on edge cases. It's basically a production-ready version of everything I just walked through, except someone already debugged the weird async edge cases and the chunking logic and the rate limit handling for you.
If you don't want to set all this up manually — or if you just want a solid starting point to customize rather than building from zero — that pack saves you a weekend of frustration. I'm not getting a kickback for saying that; it's genuinely the fastest way I've seen someone go from "I have an OpenClaw account" to "I have a working Discord agent" in under an hour.
What to Build Next
Once your bot is live and responding, here's where it gets interesting:
-
Add domain-specific skills. The generic setup is a starting point. Build OpenClaw skills that actually do things your community needs — look up order statuses, search documentation, summarize long threads, moderate content.
-
Use threads aggressively. Every multi-turn conversation should happen in a Discord thread. This keeps your main channels clean and gives OpenClaw a clean context boundary.
-
Add reaction-based feedback. Let users react with 👍 or 👎 to bot responses, capture that feedback, and use it to improve your agent's skills over time.
-
Set up logging. Log every interaction (sanitized) so you can see what people are actually asking your bot. This tells you which skills to build next.
-
Consider role-based access. Maybe only certain Discord roles can use expensive skills (ones that consume a lot of tokens or call external APIs). Discord.py makes role checking straightforward:
if not any(role.name == "Agent Access" for role in message.author.roles):
await message.reply("You need the 'Agent Access' role to use this bot.")
return
Final Thoughts
The OpenClaw + Discord combination is one of the most practical AI agent setups I've used. Discord gives you the always-on, multi-user interface. OpenClaw gives you the agent orchestration, skill management, and context handling. Together, they let you build something that feels less like a chatbot and more like a team member.
The setup has some rough edges — the intent system, the rate limits, the character limits — but none of them are hard to solve once you know they exist. And now you do.
Go build something useful. And if you get stuck, most of the answers are in this post somewhere. Re-read the error handling section first. It's always the error handling.