Connecting OpenClaw to Slack, Discord & Email: Complete Beginner Tutorial
Connecting OpenClaw to Slack, Discord & Email: Complete Beginner Tutorial

Look, I'll save you the frustration I went through: getting OpenClaw to talk to Slack, Discord, and email is not hard, but the official docs assume you already know what you're doing. If you're staring at your OpenClaw dashboard wondering how to make your agent actually notify you when something happens — or better yet, respond to messages in Slack — this is the guide I wish I'd had three weeks ago.
We're going to wire up all three integrations from scratch. Real examples, real config, real gotchas. Let's go.
Why You Need Integrations in the First Place
Here's the thing most people miss when they first set up an OpenClaw agent: the agent itself is only half the system. You can build the most brilliant web scraping, data extraction, or monitoring agent in the world, but if the output just sits in a terminal window or a log file, you've built a toy.
The actual value shows up when your agent reaches you where you already are — a Slack channel your team checks fifty times a day, a Discord server where your community hangs out, or a plain old email inbox for stakeholders who don't live in chat apps.
The integrations we're setting up today do three things:
- Outbound notifications — your agent pushes results, alerts, or summaries to Slack, Discord, or email when something meaningful happens.
- Inbound triggers — someone sends a message in Slack or Discord, and your agent wakes up and does something.
- Two-way workflows — an agent monitors a site, posts findings to Discord, and lets users reply to request deeper analysis.
OpenClaw makes all three possible without stitching together five different SaaS tools. Here's how.
Step 1: Set Up Your OpenClaw Agent (The Foundation)
Before we connect anything external, you need a working agent. If you already have one, skip ahead. If not, here's the minimum viable setup:
from openclaw import Browser, Agent
agent = Agent(
name="price-monitor",
description="Monitors product prices and reports changes"
)
@agent.task(schedule="every 2 hours")
async def check_prices():
async with Browser() as browser:
page = await browser.new_page()
await page.goto("https://example-store.com/product/12345")
price = await page.extract_text(".product-price")
title = await page.extract_text(".product-title")
return {
"product": title,
"price": price,
"url": "https://example-store.com/product/12345"
}
This agent checks a price every two hours and returns structured data. Right now, that data goes nowhere useful. Let's fix that.
Step 2: Connecting OpenClaw to Slack
Slack is the most common integration I see people set up, and honestly, it's the most useful one for teams. Here's the full walkthrough.
Create a Slack Webhook
First, go to api.slack.com/apps and create a new app. Choose "From scratch," name it whatever you want (I use "OpenClaw Bot"), and select your workspace.
Under Incoming Webhooks, toggle it on, then click Add New Webhook to Workspace. Pick the channel you want your agent to post to. Copy the webhook URL — it'll look something like:
https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
Configure OpenClaw's Slack Integration
Now, in your OpenClaw project, set up the Slack notifier:
from openclaw import Agent, Browser
from openclaw.integrations import SlackNotifier
slack = SlackNotifier(
webhook_url="https://hooks.slack.com/services/T00000000/B00000000/XXXX",
default_channel="#price-alerts",
bot_name="Price Monitor",
bot_icon=":chart_with_upwards_trend:"
)
agent = Agent(
name="price-monitor",
integrations=[slack]
)
@agent.task(schedule="every 2 hours")
async def check_prices():
async with Browser() as browser:
page = await browser.new_page()
await page.goto("https://example-store.com/product/12345")
price = await page.extract_text(".product-price")
title = await page.extract_text(".product-title")
# Send to Slack
await slack.send(
message=f"*{title}* is now *{price}*",
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f":package: *{title}*\n:moneybag: Current price: *{price}*\n<https://example-store.com/product/12345|View Product>"
}
}
]
)
return {"product": title, "price": price}
A few things to note:
- Use
blocksfor rich formatting. Plain text messages work, but Slack blocks let you add buttons, links, and formatting that actually looks professional. Your team will take the alerts more seriously. - Store your webhook URL in environment variables. Don't hardcode it. Use
os.environ["SLACK_WEBHOOK_URL"]in production. I'm showing it inline here for clarity. - The
default_channelis a fallback. The webhook is already tied to a channel, but if you set up the full Slack API (with OAuth), you can dynamically post to different channels.
Setting Up Inbound (Slack → OpenClaw)
This is where it gets really powerful. You want someone in Slack to type /check-price https://some-url.com and have your agent respond with the current price.
from openclaw.integrations import SlackCommandHandler
commands = SlackCommandHandler(
signing_secret="your-slack-signing-secret",
port=3000
)
@commands.on("/check-price")
async def handle_price_check(payload):
url = payload.text.strip()
async with Browser() as browser:
page = await browser.new_page()
await page.goto(url)
price = await page.extract_text(
selectors=[".product-price", ".price", "[data-price]", ".amount"],
timeout=10000
)
return {
"response_type": "in_channel",
"text": f"Current price at {url}: *{price}*"
}
commands.start()
Notice the multi-selector strategy: selectors=[".product-price", ".price", "[data-price]", ".amount"]. This is one of OpenClaw's best features. Instead of hardcoding a single CSS selector that breaks whenever a site changes its HTML, you provide fallbacks. OpenClaw tries each one in order until something hits. This is the kind of real-world robustness that other frameworks just don't handle well.
Step 3: Connecting OpenClaw to Discord
Discord is similar to Slack conceptually but the setup is a bit different. Here's the play.
Create a Discord Webhook
In your Discord server, go to Server Settings → Integrations → Webhooks. Create a new webhook, assign it to the channel you want, and copy the URL.
Configure the Integration
from openclaw import Agent, Browser
from openclaw.integrations import DiscordNotifier
discord = DiscordNotifier(
webhook_url="https://discord.com/api/webhooks/000000000/XXXXXXXXXXXX",
bot_name="OpenClaw Monitor",
avatar_url="https://your-site.com/bot-avatar.png" # Optional
)
agent = Agent(
name="competitor-tracker",
integrations=[discord]
)
@agent.task(schedule="daily at 9am")
async def daily_report():
async with Browser() as browser:
page = await browser.new_page()
await page.goto("https://competitor.com/pricing")
# Use schema-based extraction for clean data
pricing_data = await page.extract_with_schema(
schema={
"plan_name": "string",
"monthly_price": "number",
"features": "list[string]"
},
description="Extract all pricing plan details"
)
# Format for Discord (uses Markdown)
report_lines = []
for plan in pricing_data:
report_lines.append(
f"**{plan['plan_name']}** — ${plan['monthly_price']}/mo\n"
f"Features: {', '.join(plan['features'][:5])}"
)
await discord.send(
content="## 📊 Daily Competitor Pricing Report\n\n" +
"\n\n".join(report_lines)
)
return pricing_data
Discord Bot (Two-Way Communication)
For inbound messages — where Discord users can interact with your agent — you'll need a proper Discord bot:
from openclaw.integrations import DiscordBot
bot = DiscordBot(
token="your-discord-bot-token",
command_prefix="!"
)
@bot.command("analyze")
async def analyze_url(ctx, url: str):
"""Usage: !analyze https://example.com"""
await ctx.reply("🔍 Analyzing... give me a sec.")
async with Browser() as browser:
page = await browser.new_page()
try:
await page.goto(url, retry_count=3, retry_delay=2000)
content = await page.extract_for_llm(
selector="body",
max_tokens=2000,
format="markdown"
)
await ctx.reply(f"## Analysis of {url}\n\n{content[:1800]}")
except NavigationError:
await ctx.reply(f"❌ Couldn't load {url}. Check the URL and try again.")
bot.start()
The extract_for_llm method is worth highlighting here. Instead of dumping raw HTML (which is massive and useless for most purposes), OpenClaw converts the page content into clean markdown, automatically truncated to fit within your token limit. If you've ever tried to feed a full webpage into an LLM and watched your context window explode, you know why this matters.
Step 4: Connecting OpenClaw to Email
Email is the "boring but essential" integration. It's perfect for stakeholders who aren't in Slack or Discord — executives, clients, or automated reporting pipelines.
from openclaw.integrations import EmailNotifier
email = EmailNotifier(
smtp_host="smtp.gmail.com",
smtp_port=587,
username="your-bot@gmail.com",
password="your-app-password", # Use app passwords, not your real password
from_address="your-bot@gmail.com",
use_tls=True
)
agent = Agent(
name="weekly-report",
integrations=[email]
)
@agent.task(schedule="every monday at 8am")
async def weekly_summary():
async with Browser() as browser:
page = await browser.new_page()
await page.goto("https://analytics-dashboard.com")
# Extract key metrics
metrics = await page.extract_with_schema(
schema={
"total_visitors": "number",
"conversion_rate": "string",
"top_pages": "list[string]"
},
description="Extract weekly analytics summary"
)
# Send formatted email
await email.send(
to=["team-lead@company.com", "ceo@company.com"],
subject=f"Weekly Analytics Report — {metrics['total_visitors']} visitors",
html_body=f"""
<h2>Weekly Analytics Summary</h2>
<ul>
<li><strong>Total Visitors:</strong> {metrics['total_visitors']}</li>
<li><strong>Conversion Rate:</strong> {metrics['conversion_rate']}</li>
<li><strong>Top Pages:</strong> {', '.join(metrics['top_pages'][:5])}</li>
</ul>
<p>Generated automatically by OpenClaw.</p>
""",
plain_text_fallback=f"Visitors: {metrics['total_visitors']}, "
f"Conversion: {metrics['conversion_rate']}"
)
return metrics
Protip: Always include a plain_text_fallback. Some email clients strip HTML, and some recipients have accessibility settings that prefer plain text. It takes ten seconds and prevents your report from arriving as a blank email.
Handling Email Replies (Inbound)
This is more advanced, but OpenClaw supports IMAP polling for inbound emails:
from openclaw.integrations import EmailListener
listener = EmailListener(
imap_host="imap.gmail.com",
username="your-bot@gmail.com",
password="your-app-password",
folder="INBOX",
poll_interval=60 # Check every 60 seconds
)
@listener.on_new_email(subject_contains="ANALYZE")
async def handle_analysis_request(email_message):
url = email_message.body.strip()
async with Browser() as browser:
page = await browser.new_page()
await page.goto(url)
content = await page.extract_for_llm(
selector="body",
max_tokens=3000,
format="markdown"
)
await email.send(
to=[email_message.from_address],
subject=f"RE: {email_message.subject}",
html_body=f"<h2>Analysis Results</h2><pre>{content}</pre>"
)
listener.start()
Step 5: Combining All Three (The Real Power Move)
Here's where things get genuinely useful. Most agents shouldn't just post to one channel — they should notify different audiences in different ways:
from openclaw import Agent, Browser
from openclaw.integrations import SlackNotifier, DiscordNotifier, EmailNotifier
slack = SlackNotifier(webhook_url=os.environ["SLACK_WEBHOOK"])
discord = DiscordNotifier(webhook_url=os.environ["DISCORD_WEBHOOK"])
email_notifier = EmailNotifier(
smtp_host="smtp.gmail.com",
smtp_port=587,
username=os.environ["EMAIL_USER"],
password=os.environ["EMAIL_PASS"],
from_address=os.environ["EMAIL_USER"],
use_tls=True
)
agent = Agent(
name="outage-monitor",
integrations=[slack, discord, email_notifier]
)
@agent.task(schedule="every 5 minutes")
async def check_site_health():
async with Browser() as browser:
page = await browser.new_page()
try:
await page.goto(
"https://your-app.com",
timeout=15000,
retry_count=2,
retry_delay=3000
)
status = await page.evaluate("() => document.readyState")
if status != "complete":
raise Exception(f"Page not fully loaded: {status}")
# Everything's fine — no notification needed
return {"status": "healthy"}
except Exception as e:
# ALERT EVERYONE
alert_msg = f"🚨 Site outage detected: {str(e)}"
# Instant Slack alert for the engineering team
await slack.send(
message=alert_msg,
channel="#incidents"
)
# Discord for the broader team
await discord.send(content=alert_msg)
# Email for management
await email_notifier.send(
to=["vp-engineering@company.com"],
subject="🚨 SITE OUTAGE DETECTED",
html_body=f"<h1>Outage Alert</h1><p>{str(e)}</p>"
)
return {"status": "down", "error": str(e)}
This is a real monitoring agent that checks your site every five minutes and blasts alerts across three channels if something breaks. The engineering team gets it in Slack (where they live), the broader org sees it in Discord, and the VP gets an email they can't miss.
Common Gotchas and How to Fix Them
After setting up dozens of these integrations, here are the mistakes I see over and over:
1. Webhook URLs in source code. Use environment variables. Always. One accidental git push and someone is posting memes to your #incidents channel.
2. No error handling on the integration itself. What happens if Slack's API is down when your agent tries to send an alert? Wrap your notification calls in try/except blocks or use OpenClaw's built-in retry:
await slack.send(
message="Alert!",
retry_count=3,
retry_delay=5000,
fallback=lambda: email_notifier.send(
to=["fallback@company.com"],
subject="Slack delivery failed",
html_body="<p>Original alert: ...</p>"
)
)
3. Rate limiting yourself. Discord webhooks have a rate limit of about 30 requests per minute. Slack is more generous, but still has limits. If your agent runs every minute and sends multiple messages, you'll get throttled. Batch your notifications or use OpenClaw's PoliteSession concepts even for API calls.
4. Not testing locally first. Before deploying, run your agent once with log_level=LogLevel.DEBUG and verify that messages actually arrive. A typo in a webhook URL will fail silently more often than you'd think.
The Shortcut: Skip the Manual Setup
I've walked you through the full manual configuration because understanding how it works matters. But I'll be honest — when I was first setting all of this up, I burned a solid weekend on webhook configs, message formatting, and debugging SMTP authentication quirks.
If you don't want to set this all up manually, Felix's OpenClaw Starter Pack on Claw Mart includes pre-built integration skills for Slack, Discord, and email — already configured with proper error handling, retry logic, and message formatting templates. It's $29, and it genuinely saved me hours when I used it to spin up a second project. The Slack and Discord notification skills alone are worth it because they handle edge cases (rate limits, message length truncation, rich embeds) that you'll otherwise discover the hard way.
It's not required — everything in this post works on its own. But if you're trying to ship something this week instead of next month, it's the fastest path I've found.
Where to Go From Here
Once your integrations are live, the natural next steps are:
- Add conditional logic. Don't notify on every run — only when something changes. Store previous results and compare.
- Build interactive workflows. Let Slack users click buttons to trigger deeper analysis, or reply to Discord messages to adjust monitoring parameters.
- Set up escalation chains. If no one acknowledges a Slack alert within 10 minutes, send an email. If no email response in 30 minutes, send an SMS (yes, OpenClaw has a Twilio integration too).
- Monitor your monitors. Use OpenClaw's built-in health checks to make sure your agents are actually running. An agent that silently dies is worse than no agent at all.
The whole point of connecting OpenClaw to these platforms is turning your agents from scripts-that-run-somewhere into tools-your-team-actually-uses. The agent does the work. The integrations make it visible. And once your team starts relying on those notifications, you'll wonder how you ever operated without them.
Now go build something.
Recommended for this post